热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

根据C++标准,如果const的引用被初始化为对一个临时变量的引用,那么它会使这个临时变量的生命期变得和它自己一样

昨天在看某位网友回复时贴出来的文章时,发现了C++一个闻所未闻的特性“根据C++标准,如果const的引用被初始化为对一个临时变量的引用,那么它会使这个临时变量的生命期变得和它自己一样。”看来

昨天在看某位网友回复时贴出来的文章时,发现了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,所以当该引用出了定义的范围,该临时变量就析构了。也就是说,即使返回该引用,对于函数调用点,该引用仍然是相当于引用了临时变量,不可用。这样内存的一致性就不会被破坏了。
个人认为这个特性只是为了使用方便而已。

 

推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 为了加速游戏,一提起汇编语言,大家也许会感到很神秘。其实如果你学起来就会发现,它并非想象中那样难。特别是内嵌汇编,由于它和C++紧密结合,使你不必考虑很多烦琐的细节(例如输入输出函数的写法),学习起来 ... [详细]
  • 第五周项目一——体验常成员函数(1)
    设计平面坐标点类,计算两点之间距离、到原点距离、关于坐标轴和原点的对称点等。在设计中,由于求距离、求对称点等操作对原对象不能造成任何改变,所以,将这些函数设计为常成员函数是合适的,能够避免数据成 ... [详细]
  • 对象内存地址
    主  题 ... [详细]
  • #include<iostream>#include<string>usingnamespacestd;classA{private:stringr; ... [详细]
  • HDU1787欧拉函数之在线算法欧拉函数的介绍:φ函数的值通式:φ(x)x(1-1p1)(1-1p2)(1-1p3)(1-1p4)…..(1-1pn),其中p1,p2…… ... [详细]
  • 从vc6.0转到vs20052008等出现的错误详解(HYD整理)最近开发平台由VC6.0升级至VS2005,需要将原有的项目迁移,特将碰到的问题归纳如下:1消 ... [详细]
  • ProblemDescriptionAninchwormisatthebottomofawellninchesdeep.Ithasenoughene ... [详细]
  • 要求:海伦公式:ssqrt(p*(p-a)*(p-b*)(p-c)),其中p(a+b+c)2,a,b,c为三角形的三个边。定义两个带参数的宏,一个用来求p,另一个用来求s题目分 ... [详细]
  • 题目:poj2342Anniversaryparty题意:话说一个公司的一些然要去参加一个party,每个人有一个愉悦值,而如果某个人的直接上司在场的话会非常扫兴,所以避免这样 ... [详细]
  • Here是指向最小代码的链接,如果消失了, ... [详细]
  • 第3章 感受(一)——3.1. Hello world 经典版
    [回到目录]白话C++第3章.感受Helloworld!,HelloC++,我们来了!3.1.Helloworld经典版毫无疑义,一 ... [详细]
  • CC++如何复制 ... [详细]
author-avatar
街对面的怪蜀黍
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有