昨天在看某位网友回复时贴出来的文章时,发现了C++一个闻所未闻的特性“根据C++标准,如果const的引用被初始化为对一个临时变量的引用, 那么它会使这个临时变量的生命期变得和它自己一样。”看来我真的是我仔细的思考这个问题许久,也没有搞清楚具体是怎么回事,后来写了一个例程,如下:
#include
using namespace std;
class A
{
public:
A( void ){}
~A( void ){cout << "对象析构/n";}
};
typedef const A& myclass;
A GenA( void )
{
A a;
return a;
}
int main( void )
{
myclass test(GenA());
cout << "对象生成/n";
system( "pause" );
return 0;
}
请注意这个函数
A GenA( void )
{
A a;
return a;
}
接 常理说,这个函数返回了一个临时变量——“A a;”而在return a;的时候,准确的说是在函数弹栈的时候会把这个临时变量销毁,调用这个对象的析构函数。但是,在Dev-C++的环境下编译、运行时,却没有输出预期的 结构“对象析构”。而当我试着把typedef const A& myclass;中的const去掉时却发现出现了一个警告,跟本不能链接。看来这足以验证上面说的那个奇妙的特性。可是这又是为什么呢?这让我想起了以 前在看C++Primer发现的一点:
const int &a = 111; //这样是允许的
int &a = 111; // 这样是非法的
当 时看就当作法则记了下来,没有在意太多原理性的东西,而现在我却注意到这些规定和上面那个例程内在的联系。在翻阅了相关资料后得知,这两条语句看似只差一 个const,但是其内部实现是不一样的。const int &a = 111; 实际上完成了两步,第一步生成了一个和这个a的生存期一样长的int变量,第二步让a等于这个int型临时变量的地址。而非const引用是不可以生成一 个临时变量的。但是当下面的性况又会发生什么呢?
int n = 3;
const int &a = n;
实 际情况是,在这个const语句的执行中什么临时变量也不会生成,只进行了地址的赋值操作。那么倒底什么样的情况才会为const的引用生成临时变量呢? 关于这个规定我还没有能够找到相关的资料,希望热心的朋友能助我一臂之力。但具我多次试验发现有以下两种情况会为const引用生成临时变量:
1. 生存期在地址赋值后就结束的变量。
2. 数值常量。
在一开始讲的那个例子当然就是情况1了,为了验证这种情况,我又在Dev-C++作了下面的这个试验:
class A
{
};
void fun( const A &a )
{
}
int main( void )
{
fun( A() );
return 1;
}
上面这个例程是完全可以编译运行的,但是如果把fun函数的参数改为:
void fun( A &a );
在编译时,fun( A() );这一句就会出错。原因是非const的引用不会生成临时变量,而A()实际上是生成了一个临时变量,生存期就在fun函数的参数括号内,所以如果这一句如果可以编译通过的话,A &a这个引用将是无效的。
现 在因该对这些规则比较了解了,但我仅仅到了知其然的地步,然而为什么要有这样的规定呢?当然,你也看到了,这篇文章的标题并不是“您知道吗?”,我并不是 在这里向大家讲述这个特性,因为真正使我迷惑不解的是编译器之间的离奇差异!最上面那个例程在VC7.1下编译运行的结果是这样的:
对象析构
对象生成
……
对象析构
天哪,整个程序我只生成了一个对象,却有两次析构!为了一探究竟,我打开了反汇编查看源代码,下面的代码是GenA函数中的:
A a;
0041823D lea ecx,[a]
00418240 call A::A (4157B2h) ; 只有这里调用了唯一的一次A的构造函数。
return a;
00418245 mov eax,dword ptr [ebp-0E0h]
0041824B or eax,1
0041824E mov dword ptr [ebp-0E0h],eax
00418254 lea ecx,[a]
00418257 call A::~A (4158F2h) ; 请注意这里,调用了一次A的析构
0041825C mov eax,dword ptr [ebp+8]
下面的代码是在main函数的最后:
return 0;
00422C27 mov dword ptr [ebp-0ECh],0
00422C31 mov dword ptr [ebp-4],0FFFFFFFFh
00422C38 lea ecx,[ebp-1Dh]
00422C3B call A::~A (4158F2h) ; 请意这里,又调用一次A的析构
00422C40 mov eax,dword ptr [ebp-0ECh]
怪 不得会出现上面的运行结果,这显然是编译器问题。但当我把typedef const A& myclass;中的const去掉,发现整个程序的编译运行结果没有一丁点的变化……这真让我倒吸一口凉气。之后我又作一个小试验,却得到了一个惊人的 发现!我把GenA函数改成了下面这样:
A GenA( void )
{
return A();
}
之后的编译和运行结果和Dev C++也就是C++标准结果是一样的了。难道
A a;
return a;
和
return A();
有什么区别吗??难道这足以导致编译器在处理const引用时采取不同的方法吗?
后面我又在VC7.1下按照C++Primer上的说法做了另一个试验,结果……吐血中……
class A
{
};
void fun( A &a )
{
}
int main( void )
{
fun( A() );
return 1;
}
居 然编译通过了!!那么A()生成的临时变量的生存期到底是什么?后面的试验我真不敢再继续下去了,再作恐怕就真的要西去了。后面我又在BCB6.0下做了 上面几个试验,发现和VC7.1的结果大同小异,现在我真的不知道该信谁,只有Dev-C++才是真正标准的C++啊!
我现在的问题有这几个:
构造const 引用真正的生成过程是什么?
为const引用生成临时变量有何目的?
生成临时变量是否破坏了变量生存期的一致性,让系统变的混乱?
VC和BCB的编译器为何与标准相去甚远?这样做有何意义?
VC和BCB的编译器为这些情况倒底是如何处理的?
fun( A() )里构造的A 的临时对象的生存期到底是什么?
//下面是一段例程及其在VC.net下的运行结果
//这段例程可以比较全面的表达我的意图
#include
using namespace std;
class A
{
public:
A( void ){ cout << "对象构造/t" << this << endl; }
~A( void ){ cout << "对象析构/t" << this << endl; }
A( const A &t ){ cout << "拷贝构造/t" << this << endl; }
};
typedef const A& cmytype;
typedef A& mytype;
A GenA1( void )
{
A a;
return a;
}
A GenA2( void )
{
return A();
}
int main( void )
{
cmytype test1( GenA1() );
cout << "test1生成/t" << &test1 << endl;
cmytype test2( GenA2() );
cout << "test2生成/t" << &test1 << endl;
mytype test3( GenA1() );
cout << "test3生成/t" << &test1 << endl;
mytype test4( GenA2() );
cout << "test4生成/t" << &test1 << endl;
// system( "pause" );
return 0;
}
//================================================================
运行结果:
对象构造 0012FD7B
拷贝构造 0012FEBF
对象析构 0012FD7B
test1生成 0012FEBF
对象构造 0012FEA7
test2生成 0012FEBF
对象构造 0012FD7B
拷贝构造 0012FE8F
对象析构 0012FD7B
test3生成 0012FEBF
对象构造 0012FE77
test4生成 0012FEBF
对象析构 0012FE77
对象析构 0012FE8F
对象析构 0012FEA7
对象析构 0012FEBF
===========================================================================
按标准规定,临时对象可以被const reference,这里临时对象的生命期将延长。而延长对象生命期的方法没有作规定,由编译器决定。
你的试验中看到只调用了一次ctor而调用了两次dtor,事实上是只调用了一次default ctor,此外还调用了一次copy ctor,而由于你的copy ctor是implicitly-declared,所以
在结果中看不出来。也就是说,它事实上是调用了两次ctor和两次dtor,没有任何问题。
而传递参数时,按标准是不允许把临时对象作为要求non-const reference的参数的。VC和BCC在这一点上不符合标准。这样规定主要是因为,如果一个函数要求一个non-const reference参数,说明它要修改这个参数的状态(值),而如果你传递了一个临时对象进去,则这一修改将是无效的。如果你的函数并不修改参数的状态, 应该声明为const reference类型的参数。
===========================================================================
从标准语义上讲二者没区别,但因为标准允许NRV,所以
A GenA( void )
{
A a;
return a;
}
可以被编译器作为:
void GenA( A& a )
{
return;
}
处理。
===========================================================================
const问题:这是标准规定的。因为const对象不能做修改,所以生成一个临时的对象的const引用是可以的,不违背原来的C的临时变量生存期的定义,因为当作常量处理了。而非const就有问题了。
TC++PL里是这样描述的:
A temporary created to hold a reference initializer persists until the end of its reference’s scope.
注意只是references scope,所以当该引用出了定义的范围,该临时变量就析构了。也就是说,即使返回该引用,对于函数调用点,该引用仍然是相当于引用了临时变量,不可用。这样内存的一致性就不会被破坏了。
个人认为这个特性只是为了使用方便而已。