热门标签 | 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,所以当该引用出了定义的范围,该临时变量就析构了。也就是说,即使返回该引用,对于函数调用点,该引用仍然是相当于引用了临时变量,不可用。这样内存的一致性就不会被破坏了。
个人认为这个特性只是为了使用方便而已。

 

推荐阅读
  • 对象内存地址
    主  题 ... [详细]
  • 为了加速游戏,一提起汇编语言,大家也许会感到很神秘。其实如果你学起来就会发现,它并非想象中那样难。特别是内嵌汇编,由于它和C++紧密结合,使你不必考虑很多烦琐的细节(例如输入输出函数的写法),学习起来 ... [详细]
  • 第五周项目一——体验常成员函数(1)
    设计平面坐标点类,计算两点之间距离、到原点距离、关于坐标轴和原点的对称点等。在设计中,由于求距离、求对称点等操作对原对象不能造成任何改变,所以,将这些函数设计为常成员函数是合适的,能够避免数据成 ... [详细]
  • ProblemDescriptionAninchwormisatthebottomofawellninchesdeep.Ithasenoughene ... [详细]
  • CC++如何复制 ... [详细]
  • 深入探讨栈和队列的应用实例——铁轨问题(Rails, ACM/ICPC CERC 1997, UVa 514)。该问题设定在一个城市火车站,涉及n节车厢从A方向驶入车站,并需按照特定顺序驶出B方向的铁轨。本文将通过算法实现来验证特定顺序的可行性。 ... [详细]
  • 本文介绍了一个C++程序,该程序用于计算一个向量首尾索引的和。当向量长度为偶数时,程序会遇到对称对,如v1[0] + v1[last]与v1[last] + v1[0],这些实际上是相同的计算结果,因此需要排除重复项以提高效率。 ... [详细]
  • 首先说一下,这是我在CSDN上的第一个文章,其实这个账号早在几年前就申请了,不过当时只是为了下载一个资源,而且也不怎么懂信息技术相关的领域,后来就再也没怎么动过,直到今天我才开始使用这个账号 ... [详细]
  • 本文探讨了一个在Mac Mavericks系统上使用Clang++成功编译但通过R CMD SHLIB构建并在R中加载时遇到‘符号未找到’错误的C++程序问题。文章详细分析了错误原因,并提供了有效的解决方案。 ... [详细]
  • 深入解析C++ Atomic编程中的内存顺序
    在多线程环境中,为了防止多个线程同时修改同一数据导致的竞争条件,通常会使用内核级同步对象,如事件、互斥锁和信号量等。然而,这些方法往往伴随着高昂的上下文切换成本。本文将探讨如何利用C++11中的原子操作和内存顺序来优化多线程编程,减少不必要的开销。 ... [详细]
  • 本文探讨了在形状类族中应用纯虚函数的设计模式及其解析方法。通过定义一个基类 `Shape`,其中包含一个纯虚函数 `area()`,实现了多态性和代码的灵活性。该设计使得派生类能够根据具体的形状计算面积,从而提高了代码的可扩展性和复用性。示例代码展示了如何利用纯虚函数实现这一机制。 ... [详细]
  • 要求:海伦公式:ssqrt(p*(p-a)*(p-b*)(p-c)),其中p(a+b+c)2,a,b,c为三角形的三个边。定义两个带参数的宏,一个用来求p,另一个用来求s题目分 ... [详细]
  • Here是指向最小代码的链接,如果消失了, ... [详细]
  • 第3章 感受(一)——3.1. Hello world 经典版
    [回到目录]白话C++第3章.感受Helloworld!,HelloC++,我们来了!3.1.Helloworld经典版毫无疑义,一 ... [详细]
  • 下面想跟大家分享一下,请大家看下面一个例子,看看结果是什么?#include<iostream>usingnamespacestd;intmain() ... [详细]
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社区 版权所有