临时对象
C++中有这样一种对象:它在代码中看不到,但是确实存在。它就是临时对象—由编译器定义的一个没有命名的非堆对象(non-heap object)。
临时对象通常产生于以下4种情况:
- 类型装换
- 按值传递
- 按值返回
- 对象定义
1、类型转换
它通常是为了让函数调用成功而产生临时对象。发生于 “传递某对象给一个函数,而其类型与它即将绑定上去的参数类型不同” 的时候。
例如:
void test(const string& str);
char buffer[] = "buffer";
test(buffer); // 此时发生类型转换
此时,编译器会帮你进行类型转换:它产生一个类型为string的临时对象,该对象以buffer为参数调用string constructor。当test函数返回时,此临时对象会被自动销毁。
注意:对于引用(reference)参数而言,只有当对象被传递给一个reference-to-const参数时,转换才发生。如果对象传递给一个reference-to-non-const对象,不会发生转换。
例如:
void upper(string& str);
char lower[] = "lower";
upper(lower); // 此时不能转换,编译出错
此时如果编译器对reference-to-non-const对象进行了类型转换,那么将会允许临时对象的值被修改。而这和程序员的期望是不一致的。
试想,在上面的代码中,如果编译器允许upper运行,将lower中的值转换为大写,但是这是对临时对象而言的,char lower[]的值还是“lower”,这和你的期望一致吗?
有时候,这种隐式类型转换不是我们期望的,那么我们可以通过声明constructor为explicit来实现。explicit告诉编译器,我们反对将constructor用于类型转换。
例如:
explicit string(const char*);
2、按值传递
这通常也是为了让函数调用成功而产生临时对象。当按值传递对象时,实参对形参的初始化与
T formalArg = actualArg
的形式等价。
例如:
1
2
3
|
void test(T formalArg);
T actualArg;
test(actualArg);
|
此时编译器产生的伪码为:
1
2
3
4
|
T _temp;
_temp.T::T(acutalArg); // 通过拷贝构造函数生成_temp
g(_temp); // 按引用传递_temp
_temp.T::~T(); // 析构_temp
|
因为存在局部参数formalArg,test()的调用栈中将存在formalArg的占位符。编译器必须复制对象actualArg的内容到formalArg的占位符中。所以,此时编译器生成了临时对象。
3、按值返回
如果函数是按值返回的,那么编译器很可能为之产生临时对象。
例如:
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
|
class Integer {
public:
friend Integer operator+(const Integer& a, const Integer& b);
Integer(int val=0): value(val) {
}
Integer(const Integer& rhs): value(rhs.value) {
}
Integer& operator=(const Integer& rhs);
~Integer() {
}
private:
int value;
};
Integer operator+(const Integer& a, const Integer& b) {
Integer retVal;
retVal.value = a.value + b.value;
return retVal;
}
Integer c1, c2, c3;
c3 = c1 + c2;
|
编译器生成的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct Integer _tempResult; // 表示占位符,不调用构造函数
operator+(_tempResult, c1, c2); // 所有参数按引用传递
c3 = _tempResult; // operator=函数执行
Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {
struct Integer retVal;
retVal.Integer::Integer(); // Integer(int val=0)执行
retVal.value = a.value + b.value;
_tempResult.Integer::Integer(retVal); // 拷贝构造函数Integer(const Integer& rhs)执行,生成临时对象。
retVal.Integer::~Integer(); // 析构函数执行
return;
}
return retVal;
}
|
如果对operator+进行返回值优化(RVO:Return Value Optimization),那么临时对象将不会产生。
例如:
1
2
3
|
Integer operator+(const Integer& a, const Integer& b) {
return Integer(a.value + b.value);
}
|
编译器生成的伪代码:
1
2
3
4
5
|
Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {
_tempResult.Integer::Integer(); // Integer(int val=0)执行
_tempResult.value = a.value + b.value;
return;
}
|
对照上面的版本,我们可以看出临时对象retVal消除了。
4、对象定义
例如:
1
2
3
|
Integer i1(100); // 编译器肯定不会生成临时对象
Integer i2 = Integer(100); // 编译器可能生成临时对象
Integer i3 = 100; // 编译器可能生成临时对象
|
然而,实际上大多数的编译器都会通过优化省去临时对象,所以这里的初始化形式基本上在效率上都是相同的。
临时对象的生命期
有关临时对象的生命周期有三种情况:
1.一般情况
临时性对象的被摧毁,应该是对完整表达式(full-expression)求值过程中的最后一个步骤。该完整表达式造成临时对象的产生。
例:
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
27
28
29
30
31
32
|
#include<iostream>
using namespace std;
class A
{
public:
A(int i): m_i(i)
{
cout << "A(): "<< m_i << endl;
}
~A()
{
cout << "~A(): "<< m_i << endl;
}
A operator+(const A& rhs)
{
cout << "Aoperator+(const A& rhs)" << endl;
return A(m_i + rhs.m_i);
}
int m_i;
};
int main()
{
A a1(1), a2(2);
a1 + a2;
cout <<"------------------------------------" << endl; //运行到这里,a1 + a2产生的临时变量已经被释放
return 0;
}
|
运行结果:

特例1:含有表达式执行结果的临时性对象
凡含有表达式执行结果的临时性对象,应该存留到object的初始化操作完成为止。
例:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#include<iostream>
using namespace std;
class A
{
public:
A(int i = 0): m_i(i)
{
cout << "A(): "<< m_i << endl;
}
~A()
{
cout << "~A(): "<< m_i << endl;
}
A operator+(const A& rhs)
{
cout << "Aoperator+(const A& rhs)" << endl;
return A(m_i + rhs.m_i);
}
A& operator=(const A& rhs)
{
cout << "A&operator=(const A& rhs)" << endl;
m_i += rhs.m_i;
return *this;
}
int m_i;
};
int main()
{
A a1(1), a2(2);
A a3;
a3 = a1 + a2; //a1 + a2产生的临时变量在a3的赋值操作完成后,才释放
return 0;
}
|
运行结果:

特例2. 临时性对象被绑定于一个reference
如果一个临时性对象被绑定于一个reference,对象将残留,直到被初始化之reference的生命结束,或直到临时对象的生命范畴(scope)结束——视哪一种情况先到达而定。
例:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#include<iostream>
using namespace std;
class A
{
friend ostream& operator<<(ostream& os, const A&);
public:
A()
{
}
A(const A&)
{
cout << "A(constA&)" << endl;
}
~A()
{
cout << "~A()"<< endl;
}
};
ostream& operator<<(ostream&os, const A&)
{
os << "ostream& operator<<(ostream&os, const A&)" << endl;
return os;
}
const A&f(const A& a)
{
return a;
}
int main(intargc, char* argv[])
{
{
const A& a = A();
cout << "-------------------"<< endl;
}//直到被初始化之reference的生命结束
cout << f(A()) << endl; //直到临时对象的生命范畴(scope)结束:
//临时对象的const引用在f的参数上(而不是返回值)。
//这个引用在f()返回的时候就结束了,但是临时对象未必销毁。
cout << "-------------------" << endl;
return 0;
}
|
运行结果:

具名返回值优化(Named Return Value)
NRV是Named Return Value的简称。NRV优化简单的说:有一条语句,A a = f();其中f()是一个函数,函数里边申请了一个A的对象b,然后把对象b返回。在对象返回的时候,一般情况下要调用拷贝函数,把函数f()里边的局部对象b拷贝到函数外部的对象a。但是如果用了NRV优化,那就不必要调用拷贝构造函数,编译器可以这样做,把a的地址传递进函数f(),然后不让f()申请要返回的对象b的空间,用a的地址来代替b的地址,这样当要返回对象b的时候,就不必要拷贝了,因为b就是a,省去了局部变量b,省去了拷贝的过程。
函数返回局部对象的拷贝的一般实现方式
1
2
3
4
5
6
7
|
class X;
X bar()
{
X x1;
// 处理 x1..
return x1;
}
|
针对”Xbar()”这样的函数,是返回class X的一个对象的拷贝。其返回值是一个对象,比如叫做x2。在执行return时,x2通过调用拷贝构造函数,拷贝对象x1来实现其初始化。也就是说,这里会存在两个对象x1、x2。那么这种返回对象的拷贝,是怎么实现的呢?一般来说,C++编译器会将上段代码中bar的实现转换成如下的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void bar(X& __result) // 加上一个额外参数
{
// 预留x1的内存空间
X x1;
// 编译器产生的默认构造函数的调用,
x1.X::X();
// 处理 x1..
// 编译器产生的拷贝操作
__result.X::X(x1);
return;
}
X x2; //这里只是预留内存,执行默认构造函数,并未调用初始化函数
bar(x2);
|
通过上述代码,我们可见编译器对于返回对象拷贝的处理方式。
1、函数添加一个额外参数,为返回对象的引用;
2、函数调用前,先申请欲返回对象x2的内存空间;
3、将对象x2的引用传入函数中,并在函数返回前,调用x2的拷贝构造函数。
通过上述实现方式
X x2 = bar();
被转换成了
X x2;
bar(x2);
NRV(Named Return Value)优化
上面的实现中,存在着x1、x2两个对象,而x1的生命周期转瞬即逝。而且对于bar()的调用者来说,根本就没有x1这个对象,调用者想要的只有x2。这样的实现能不能够将其优化变得更快呢。编译器有一种优化方式,直接将x2替代x1。编译器转换后的伪代码如下。
1
2
3
4
5
6
7
|
void bar(X& __result)
{
// 调用__result的默认构造函数
__result.X::X();
// 处理__result
return;
}
|
从代码看出,这里只有一个对象,也就是传入的x2。NRV优化后的实现,比原来的实现省去了如下操作:
-
在堆栈中预留x1的内存;
-
调用X的默认构造函数,构造x1
-
调用X的拷贝构造函数,构造x2
-
调用x1的析构函数
-
堆栈中回收x1的内存
但是多了一个操作,就是调用X的默认构造函数,构造x2。对于函数的调用者(只关心x2不关心x1)来说,只有一个区别,就是x2的构造方式由调用拷贝构造函数,转变成了调用默认构造函数。
##NRV优化的触发条件
《深度探索C++对象模型》中作者提到程序员必须给class X定义拷贝构造函数才能触发NRV优化,不然还是按照最初的较慢的方式执行。在现在的编译器中NRV优化和拷贝构造函数是否定义没关系。
因为早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有客户(程序员)显式提供的拷贝构造函数:如果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。”
作者在书中一直都是以cfront来举例说明的,所以其才会有NRV开关的说法。
测试如下:
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
27
28
29
30
|
class CTest
{
public:
CTest()
{
cout << "CTest()" << this << endl;
}
CTest(const CTest& rcTest)
{
cout << "CTest(CTest)" << this << endl;
}
~CTest()
{
cout << "~CTest()" << this << endl;
}
private:
int a;
};
CTest foo()
{
CTest oTestInFoo;
return oTestInFoo;
}
int main()
{
CTest oTest = foo();
return 0;
}
|
在不同的编译环境的执行结果分别为:
VS2005(Debug)

VS2005(Release)

g++(-c -o)

也就是说,在vs2005的release环境和g++中,都触发了编译器的NRV优化。
然后,再将代码中的class CTest的拷贝构造函数去掉,执行结果依次为:
VS2005(Debug)

VS2005(Release)

g++(-c -o)

我们去掉class CTest的拷贝构造函数后,按照《深度探索》中所说,class CTest的拷贝动作只需要bitwise copy就可以实现。所以编译器也不会给其合成一个implicit的拷贝构造函数。也就是说,这个时候class CTest是没有拷贝构造的。但执行结果和去掉拷贝构造前一样,vs2005译编的release程序和g++中,均使用了NRV优化。
最后将CTest的代码改为
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 CSub
{
public:
CSub(){}
CSub(const CSub& rcSub)
{
cout << "CSub(CSub)" << endl;
}
};
class CTest
{
public:
CTest()
{
cout << "CTest()" << this << endl;
}
~CTest()
{
cout << "~CTest()" << this << endl;
}
private:
int a;
CSub oSub;
};
|
这个时候的执行结果为:
VS2005(Debug)

VS2005(Release)

g++(-c -o)

实验结果显示,不管是类有explicit的构造、implicit的拷贝构造还是没有拷贝构造,在vs2005的Release和g++下都会触发NRV优化,在vs2005(Debug)下都没有NRV优化。所以可以得出结论,在这两个编译器中,NRV优化和拷贝构造函数是否定义没关系。
现在已经确定的是vs(Release)和g++都会执行NRV优化。而NRV优化会导致原本预想中的调用拷贝构造函数变成调用别的构造函数(视函数中的对象调用的构造函数而定)。这一点一定要注意,因为一旦这个时候,拷贝构造函数和别的构造函数提供的功能不同(其实一直都不应该这样),会导致debug和release出现执行结果不同的情况。
参考:
http://www.myexception.cn/cpp/1456175.html
http://blog.csdn.net/zha_1525515/article/details/7170059
http://blog.csdn.net/chdhust/article/details/9617567