2011-08-27 102 views
1

这里是一个混淆了我的代码:在类的初始化过程中会发生什么?

#include <iostream> 
using namespace std; 

class B { 
public: 
    B() { 
     cout << "constructor\n"; 
    } 
    B(const B& rhs) { 
     cout << "copy ctor\n"; 
    } 
    B & operator=(const B & rhs) { 
     cout << "assignment\n"; 
    } 
    ~B() { 
     cout << "destructed\n"; 
    } 
    B(int i) : data(i) { 
     cout << "constructed by parameter " << data << endl; 
    } 

private: 
    int data; 
}; 

B play(B b) 
{ 
    return b; 
} 

int main(int argc, char *argv[]) 
{ 
#if 1 
    B t1; 
    t1 = play(5); 
#endif 

#if 0 
    B t1 = play(5); 
#endif 

    return 0; 
} 

环境为g ++ 4.6.0在Fedora 15. 第一个代码片段输出如下:

constructor 
constructed by parameter 5 
copy ctor 
assignment 
destructed 
destructed 
destructed 

和第2片段代码输出是:

constructed by parameter 5 
copy ctor 
destructed 
destructed 

为什么有三种析构函数被调用的第一个例子,而在第二个它是只有两个?

+0

您期望看到多少构造函数?如果没有足够的尝试使用'-fno-elide-constructors'进行编译,并且gcc不会消除默认情况下执行的一些构造函数调用。 –

回答

2

第一种情况:

B t1; 
t1 = play(5); 
  1. 创建通过调用B默认构造对象t1
  2. 为了调用play(),使用B(int i)创建了B的临时对象。 5作为和B的对象被创建,play()被调用。
  3. return b;内部play()导致调用copy constructor返回对象的副本。
  4. t1 =调用Assignemnt运算符将返回的对象副本分配给t1
  5. 第一个析构函数,破坏在#3中创建的临时对象。
  6. 第二个析构函数破坏#2中返回的临时对象。
  7. 第三个析构函数破坏对象t1

第二种情况:

B t1 = play(5); 
  1. B类的临时对象通过调用的B参数的构造这需要int作为paraemter创建。
  2. 此临时对象用于调用B类的复制构造函数
  3. 第一个析构函数破坏#1中创建的临时文件。
  4. 第二个析构函数破坏对象t1

一个析构函数调用是在第二种情况较少,因为在第二种情况下,编译器使用Return value Optimization和elides通话,而从play()回国创建一个额外的临时对象。而是在临时分配的位置创建Base对象。

+0

#5和#6是错误的方式。 –

+0

@Charles Bailey:grr ..混乱的困惑,我希望我的编辑能够做到。 –

+0

我不同意你最后一段的推理。我没有看到任何理由为什么函数调用在两种情况下都会得到不同的优化。从副本的数量来看,这两种情况下都使用NRVO。 –

0

第一个片段构造三个对象:

  • 乙T1
  • B(5)< - 从(int)构造;这是播放功能的临时对象
  • return b;或B(b)< - 复制ctor

这是我的猜测,虽然它看起来效率低下。

+0

但是第二个解释是什么? – zhanglistar

0

请参阅Als发布的关于第一个场景的详情。我认为(编辑:错误;见下文)与第二种情况的区别在于,编译器足够聪明,可以使用NRVO(命名为返回值优化)并取消中间副本:而不是创建临时副本返回(来自播放),编译器使用播放函数内的实际“b”作为t1的拷贝构造函数的右值。

戴夫亚伯拉罕有一个article复制elision,这里的维基百科return value optimization

编辑:其实,ALS增加了播放的播放第二个方案中,太多。 :)

进一步编辑:其实,我是不正确的上面。 NRVO在这两种情况下都没有被使用,因为标准根据接受的答案for this question直接从函数参数(b在播放)到函数的返回值位置(至少没有内联)中直接删除副本。

即使允许NRVO,我们也可以知道它至少在第一种情况下没有被使用:如果是,则第一种情况不会涉及任何拷贝构造函数。第一种情况下的复制构造函数来自隐藏的副本,从命名值b(在播放函数中)到用于播放的隐藏返回值位置。第一种情况不涉及明确的复制结构,所以这是唯一可以出现的地方。

实际发生的情况是:无论是哪种情况都没有发生NRVO,并且在返回时创建了一个隐藏副本......但在第二种情况下,编译器能够直接在t1处构造隐藏返回副本位置。所以,从b到返回值的拷贝没有被消除,但是从t1返回值的副本是。但是,编译器在t1已经构建的第一种情况下做了这种优化的时间更加困难(阅读:它没有这样做))。如果t1已经在与返回值的位置不兼容的地址构造,那么编译器不能直接将t1的地址用于隐藏的返回值副本。

1

首先,检查子表达式play(5)。这两种情况下的表达方式都是相同的。

在一个函数调用表达式中的每个参数是从它的参数(ISO/IEC 14882:2003 5.2.2/4)复制初始化。在这种情况下,这涉及将5转换为B,方法是使用非显式构造函数,使用int创建临时B,然后使用复制构造函数初始化参数b。但是,通过使用12.8中规定的规则下的转换构造函数int直接初始化b,可以实现删除临时实现。

类型的play(5)B以及 - 作为函数返回一个非参考 - 它是一个右值

return语句隐式地将返回表达式转换为返回值类型(6.6.3),然后使用转换后的表达式对返回对象进行复制初始化(8.5/12)。

在这种情况下,返回表达式已经是正确的类型,所以不需要转换,但仍需要复制初始化。


除了在返回值优化

命名返回值优化(NRVO)是指情况return语句是,如果形式return x;其中x是一个自动的对象本地的功能。发生时,允许实现在返回值的位置构造x,并消除return点处的复制初始化。

尽管标准中没有这样命名,但NRVO通常是指12.8/15中描述的第一种情况。

play这个特殊的优化是不可能的,因为b不是函数体的本地对象,它是在函数输入时已经构造的参数的名称。

(未命名的)返回值优化(RVO)在引用的内容上甚至不太一致,但通常用于引用返回表达式不是命名对象而是转换为返回值的表达式的情况可以合并返回对象的复制初始化,以便从转换结果中直接初始化返回对象,从而消除一个临时对象。

的RVO不play适用,因为b已经B型的,因此副本初始化相当于直接初始化并没有临时对象是必要的。


在这两种情况下play(5)需要B(int)一个B使用参数和B副本初始化到返回对象的构造。它也可能在参数初始化时使用第二个副本,但即使未明确请求优化,许多编译器也会消除此副本。这两个(或全部)对象都是临时对象。

在表达式语句t1 = play(5);拷贝赋值运算符将被称为复制的play返回值t1和两个临时变量的值(参数和play返回值)将被销毁。自然t1必须在此声明之前构建,其析构函数将在其生命周期结束时调用。

在声明语句B t1 = play(5);,逻辑t1被初始化的发挥返回值和相同数量的临时将被作为表达式语句t1 = play(5);。但是,这是12.8/15中所涉及的第二种情况,允许实现消除用于返回值play的临时值,并允许返回对象的别名为t1play函数的运行方式完全相同,但因为返回对象只是t1的别名,所以它的return语句有效地直接初始化t1,并且没有单独的临时对象用于需要销毁的返回值。

0

在你的第一个例子,你打电话三个构造函数:

  • 在声明B t1;B()构造,这也是一个定义,如果B()是公开的。换句话说,编译器会尝试将任何已声明的对象初始化为一些基本的有效状态,并将B()作为将B大小的内存块转换为所述基本有效状态的方法,以便在t1上调用的方法不会中断该程序。

  • B(int)构造函数,用作隐式转换; play()需要一个B,但给了一个int,但B(int)被认为是一种将int转换为B的方法。

  • B(const B& rhs)拷贝构造函数,这将复制由play()返回到临时值,使其有足够的范围长的生存B的值赋值运算符使用。

当范围退出时,上述每个构造函数都必须与析构函数匹配。

在你的第二个例子,但是,你明确初始化的t1play()结果值的,所以编译器不需要浪费周期提供了基本的状态t1其指定的play()的一个副本之前导致新变量。所以,你只能叫

  • B(int)得到一个有用的论据play(B)

  • B(const B& rhs),使t1将被初始化(无论你的拷贝构造决定的)的play()的结果正确的副本。

你看不到三分之一的构造在这种情况下,因为编译器“eliding”的play()返回值到t1;也就是说,它知道t1play()返回之前不存在有效状态,所以它只是将返回值直接写入为t1预留的内存中。

相关问题