2023年12月7日发(作者:)

C++的临时变量它们是被神所遗弃的孩子,没有人见过它们,更没有人知道它们的名字.它们命中注定徘徊于命运边缘高耸的悬崖和幽深的深渊之间,用自己短暂的生命抚平了生与死之间的缝隙.譬如朝露,却与阳光无缘.是该为它们立一座丰碑的时候了,墓铭志上写着:我来了,我走了,我快乐过. 许多人对临时变量的理解仅仅限于: string temp; 其实,从C++的观点来看,这根本就不是临时变量,而是局部变量.

C++的临时变量是编译器在需要的时候自动生成的临时性变量,它们并不在代码中出现.但是它们在编译器生成的二进制编码中是存在的, 也创建和销毁.在C++语言中,临时变量的问题格外的重要,因为每个用户自定义类型的临时变量都要出发用户自定义的构造函数和析构函数(如果用户提供了)

又是该死的编译器!又该有人抱怨编译器总在自己背后干着偷偷摸摸的事情了.但是如果离开了编译器的这些工作,我们可能寸步难行.

如果X是一个用户自定义的类型,有默认构造函数,拷贝构造函数,赋值运算函数,析构函数(这也是类的4个基本函数),那么请考虑以下代码: X get(X arg) { return arg; }

X a; X b = get(a); 即使是这么简单的代码也是很难实现的 让我们分析一下代码执行过程中发生了什么? 首先我要告诉你一个秘密:对于一个函数来说,无论是传入一个对象还是传出一个对象其实都是不可能的. 让一个函数传入或传出一个内置的数据类型,例如int,是很容易的,但是对于用户自定义类型得对象却非常的困难,因为编译器总得找地方为这些对象 写上构造函数和析构函数,不是在函数内,就是在函数外,除非你用指针或引用跳过这些困难

那么怎么办?在这里,编译器必须玩一些必要的小花招,嗯,其中的关键恰恰就是临时变量

对于以对象为形参的函数: void foo(X x0) { } X xx; foo(xx); 编译器一般按照以下两种转换方式中的一种进行转换 1.在函数外提供临时变量 void foo(X& x0) //修改foo的声明为引用 { } X xx; //声明xx X::X(xx); //调用xx的默认构造函数 X __temp0; //声明临时变量__temp0 X::X(__temp0, xx); //调用__temp0的拷贝构造函数 foo(__temp0); //调用foo X::~X(__temp0); //调用__temp0的析构函数 X::~X(xx); //调用xx的析构函数 2.在函数内提供临时变量 void foo(X& x0) //修改foo的声明为引用 { X __temp0; //声明临时变量__temp0 X::X(__temp0, x0); //调用__temp0的拷贝构造函数 X::~X(__temp0); //调用__temp0的析构函数

} X xx; //声明xx X::X(xx); //调用xx的默认构造函数 foo(xx); //调用foo X::~X(xx); //调用xx的析构函数

无论是在函数的内部声明临时变量还是在函数的外部声明临时变量,其实都是差不多的,这里的含义是说既然参数要以传值的 语意传入函数,也就是实参xx其实并不能修改,那么我们就用一个一摸一样临时变量来移花接木,完成这个传值的语意 但是这样做也不是没有代价,编译器要修改函数的声明,把对象改为对象的引用,同时修改所有函数调用的地方,代价确实巨大啊, 但是这只是编译器不高兴而已,程序员和程序执行效率却没有影响 对于以对象为返回值的函数: X foo() { X xx; return xx; } X yy = foo(); 编译器一般按照以下方式进行转换 void foo(X& __temp0) //修改foo的声明为引用 { X xx; //声明xx X::X(xx); //调用xx的默认构造函数 __temp0::X::X(xx); //调用__temp0的拷贝构造函数 X::~X(xx); //调用xx的析构函数 }

X yy; //声明yy X __temp0; //声明临时变量__temp0 foo(__temp0); //调用foo X::X(yy, __temp0); //调用yy的拷贝构造函数 X::~X(__temp0); //调用__temp0的析构函数 X::~X(yy); //调用yy的析构函数

既然我们已经声明了yy,为什么还要紧接着声明__temp0,其实这里完全可以把yy和临时变量合一 优化后,上面的代码看起来象这个样子: void foo(X& __temp0) //修改foo的声明为引用 { X xx; //声明xx X::X(xx); //调用xx的默认构造函数 __temp0::X::X(xx); //调用__temp0的拷贝构造函数 X::~X(xx); //调用xx的析构函数 }

X yy; //声明yy foo(yy); //调用foo X::~X(yy); //调用yy的析构函数

嗯,怎么说呢,这算是一种优化算法吧,其实这各个技巧已经非常普遍了,并拥有一个专门的名称Named Return Value(NRV)优化 NRV优化如今被视为标准C++编译器的一个义不容辞的优化操作(虽然其需求其实超出了正式标准之外) 除了以类为参数以外,如果参数的类型是const T&类型,这也可能导致临时变量 void fun(const string& str) const char* name = "wgs"; fun(name); 嗯,还记得在const文档中的论述吗?对于这种特殊的参数类型,编译器是很乐意为你做自动转换的工作的,代价嘛,就是一个临时变量, 不过如果是你自己去做,大概就只能声明一个局部变量了

为什么函数和临时变量这么有缘,其实根本的原因在于对象传值的语意,这一个也是为什么C++中鼓励传对象地址的原因

和函数的情况类似的,还有一大类情况是临时变量的乐土,那就是表达式 string s,t; printf("%s", s + t); 这里s+t的结果该放在什么地方呢?只能是临时变量中.

这个printf语句带来了新的问题,那就是"临时变量的生命期"是如何的? 对于函数的情况,我们已经看到了,临时变量在完成交换内容的使命后都是尽量早的被析构了,那么对于表达式呢? 如果在s+t计算后析构,那么print函数打印的就是一个非法内容了,因此C++给出的规则是: 临时变量应该在导致临时变量创建的"完整表达式"求值过程的最后一个步骤被析构 什么又是"完整表达式"?简单的说,就是不是表达式的子表达式 这条规则听起来很简单,但具体实现起来就非常的麻烦了,例如: X foo(int n) if (foo(1) || foo(2) || foo(3) ) 其中X中有operator int()转换,所以可以用在if语句中 这里的foo(1)将产生一个临时变量1,如果这部分为false,foo(2)将继续产生一个临时变量,如果这部分也为false,foo(3)... 一个临时变量的参数居然是和运行时相关的,更要命的是你要记住你到底产生了几个临时变量并在这个表达式结束的时候进行析构以小心的维护对象构造和析构的一致 我猜想,这里会展开成一段复杂的代码,并加入更多的if判断才能搞定,呵呵,好在我不是做编译器的

上面的规则其实还有两条例外: string s,t; string v = 1 ? s + t : s - t; 这里完整表达式是?语句,但是在完整表达式结束以后临时变量还不能立即销毁,而必须在变量v赋值完成后才能销毁,这就是例外规则1: 凡含有表达式执行结果的临时变量,应该存留到对象的初始化操作完成后销毁

string s,t; string& v = s + t; 这里s+t产生的临时变量即使在变量v的赋值完成后也不能销毁,否则这个引用就没用了,这就是例外规则2: 如果一个临时变量被绑定到一个引用,这个临时变量应该留到这个临时变量和这个引用那个先超出变量的作用域后才销毁 这篇文章可能有些深奥了,毕竟大多数内容来自于<> 那么就留下一条忠告: 在stl中,以下的代码是错误的 string getName(); char* pTemp = getName().c_str(); getName返回的就是一个临时变量,在把它内部的char指针赋值给pTemp后析构了,这时pTemp就是一个非法地址 确实如C++发明者Bjarne Stroustrup所说,这种情况一般发生在不同类型的相互转换上

在Qt中,类似的代码是这样的 QString getName(); char* pTemp = getName().toAscii().data(); 这时pTemp是非法地址

希望大家不要犯类似的错误

----------------------------------------------------------------------------------------------------------------如果函数返回值是一个对象,要考虑 return 语句的效率。例如

return String(s1 + s2);

这是临时对象的语法,表示“创建一个临时对象并返回它” 。不要以为它与“先创建一个局部对象 temp 并返回它的结果”是等价的,如

String temp(s1 + s2);

return temp;

实质不然,上述代码将发生三件事。首先,temp 对象被创建,同时完成初始化;然后拷贝构造函数把 temp 拷贝到保存返回值的外部存储单元中;最后,temp 在函数结束时被销毁(调用析构函数) 。然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。

类似地,我们不要将

return int(x + y); // 创建一个临时变量并返回它

写成

int temp = x + y;

return temp;

由于内部数据类型如 int,float,double 的变量不存在构造函数与析构函数, 虽然该

“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。-----------------------------------------------------------------------------------------------------------------c++陷阱之临时变量我们开始都会认为在调用Say()之后,对象d的m_b成员变量会被修改为7但是结果却输出“1”,原因如下:50,51行处出现了一个$T563,这其实是一个C++生成的临时对象汇编代码37,38行如下:37 _d$ = -8

38 $T563 = -12( sizeof(Derived)==8, sizeof(Base)==4 )上面mov eax,DWORD PTR _d$[ebp]mov DWORD PTR $T563[ebp],eax这段代码是将对象d的内容拷贝到临时变量中,并且只拷贝Base中有的部分,这样做就是所谓的“Slicing”。有些书中说这一步是由拷贝构造函数完成的。概念上是这样的,但是实际上,编译器并没有生成一个真正意义上的拷贝构造函数。这更进一步说明C++产生了一个临时对象作为强制转换的中间结果。然后以这个临时对象代替我们的对象d,来调用函数Say()。那么结果自然是,临时变量的m_b被改变,而我们的d.m_b没有发生变化这种强制类型转换就是所谓的"向上转型",upcasting。 也叫Object Slicing。这种操作应该小心使用,甚至避免-------------------------------------------指向临时变量的引用c++规定指向临时变量的引用只能为常数,具体看看代码了什么是临时变量又怎么产生的呢,可以看看一下代码void f(short const & b)...{}int _tmain(int argc, _TCHAR* argv[])...{ int i = 20; short const & b = i;//i is not a short, so generate a short variable as a temporary variable

during the convert i = 0; f(i);//i is not a short, so generate a short variable as a temporary variable during the

convert i = 0; cout << i << "," << b << endl; //output : 0, 20 because b is a reference to a temporary

variable return 0;} 对于临时变量,只能用常量的引用指向之。否则下面代码将不能编译通过void f(short & b)...{}int _tmain(int argc, _TCHAR* argv[])...{ int i = 20; short & b = i;// can not set a non-const referenct to temporary variable i = 0; f(i);// can not set a non-const referenct to temporary variable return 0;}有人会问指针呢指针无论是const或是non-const在这种情况下都会编译失败的--------------------------------------------------------------------c++中为什么临时变量不能作为非const的引用参数试看下面的代码:#include using namespace std;void f(int &a) { cout << "f(" << a << ") is being called" << endl;}void g(const int &a) { cout << "g(" << a << ") is being called" << endl;}int main() { int a = 3, b = 4; f(a + b); //编译错误,把临时变量作为非const的引用参数传递了 g(a + b); //OK,把临时变量作为const&传递是允许的}上面的两个调用之前,a+b的值会存在一个临时变量中,当把这个临时变量传给f时,由于f的声明中,参数是int&,不是常量引用,所以产生以下编译错误:const_: In function `int main()':const_:14: error: invalid initialization of non-const reference of type ' int&' from a temporary of type 'int'const_:4: error: in passing argument 1 of `void f(int&)'而在g(a+b)中,由于g定义的参数是const int&,编译通过。

问题是为什么临时变量作为引用参数传递时,必须是常量引用呢?很多人对此的解释是临时变量是常量,不允许赋值,改动,所以当作为非常量引用传递时,编译器就会报错。这个解释在关于理解临时变量不能作为非const引用参数这个问题上是可以的,但不够准确。事实上,临时变量是可以被作为左值(LValue)并被赋值的,请看下面的代码:#include

using namespace std;class CComplex {

friend CComplex operator+(const CComplex &cp1, const CComplex &cp2);friend ostream& operator<<(ostream &os, const CComplex &cp);private:

int x;

public:

CComplex(){}

CComplex(int x1) {

x = x1;

}};

CComplex operator+(const CComplex &cp1, const CComplex &cp2) {

CComplex cp3;

cp3.x = cp1.x + cp2.x;

return cp3;

}ostream& operator<<(ostream &os, const CComplex &cp) { os << cp.x; return os;}int main() {

CComplex a(2), b(3), c(4);

cout << (a + b) << endl; cout << ((a + b) = c) << endl; //临时对象作为左值 return 0;

}上面的程序编译通过,而且运行结果是:45临时变量确实被赋值,而且成功了。所以,临时变量不能作为非const引用参数,不是因为他是常量,而是因为c++编译器的一个关于语义的限制。如果一个参数是以非const引用传入,c++编译器就有理由认为程序员会在函数中修改这个值,并且这个被修改的引用在函数返回后要发挥作用。但如果你把一个临时变量当作非const引用参数传进来,由于临时变量的特殊性,程序员并不能操作临时变量,而且临时变量随时可能被释放掉,所以,一般说来,修改一个临时变量是毫无意义的,据此,c++编译器加入了临时变量不能作为非const引用的这个语义限制,意在限制这个非常规用法的潜在错误。还不明白?OK,我们说直白一点,如果你把临时变量作为非const引用参数传递,一方面,在函数申明中,使用非常量型的引用告诉编译器你需要得到函数对某个对象的修改结果,可是你自己又不给变量起名字,直接丢弃了函数的修改结果,编译器只能说:“大哥,你这是干啥呢,告诉我把结果给你,等我把结果给你了,你又直接给扔了,你这不是在玩我呢吗?”所以编译器一怒之下就不让过了。这下大家明白了吧?

那么,在函数中修改一个临时变量的引用是不是真的就毫无意义呢,其实也不尽然。在某些特定场合下,还是有意义的。比如我前几天在写一个高精度算法的类big_number,自然地,用了一个int数组nums[]存一个大数,而且为了提高效率,我还给了一个length指定位数,就是说这个大数就存在nums[0..length-1]里,其他的高位都是无效的,也省得我用big_number表示小数的时候一个个去填零。一开始,我定义加法如下:big_number operator+(const big_number &n1, const big_number &n2);但当我实现的时候,我想偷懒直接把n1和n2的低或(取决于哪个大)相加完事,这就得把短位数的大数补零,这样一来,就对n1或n2进行了修改,不能用const了,于是加法声明变成了:big_number operator+(big_number &n1, big_number &n2);虽然补了一些零,但由于nums[0..length-1]没便,他所表示的大数数值其实没变。这个声明和实现似乎也没什么问题。但当我用如下表达式时就出错了:big_number n1, n2, n3, n4;n4 = n1 + n2 + n3;为什么呢?就是因为在计算n1+n2+n3的时候,先要算n1+n2,把值存到一个临时变量里tmp里,再去算tmp+n3,这时,tmp是一个临时变量,而我们只定义了big_number operator+(big_number &n1, big_number &n2);编译器不能把一个临时变量代入到一个非const的引用参数,所以他就找不到与之匹配的函数声明原型和实现,也就出错了::6: error: no match for 'operator+' in 'operator+(big_number&, big_number&)((&n2))

+ n3'big_number.h:8: error: candidates are: big_number operator+(big_number&, big_number&)怎么办呢,只好重新实现加法重载,做到真正的const引用,也就是在不修改n1,n2的情况下,算出结果。没法偷懒了,:( 所以说啊,c++编译器的这个语义限制虽好,但有时也挺累人的。--------------------------------------------------CC++中的临时变量说到临时变量,我们大家也许都挺熟悉, 但是我自己对临时变量的理解却一直存在一个误区。通常情况下,我会把为了做某一件事情而临时创建的一个变量叫做临时变量。比如说在交换两个变量的值时,通常我们会创建第三个变量来达到我们最终的目的,而我们称之为“临时变量”。然而,大师Scott Meyers告诉我们,事实不是这么简单!在CC++中,真正意义上的临时变量是看不见的,就是说它们不会出现在你的代码中,参考一下这句话(来自《More Effective C++》):True temporary objects in C++ are invisible — they don't appear in your source code. They

arise whenever a non-heap object is created but not named.按照这种定义,我以前所理解的“临时变量”其实并不是真正意义上的临时变量,他只是我所定义的一个短命的局部变量而已。临时变量通常在函数参数传递发生类型转换以及函数返回值时被创建。比如下面这个例子:void uppercasify(const string& str){}int main(int argc, char* argv[]){ char subtleBookPlug[] = "Effective C++"; uppercasify(subtleBookPlug); // 此处有类型转换 return 1;}函数uppercasify需要const string&类型的参数,而实参类型为char *,故编译器会尝试着进行类型转换。此时一个string类型的临时变量将被创建,并用subtleBookPlug来初始化对象,最后将临时变量传给函数uppercasify。理解了这个例子,也就能较好的理解为什么CC++不允许为非const的引用类型创建临时变量了。比如下面这个例子:void uppercasify(string& str) // 参数类型改为string &{}int main(int argc, char* argv[]){ char subtleBookPlug[] = "Effective C++"; uppercasify(subtleBookPlug); return 1;}此时,如果创建了一个临时变量,那函数所修改的对象为临时变量,而不是用户所期待的subtleBookPlug了,从而容易引起误操作。