《C++中的临时变量》由会员分享,可在线阅读,更多相关《C++中的临时变量(10页珍藏版)》请在金锄头文库上搜索。
1、C+的临时变量的临时变量它们是被神所遗弃的孩子,没有人见过它们,更没有人知道它们的名字.它们命中注定徘徊 于命运边缘高耸的悬崖和幽深的深渊之间, 用自己短暂的生命抚平了生与死之间的缝隙.譬如朝露,却与阳光无缘.是该为它们立一座 丰碑的时候了,墓铭志上写着:我来了,我走了,我快乐过.许多人对临时变量的理解仅仅限于:string temp;其实,从 C+的观点来看,这根本就不是临时变量,而是局部变量.C+的临时变量是编译器在需要的时候自动生成的临时性变量,它们并不在代码中出现. 但是它们在编译器生成的二进制编码中是存在的,也创建和销毁.在 C+语言中,临时变量的问题格外的重要,因为每个用户自定义类
2、型的 临时变量都要出发用户自定义的构造函数和析构函数(如果用户提供了)又是该死的编译器!又该有人抱怨编译器总在自己背后干着偷偷摸摸的事情了.但是如 果离开了编译器的这些工作,我们可能寸步难行.如果 X 是一个用户自定义的类型,有默认构造函数,拷贝构造函数,赋值运算函数,析构函 数(这也是类的 4 个基本函数),那么请考虑以下代码:X get(X arg)return arg;X a;X b = get(a);即使是这么简单的代码也是很难实现的让我们分析一下代码执行过程中发生了什么?首先我要告诉你一个秘密:对于一个函数来说,无论是传入一个对象还是传出一个对象 其实都是不可能的.让一个函数传入或传
3、出一个内置的数据类型,例如 int,是很容易的,但是对于用户自定义 类型得对象却非常的困难,因为编译器总得找地方为这些对象写上构造函数和析构函数,不是在函数内,就是在函数外,除非你用指针或引用跳过这些 困难那么怎么办?在这里,编译器必须玩一些必要的小花招,嗯,其中的关键恰恰就是临时变量对于以对象为形参的函数:void foo(X x0)X xx;foo(xx);编译器一般按照以下两种转换方式中的一种进行转换1.在函数外提供临时变量void foo(X /声明 xxX:X(xx); /调用 xx 的默认构造函数X _temp0; /声明临时变量_temp0X:X(_temp0, xx); /调用
4、_temp0 的拷贝构造函数foo(_temp0); /调用 fooX:X(_temp0); /调用_temp0 的析构函数X:X(xx); /调用 xx 的析构函数2.在函数内提供临时变量void foo(X /声明临时变量_temp0X:X(_temp0, x0); /调用_temp0 的拷贝构造函数X:X(_temp0); /调用_temp0 的析构函数 X xx; /声明 xxX:X(xx); /调用 xx 的默认构造函数foo(xx); /调用 fooX:X(xx); /调用 xx 的析构函数无论是在函数的内部声明临时变量还是在函数的外部声明临时变量,其实都是差不多的,这 里的含义是
5、说既然参数要以传值的语意传入函数,也就是实参 xx 其实并不能修改,那么我们就用一个一摸一样临时变量来 移花接木,完成这个传值的语意但是这样做也不是没有代价,编译器要修改函数的声明,把对象改为对象的引用,同时修 改所有函数调用的地方,代价确实巨大啊,但是这只是编译器不高兴而已,程序员和程序执行效率却没有影响对于以对象为返回值的函数:X foo()X xx;return xx;X yy = foo();编译器一般按照以下方式进行转换void foo(X /声明 xxX:X(xx); /调用 xx 的默认构造函数_temp0:X:X(xx); /调用_temp0 的拷贝构造函数X:X(xx); /
6、调用 xx 的析构函数X yy; /声明 yyX _temp0; /声明临时变量_temp0foo(_temp0); /调用 fooX:X(yy, _temp0); /调用 yy 的拷贝构造函数X:X(_temp0); /调用_temp0 的析构函数X:X(yy); /调用 yy 的析构函数既然我们已经声明了 yy,为什么还要紧接着声明_temp0,其实这里完全可以把 yy 和临 时变量合一优化后,上面的代码看起来象这个样子:void foo(X /声明 xxX:X(xx); /调用 xx 的默认构造函数_temp0:X:X(xx); /调用_temp0 的拷贝构造函数X:X(xx); /调用
7、 xx 的析构函数X yy; /声明 yyfoo(yy); /调用 fooX:X(yy); /调用 yy 的析构函数嗯,怎么说呢,这算是一种优化算法吧,其实这各个技巧已经非常普遍了,并拥有一个专门 的名称 Named Return Value(NRV)优化NRV 优化如今被视为标准 C+编译器的一个义不容辞的优化操作(虽然其需求其实超 出了正式标准之外)除了以类为参数以外,如果参数的类型是 const Tfun(name);嗯,还记得在 const 文档中的论述吗?对于这种特殊的参数类型,编译器是很乐意为你做 自动转换的工作的,代价嘛,就是一个临时变量,不过如果是你自己去做,大概就只能声明一个
8、局部变量了为什么函数和临时变量这么有缘,其实根本的原因在于对象传值的语意,这一个也是为 什么 C+中鼓励传对象地址的原因和函数的情况类似的,还有一大类情况是临时变量的乐土,那就是表达式string s,t;printf(“%s“, s + t);这里 s+t 的结果该放在什么地方呢?只能是临时变量中.这个 printf 语句带来了新的问题,那就是“临时变量的生命期“是如何的?对于函数的情况,我们已经看到了,临时变量在完成交换内容的使命后都是尽量早的被 析构了,那么对于表达式呢?如果在 s+t 计算后析构,那么 print 函数打印的就是一个非法内容了,因此 C+给出的规 则是:临时变量应该在导
9、致临时变量创建的“完整表达式“求值过程的最后一个步骤被析构什么又是“完整表达式“?简单的说,就是不是表达式的子表达式这条规则听起来很简单,但具体实现起来就非常的麻烦了,例如:X foo(int n)if (foo(1) | foo(2) | foo(3) )其中 X 中有 operator int()转换,所以可以用在 if 语句中这里的 foo(1)将产生一个临时变量 1,如果这部分为 false,foo(2)将继续产生一个临时变 量,如果这部分也为 false,foo(3).一个临时变量的参数居然是和运行时相关的,更要命的是你要记住你到底产生了几个临 时变量并在这个表达式结束的时候进行析构
10、以小心的维护对象构造和析构的一致我猜想,这里会展开成一段复杂的代码,并加入更多的 if 判断才能搞定,呵呵,好在我不是 做编译器的上面的规则其实还有两条例外:string s,t;string v = 1 ? s + t : s - t;这里完整表达式是?语句,但是在完整表达式结束以后临时变量还不能立即销毁,而必须 在变量 v 赋值完成后才能销毁,这就是例外规则 1:凡含有表达式执行结果的临时变量,应该存留到对象的初始化操作完成后销毁string s,t;string这里 s+t 产生的临时变量即使在变量 v 的赋值完成后也不能销毁,否则这个引用就没用 了,这就是例外规则 2:如果一个临时变量
11、被绑定到一个引用,这个临时变量应该留到这个临时变量和这个引用 那个先超出变量的作用域后才销毁这篇文章可能有些深奥了,毕竟大多数内容来自于那么就留下一条忠告:在 stl 中,以下的代码是错误的string getName();char* pTemp = getName().c_str();getName 返回的就是一个临时变量,在把它内部的 char 指针赋值给 pTemp 后析构了,这 时 pTemp 就是一个非法地址确实如 C+发明者 Bjarne Stroustrup 所说,这种情况一般发生在不同类型的相互转换上在 Qt 中,类似的代码是这样的QString getName();char*
12、 pTemp = getName().toAscii().data();这时 pTemp 是非法地址希望大家不要犯类似的错误 -如果函数返回值是一个对象,要考虑 return 语句的效率。例如 return String(s1 + s2); 这是临时对象的语法,表示“创建一个临时对象并返回它” 。不要以为它与“先创建 一个局部对象 temp 并返回它的结果”是等价的,如 String temp(s1 + s2); return temp; 实质不然,上述代码将发生三件事。首先,temp 对象被创建,同时完成初始化;然后 拷贝构造函数把 temp 拷贝到保存返回值的外部存储单元中;最后,temp
13、 在函数结束时被 销毁(调用析构函数) 。然而“创建一个临时对象并返回它”的过程是不同的,编译器直 接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了 效率。 类似地,我们不要将 return int(x + y); / 创建一个临时变量并返回它 写成 int temp = x + y; return temp; 由于内部数据类型如 int,float,double 的变量不存在构造函数与析构函数, 虽然该 “临时变量的语法”不会提高多少效率,但是程序更加简洁易读。- c+陷阱之临时变量我们开始都会认为在调用 Say()之后,对象 d 的 m_b 成员变量会被修改为 7
14、但是结果却 输出“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 $T563ebp,eax 这段代码是将对象 d 的内容拷贝到临时变量中,并且只拷贝 Base 中有的部分,这样做 就是所谓的“Slicing” 。有些书中说这一步是由拷贝构造函数完成的。概念上是这样的,但是实际上,编译器并没有生成一个真正意义上的拷贝构造函数。这更进一步说明 C+产生了一个临时对象作为强制转换的中间结果。然后以这个临时 对象代替我们的对象 d,来调用函数 Say()。那么结果自然是,临时变量的 m_b 被改变,而我们的 d.m_b 没有发生变化 这种强制类型转换就是所谓的“向上转型“,upcasting。 也叫 Object Slicing。这种操作 应该小心使用,甚至避免- 指向临时变量的引用 c+规定指向临时变量的引用只能为常数,具体看看代码了什么是临时变量又怎么产生的呢,可以看看一下代码void f(short const short const /i