首先问一个问题,什么是c++中的临时对象呢?
有时候,在求表达式值期间,编译器必须创建临时对象(temporary object)。像其他任何对象一样,它们需要存储空间,并且必须能够构造和销毁。区别是从来看不到它们——编译器负责决定它们的去留以及它们存在的细节。(c++编程思想第一卷(第一版)183页)
从上面一段话中,我们能够窥得临时对象的一些基本特征,那么它的定义又是怎样呢?C++中,我们说,对于一切非显示定义和生成的对象均为临时对象。
现在我们终于知道什么是临时对象了。那么你可能接着要问了,既然编译器负责它们的存活去留,我们为什么还要关注临时对象呢?好问题,但我们先耐住性子接着往下看,我将逐一的为您分析讲解。
1、正本朔源,临时对象何时产生?
一般的来说,在如下的几种情况中会产生临时对象:
1、隐式类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor)。(C++ Primer中文版(第五版)第263页)
如果定义一个构造函数,这个构造函数能把另一类型对象(或引用)作为它的单个参数,那么这个构造函数允许编译器执行自动类型转换。(c++编程思想第一卷(第一版)296页)
看下面代码:
#include <iostream>
class CTest
{
public:
CTest(const int&){std::cout<<"call constructor"<<std::endl;}
~CTest(){std::cout<<"call destructor"<<std::endl;}
};
void func(CTest);
void func(CTest){}
int main()
{
int number(5);
func(number);
return 0;
}
编译并执行,结果如下:
从上图可以看到,我们确实构造了一个CTest临时对象并且析构了。但是,有时候这种隐式转换并不是我们想要的,所以可以对构造函数加上explicit 关键字。关于这方面的内容,感兴趣的读者们可以去自行查阅资料。
2、按值传递参数
在我们学习c语言的时候,老师们一定都讲过c语言的函数传参方式是传值方式(pass-by-value)。
实际上,当按值传参时,编译器会创建一个临时对象并按位拷贝实参的值,当作实参的副本,在函数作用域中操作的都是这个副本,与原实参无关。参考如下代码:
#include <iostream>
class CTest
{
public:
CTest(){std::cout<<"call constructor"<<std::endl;}
CTest(const CTest& other){std::cout<<"call copy-constructor"<<std::endl;}
~CTest(){std::cout<<"call destructor"<<std::endl;}
};
void func(CTest);
int main()
{
CTest ctest;
std::cout<<"pass by value."<<std::endl;
func(ctest);
return 0;
}
void func(CTest){}
// g++ -std=c++11 xxx.cpp -o xxx
// 这里采用的gcc版本为gcc 5.3.1
编译并执行以上代码,结果如下:
从上面的结果来看,在按值传参的时候,编译器调用了拷贝构造函数构造了一个临时对象,并在离开void func(CTest)
函数的作用域时析构。
3、函数按值返回
与按值传参一样,函数按值返回一个自定义类型的对象时,同样会调用拷贝构造函数创建一个临时对象。具体可参考下列代码。
#include <iostream>
#include <cstdlib>
using namespace std;
class CTest
{
public:
CTest(){cout << "call constructor"<< endl;}
CTest(const CTest& other){cout << "call copy-constructor"<< endl;}
~CTest(){cout << "call destructor"<< endl;}
};
CTest func();
int main()
{
func();
system("pause");
return 0;
}
CTest func()
{
CTest test;
return test;
}
// !!!注意,这里是在visual studio 2013 Debug模式下编译执行的,不同的编译器出来的结果可能不一样。原因后面有介绍。
打开cmd,在console下用命令行执行上程序,结果如下:
我们来分析下上述代码。在main()
函数中,在表达式func()
处创建了一个临时对象,用来存放func()
函数中返回的对象。在func()
函数中,首先构造了test对象,然后调用拷贝构造函数将test对象拷贝外部临时对象的存储单元内,然后test对象被析构。
4、建立无名对象
当建立一个匿名的非堆(non-heap)对象,这时候也会产生临时对象。
执行如下代码:
#include <iostream>
class CTest
{
CTest(){std:cout<<"constructor"<<std::endl;}
~CTest(){std::cout<<"destructor"<<std::endl;}
};
int main()
{
CTest();
return 0;
}
可以看到,我们这里确实产生了一个临时对象。
5、表达式求值时
考虑以下代码
#include <string>
#include <cstdio>
int main()
{
std::string str1 = "Hello ";
std::string str2 = "World";
printf("str1+str2 = %s",(str1+str2).c_str());
return 0;
}
执行结果如下:
在这里我们输出的实际上是由表达式str1 + str2
得到的临时对象的值。
2、风中秉烛,临时对象何时死亡?
通过上面的表述,我们已经知道了何时会产生临时对象了。那么,临时对象什么时候死亡呢?毕竟,我们要是用了个已死的对象或者资源,那就哭都哭不出来了…
在c++中规定了,在生成临时对象的最大表达式结束处即是临时对象的析构处(EOS,end of statement),也就是临时对象的死亡时刻。
在1.5中的表达式求值的代码中,str1+str2
生成的临时对象会在最大表达式结束处析构,在这里的最大表达式为printf()
函数调用,即在printf()
函数调用完成后才析构str1 + str2
生成的临时对象。
将1.5中的代码稍稍变形,改为如下:
#include <string>
#include <cstdio>
int main()
{
std::string str1 = "Hello ";
std::string str2 = "World";
const char *p = (str1+str2).c_str();
printf("str1+str2 = %s",p);
return 0;
}
会发生什么?不知道!天知道。通过上面的分析我们可以知道,在printf()
函数调用之前,由表达式str1 + str2
创建的临时对象就已经被析构了,而标准库的实现上是肯定在析构的时候就把所有占用的资源都释放了的。(不排除某些不太合格的程序员根本就不去管资源的释放,那样在当下的情境下,反而不会出问题)。所以,这时候我们的指针p
就成了悬挂指针了,而对一个悬挂指针的操作,想想就令人害怕不是么?
总结:这一小节介绍了临时对象的出生和死亡,下面我们还会介绍关于临时变量的一些用法、性质和隐藏的坑还有右值、左值、右值引用、移动语义等与临时变量密切相关的内容。敬请期待 :)
参考资料:
《c++编程思想第一卷》
《c++ Primer 中文版第五版》
http://blog.csdn.net/imyfriend/article/details/12886577
http://blog.csdn.net/waljl/article/details/51144351
http://www.cnblogs.com/daocaoren/archive/2011/07/19/2110258.html