0%

C++中的非常量引用不能指向临时对象

引言:本文主要介绍为什么C++标准规定非常量引用不能指向临时对象,以及什么情况下会产生临时对象。

举例

非常量引用指向临时对象 —— 即:将临时对象传递给非常量引用类型。比如以下情况:

1
2
3
4
5
6
7
8
//在Rational类中定义如下
//重载2个操作符函数:“+”号和“<<” 输出符号
friend ostream &operator<< (ostream& outs,Rational &rational);
Rational operator+(Rational &secondRational);

Rational a(4,5);
Rational b(1,3);
cout << a+b << endl;

a+b 函数返回一个Rational类的临时对象;而 << 操作函数的参数却是 Rational & rational 。这种情况函数会报错如下:

no match for 'operator<<' in 'std::cout << a.Rational::operator+(((Rational&)(& b)))'

虽然定义了 operator<< 。但是编译器对 a+b 返回的临时对象不买单,直接报了个“没有匹配的函数” 的错误。

分析

以C++的语义来说,如果一个程序员只想传递参数给函数,而不希望函数修改传入的参数时,那么,或者使用值传递,或者采用常量型引用。考虑到大对象复制时产生的开销,一般使用常量型引用const &。如果函数的参数是某个类型的一个非常量的引用,那就相当于告诉编译器,程序员希望得到函数对参数的修改结果。
临时变量是由编译器生成的,C++语言规范没规定编译器生成临时变量的规则,程序员无法得知由编译器生成的临时变量的名字,程序员无法访问那个临时变量。这意味着,以引用的方式传递一个临时变量做为函数参数,如果函数内部对此临时变量做了修改,那么函数返回后,程序员无法获得函数对临时变量的修改。函数对临时变量所做出的所有更改,都将丢失。

一方面,在函数申明中,使用非常量型的引用告诉编译器你需要得到函数对某个对象的修改结果,可是你自己又不给变量起名字,直接丢弃了函数的修改结果,编译器只能说:“大哥,你这是干啥呢,告诉我把结果给你,等我把结果给你了,你又直接给扔了,你这不是在玩我吗?”

同时,C++的标准 为了防止给常量或临时变量(只有瞬间的生命周期)赋值(易产生bug),只许使用const引用之。

解决

有两种解决办法,一是将非常量引用改为值传递,二是改为常量引用。

临时对象

c++中的临时对象是看不见的,它不出现在源码中。建立一个未命名的非堆对象会产生一个临时对象,临时对象会在以下几种情况下出现。

以值传递的方式给函数传参

按值传递时,首先将需要传给函数的参数,调用拷贝构造函数创建一个副本,这个副本就是临时变量,所有在函数里的操作都是针对这个副本,也正是因为这个原因,在函数体里对该副本进行任何操作都不会影响原参数。(关于临时对象的生命周期,并不是用过之后立马析构,其生命周期根据实际情况确定,比如这种情况,其生命周期可能存在于正割函数体内,又比如一个常量引用指向的临时对象,其声明周期由该常量引用的声明周期决定)

这也是拷贝构造函数不能为值传递的原因,因为值传递会创建临时对象,而临时对象的创建仍然要调用拷贝构造函数,这样就会递归调用下去,造成栈溢出。

隐式类型转换

这种情况比较容易理解。

函数返回对象

当函数需要返回一个对象,由于这个对象的生命周期只存在于函数体内部,所以它会在栈中创建一个临时对象(调用拷贝构造函数),存储函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
public:
Person() {};
Person(const Person& p) {
cout << "Copy Constructor" << endl;
}
Person& operator = (const Person& p) {
cout << "Assign operator" << endl;
return *this;
}

private:
int age;
string name;
};

Person do() {
Person p;
return p;
}

int main() {
Person p
p = do();
return 0;
}

这种情况就会调用一次拷贝构造函数在栈上创建一个临时对象,然后调用赋值运算符将该临时对象复制给p。

手动调用构造函数

**这里的手动调用指的是代码上显式调用而非编译器自己调用,**如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
class cls
{
public:
int a;
cls(int i) {
a = i;
}

cls() {
cls(0);
}

void show() {
std::cout << "a = " << a << std::endl;
}
};

int main() {
cls c;
c.show();
return 0;
}

会打印出预期之外的结果,因为在调用cls(0)时,其实产生了一个cls类的临时对象,这个临时对象,而我们希望的是初始化对象c的成员a,但这里初始化的是临时对象的a。可以改成*this = cls(0);,这其实是和如下代码一个原理:

1
2
// 初始化对象
cls a = cls(10);

看起来调用了两次拷贝构造函数,临时对象一次,构造a一次,但实际上编译器做了如下优化,只调用了一次:

1
cls a = 10;