C++构造函数行为浅析

深度探索C++对象模型——第二章 构造函数语义学

写在最前面:本文的内容主要来自《深度探索C++对象模型》这本书,里面还写了一些在看的过程中不明白的东西,还有一些自己的理解(希望那算是理解233),很多书上的代码没码上,因为我估计这篇也没多少人能看到(再次233)。


本章主要介绍编译器对“对象构造过程”的干涉,和对于“程序形式”和“程序效率”上的影响。

需要注意

  • 单一参数的constructor能被当做是一个conversion运算符。
  • 使用explicit可以制止“单一参数的constructor"被当做一个conversion运算符。
  • global objects的内存保证会在程序启动的时候被清为0;local objects配置于程序的堆栈中,heap objects配置于栈中,都不一定被清为0,它们的内容是内存上次被使用后的痕迹。
  • 被合成的default constructor 只是为了满足编译器的需要(如构造),而非程序的需要(如不会为了初始化data member 而自动生成default constructor)。
  • 如果default constructor已经被显式定义,那么编译器无法合成第二个。但是如果class内含有其他member class objects,而且constructor没有对其进行初始化,那么编译器会扩张已经存在的constructor,安插一些代码,使得user code在执行之前,先调用必要的default constructors。

2.1 Default Constructor 的构造操作

default constructor 在需要的时候被编译器产生出来。被谁需要?做什么事?

  • answer:被编译器需要,而非被程序需要。如果是程序需要,那么这是程序员的责任。

只有编译器需要它的时候,才会合成一个default constructor,此外,被合成出来的constructor只执行编译器所需的行动

  • 对于Class X,如果没有任何user-declared constructor,那么会有一个default constructor 被隐式(implicitly)声明出来,一个被隐式声明出来的default constructor 将是一个trivial(没啥用的)constructor。

一个nontrivial default constructor就是编译器所需要的那种,必要的话会由编译器合成出来。下面是nontrivial default constructor 的四种情况:

①"带有Default Constructor"的Member Class Object

如果一个class 没有任何constructor,但它内含一个member object,而且这个member object有default constructor,那么这个class的implicit default constructor就是“nontrivial”。编译器需要为这个class合成一个default constructor,但这个合成的操作只有在constructor真正需要的时候才会发生。

出现了一个问题:如果在C++各不同的编译模块(compilation module,也就是不同的文件)中,编译器如何避免合成出多个default constructor(比如文件A和B都合成出了default constructor,分别是A.C,B.C)呢?

  • 解决方案是把合成的default constructor、copy constructor、destructor和assignment copy operator都以inline方式完成。因为inline函数都有static linkage,不会被文件以外的文件看到。如果函数复杂,不适合做成inline,那么就会合成出explicit non-inline static实例

如果default constructor已经被显式定义,那么编译器无法合成第二个。但是如果class内含有其他member class objects,而且constructor没有对其进行初始化,那么编译器会扩张已经存在的constructor,安插一些代码,使得user code在执行之前,先调用必要的default constructors。

如果有多个class member objects要求constructor初始化操作,行为如何?

  • C++语言要求以member objects 在class中的声明顺序来调用各constructors。这一点由编译器完成,它为每个constructor安插程序代码,以"member声明顺序"调用每一个member所关联的default constructors。并且这些由编译器扩张的代码会安插在explicit user code之前,也就是,如果我们没有为这个包含多个class member objects的class创建default constructor,那么编译器会为我们生成一个nontrivial default constructor,这个constructor的作用是调用这个class object中的member class object的constructor以实例化member object,而如果我们自己写了default constructor,那么编译器就没有能力为我们再生成一个default constructor,因为default constructor只能有一个,但这个时候如果我们自己写的default constructor中没有显式初始化这些member class objects,那么编译器会在我们写的constructor代码(也就是explicit user code)之前,安插编译器生成的这些调用member class objects 的constructor的代码,并且安插的顺序是我们class中声明的顺序。

②“带有Default Constructor”的Base Class

在继承体系中,如果么有任何constructors的class派生自一个**"带有default constructor"的base class**,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。这个合成的default constructor将调用上一层base classes的default constructor(根据它们声明的顺序),并且对于该class的后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”没有什么差异。也就是说,如果后续的派生的class没有提供constructor,那么会根据这个合成的constructor来合成自己的constructor。

如果写了多个constructor ,但是其中都没有default constructor,编译器会扩张现有的每个constructor,将用来调用所有必要的default constructors的代码加进去。不会合成新的default constructor,因为已经提供了其他的constructor。同时,如果也存在”带有default constructor"的member class objects,那些default constructor也会被调用,但是是在所有base class constructor都被调用之后

还有两种情况也会合成出default constructor:

  1. class 声明(或继承)一个virtual function。
  2. class派生自一个继承串链,其中有一个或多个virtual base classes。

③“带有一个Virtual Function”的Class

以下面程序为例:

class Widget{
public:
	virtual void flip() = 0;
    // ...
};

void flip(const Widget& widget)	{widget.flip();}

// 假设Bell和Whistle 都派生自Widget
void foo()
{
    Bell b;
    Wistle w;
    
    flip(b);
    flip(w);
}

由于缺乏user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。

编译期间会发生下面两个扩张行为:

  1. 一个virtual function table(vtbl)会被编译器产生出来,里面存放class的virtual function address
  2. 每个class object内,编译器会合成一个pointer member(也就是vptr),内含相关的class vtbl的地址。

此外,widget.flip()的虚拟调用操作(virtual invocation)会被重新改写,以使用widget的vptr和vtbl中的flip()条目:

// widget.flip()的虚拟调用操作的转变
(*widget.vptr[1])(&widget)

其中:

  • 1 表示flip()在virtual table中的固定索引
  • &widget表示要交给**“被调用的某个flip()函数实例”this**指针

为使这个机制有用,编译器必须为每个Widget(或其派生类的)object的vptr设定初值(也就是让这个vptr指向vtbl)。对于class所定义的每个constructor编译器都会插入一些代码来完成这样的事情。对于那些未声明任何constructors的classes,编译器会给它们合成一个default constructor,以正确地初始化每一个class object 的vptr。

④ “带有一个Virtual Base Class”的Class

Virtual base class的实现在不同编译器间有极大差异。Virtual base class和多重继承导致的菱形继承使vtbl有一些特殊的问题,具体在后面的第四章function 语义学里介绍(如果没鸽的话 😃

小结:

有四种情况,编译器必须为未声明constructor的classes合成一个default constructor。这些合成物被称为implicit nontrivial default constructor。这些被合成出来的constructor只能满足编译器的需要,而非程序的需要

它们之所以能够完成任务,是依靠**“调用member object或base class的default constructor”,或是“为每个object初始化其virtual function机制或virtual base class机制**而完成。

对于除这四种情况外,并且没声明任何constructor 的classes,它们持有的是implicit trivial default constructor,它们实际上并不会被合成出来

在编译器合成的default constructor 中,只有base class sub-objects和member class objects会被初始化。其他的non-static data member(如整数、整数指针、整数数组等)都不会被初始化。因为,这些初始化对编译器非必要(理解这一点很重要),它们或许只是对程序比较重要。如果程序需要一个将指针初始化为nullptr的default constructor,那么完成这件事的应该是写程序的人,而非编译器。

两个常见的误解

  1. 任何class如果没有定义default constructor ,就会被合成出一个(当然不是这样,首先如果定义了其他的constructor,那么编译器不会再生成default constructor,另外,如前面所介绍的那样,只有在四种情况下,编译器才会为了满足自己的需求产生出default constructor)
  2. 编译器合成出来的default constructor 会显式设定“class 内每个data member的默认值”(如前面所描述的,编译器合成的或扩展的constructor只会做对编译器来讲是必要的事,而非对于程序的)

2.2 Copy Constructor 的构造操作

copy constructor是一个constructor,其有一个参数,类型是其class type。

有三种情况,会以一个object的内容作为另一个class object的初值。

第一种,也是最明显的,就是对一个object做显式初始化操作:

class X {...};
X x;

// 显式地以一个object的内容作为另一个class object的初值
X xx = x;

另外两种情况是,当object被当做参数交给某个函数时,如:

extern void foo(X x);

void bar()
{
    X xx;
    
    // 以 xx 作为foo() 的一个参数的初值(隐式的初始化操作)
    foo(xx);
    // ...
}

以及当函数传回一个class object时,如:

X foo_bar()
{
	X xx;
    // ...
    return xx;
}

Default Member-wise Initialization

考虑class没有提供一个explicit copy constructor的情况。当class object以相同class 的另一个object作为初值时,内部会发生default member-wise initialization,也就是把每一个内建的或派生的data member的值(注意是值),从某个object拷贝一份,到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行member-wise initialization。(那member class object咋整,这个递归的方式施行member-wise initialization是啥意思>???

举个栗子:

class String{
public:
    // ... 没有explicit copy constructor
private:
    char* str;
    int len;
};

一个String object的default member-wise initialization发生在下面情况:

String noun("book");
String verb = noun;

它的完成方式就好像对每个member进行设定:

//相当于下面:
verb.str = noun.str;
verb.len = noun.len;

如果一个String object被声明为另一个class的member,比如:

class Word
{
public:
    // ... 没有explicit copy ctor
private:
    int _occurs;
    String _word;			// 此时String object 成为 class Word的一个member
}

此时,一个Word object的default member-wise initialization会拷贝其内建的member _occurs,然后对于String member object _word递归的实施member-wise initialization。

“如果一个class未定义copy constructor,编译器就自动为它产生出一个”这句话不对

应该是:“default constructors和copy constructors只有在必要的时候才由编译器产生”

这里的必要,是指class不展现bitwise copy semantics 时。(注意,是不展现bitwise copy semantics)

一个class object可用两种方式复制得到,一种是被初始化(也就是这里所关心的在初始化时对于class object的拷贝行为),另一种是被指定(也就是对一个已有的对象进行拷贝行为),也就是assignment。概念上讲,这两个操作分别是以copy constructor和copy assignment constructor完成的

如default constructor 一样,C++ Standard说,如果class没有声明一个copy constructor,就会有隐式的声明(implicitly declared)或隐式的定义(implicitly defined)出现。

也同前面的default constructor一样,C++ Standard将copy constructor分为trivial 和non-trivial两种。只有non-trivial的实例才会被合成到程序中

决定一个copy constructor是否为trivial的标准在于,class是否展现出所谓的"bitwise copy semantics"

Bitwise Copy Semantics(位拷贝)

考虑下面的栗子:

Word noun("book");

void foo()
{
    Word verb = noun;
    // ...
}

verb是由noun来初始化的。但是在没有得知class Word 的声明前,不可能预测这个初始化操作的程序行为。

如果class Word 定义了一个copy constructor,那么verb会调用这个cp ctor。

但如果没有定义explicit cp ctor,那么是否会有编译器合成的实例被调用,就得看这个class是否展现bitwise copy semantics了。

展现bitwise copy semantics的情况:

class Word{
public:
    Word(const char*);
    ~Word() {delete [] str;}
    // ...
private:
    int cnt;
    char* str;
}

未展现bitwise copy semantics的情况:

class Word{
public:
    Word(const char*);
    ~Word() {delete [] str;}
    // ...
private:
    int cnt;
    String str;				// 注意,这里不一样
}

其中String声明了一个explicit cp ctor。

在这种情况下,编译器必须合成一个cp ctor,以便调用member class String object的cp ctor

生成的pseudocode如下:

inline Word::Word(const Word& wd)
{
	str.String::String(wd.str);
	cnt = wd.cnt;
}

在被合成出来的copy constructor中,如整数、指针等non-class member也都会被复制。

总结一下,就是如果class的member的复制过程不需要调用copy constructor,那么这个就展现了bitwise copy semantics。那么如果我们没有定义copy constructor,编译器也不会帮我们生成copy constructor。

不要Bitwise Copy Semantics

一个class 不展现“bitwise copy semantics”的四种情况(也就是编译器会帮我们生成copy constructor的四种情况):

  1. 当class内含一个member object,而后者的class声明中有一个copy constructor(不论是class的设计者显式声明的,还是编译器合成的)。
  2. 当class继承自一个base class而后者存在一个copy constructor时(不论是被显式声明还是合成得到的)。
  3. 当class声明一个或多个virtual functions时。
  4. 当class派生自一个继承链,其中有一个或多个virtual base classes时。

前两种情况,编译器必须将member或base class的"copy constructor调用操作"安插到被合成的copy constructor中。

后两种情况比较复杂,介绍如下:

重新设定Virtual Table的指针

只要有一个class 声明了一个或多个virtual functions,那么程序在编译期间就会发生扩张操作:

  • 增加一个virtual function table(vtbl),内含一个有作用的virtual function的地址。
  • 每个class object都会被安插一个指向vtbl的指针vptr。

如果编译器对新产生的class object不能成功而且正确的设定其初值,那么后果很可怕。

因此,当编译器导入一个vptr到class中,这个class 就不再展现bitwise sematics了(为啥)。

因为编译器需要合成出一个copy constructor以使得vptr适当的初始化(而不是简单的把本来的vptr值copy过来)。

合成的copy constructor会显式设定object的vptr指向的class的virtual table,而不是从=的右边的class object中将其vptr值拷贝过来。

如果是相同的class 的object间发生的copy行为,那么可以直接使用bitwise copy ,因为它们的vptr指向相同的vtbl。

如果用derived class object去初始化base class object,那么derived class 中比base class多的部分会被切割掉(sliced)。

另外,通过一个class object去访问其member function(即使是virtual function),那只能访问到这个class object所在派生层级的那个function(主要是针对virtual function 讲)。而多态发生在通过指针或reference调用virtual member function的时候。而非通过class object 调用。这一点需要注意。

处理Virtual Base Class Sub-object

Virtual base class的存在需要特殊处理。

一个class object如果以另一个object作为初值,而后者有一个virtual base class sub-object,那么这也会导致“bitwise copy semantics”失效(也就是会使编译器有产生copy constructor的机会)。

每个编译器对虚拟继承的支持承诺,都代表必须让**"derived class object 中的virtual base class sub-object位置"执行期就准备妥当**。维护“位置的完整性”是编译器的责任。“bitwise copy semantics"可能会破坏这个位置。因此,编译器必须在它自己生成的copy constructor中作出仲裁。

总结一下,就是在virtual base class存在的派生体系中,两个不同的class的object间发生的copy行为,需要通过copy constructor来改变一些东西,使得程序正常运行。具体来讲,编译器会合成copy constructor或安插一些代码,来设定virtual base class pointer/offset 的初值,对每个members执行必要的初始化操作,以及执行其他的内存相关工作。

小结:

有四种情况,class不再保持"bitwise copy semantics"(也就是不能简单的把member所在内存单元的值拷贝到目标object中相应member的内存单元位置)。这时如果default copy constructor未被声明的话,会被视为nontrivial。这四种情况下,如果缺乏一个已声明的copy constructor,编译器为了正确处理“以一个class object作为另一个class object的初值”,必须合成一个copy constructor。

2.3 程序转化语义学(Program Transformation Semantics)

这一节主要讨论编译器调用copy constructor的策略,以及这些策略如何影响我们的程序。

考虑下面的程序片段:

#include "X.h"

X foo()
{
    X xx;
    // ... 
    retrun xx;
}

可能会有下面两个假设:

  1. 每次调用foo(),都会传回xx的值
  2. 如果class X 定义了一个cp ctor,那么foo()调用时,保证该cp ctor也会被调用

对于以上两个假设:第一个假设的真实性,视class X的定义而定;第二个假设的真实性,视class X 的定义和C++编译器所提供的优化层级而定。

显式的初始化操作(Explicit Initialization)

对于以下定义:

X x0;

下面三个定义都是显式的以x0来初始化object:

void foo_bar()
{
	X x1(x0);				// 定义了 x1 ,下面分别定义了x2,x3
    X x2 = x0;
    X x3 = X(x0);
}

必要的程序转化有两个阶段:

  1. 重写每个定义,其中的初始化操作会被剥除;
  2. class 的cp ctor调用操作会被安插进去。

举个栗子,双阶段转化之后,foo_bar()可能看起来如下:

// 可能的程序转换
// c++ psudocode
void foo_bar()
{
	X x1;				// 定义被重写,初始化操作被剥除
    X x2;
    X x3;
    
    // 编译器安插 X copy construction的调用操作
    x1.X::X(x0);
    x2.X::X(x1);
    x3.X::X(x2);
}

参数的初始化

把一个class object当做参数传给 一个函数(或是作为函数的返回值),相当于一下形式的初始化操作:

X xx = arg;

其中xx代表形式参数(或返回值),而arg代表真正的参数值。

因此,对于下面这个函数:

void foo(X x0);

下面的这种调用方式:

X xx;
// ...
foo(xx);

将会要求局部实例(local instance)x0以member-wise的方式将xx作为初值。

在编译器的实现上,有一种策略是,导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数。

前一段程序代码将转换如下:

// C++ pseudocode
// 编译器产生出来的临时对象
X __temp0;

// 编译器对copy constructor的调用
__temp0.X::X(xx);

// 重新改写函数调用操作,以便使用上述的临时对象
foo(__temp0);

书上讲这里有个问题,但我没看懂是啥意思

突然想明白了,上面这个地方必须得改成:

void foo(X& x0);

不然会发生无穷递归调用的copy constructor。

不对。。。还是不明白为啥本来不行

另一种实现是,以“拷贝构建(copy construct)”的方式把实际参数之间构建在其应该的位置上,此位置视函数活动范围的不同,记录在堆栈中。函数返回前,局部对象的destructor会被执行。

返回值的初始化

X foo()
{
    X xx;
    // ... 
    retrun xx;
}

对于上述函数定义,bar()的返回值如何从局部对象xx中拷贝出来?

一种编译器的实现是:

  1. 首先加上一个额外参数,类型是class object的一个reference。这个参数放被“拷贝建构(copy constructed)”而得的返回值。
  2. 在return 指令之前安插一个copy constructor调用操作,以便将想传回的object的内容当做上述新增参数的初值。

这样的实现导致它并不返回任何值,其实现如下:

// 函数转换
// c++ pseudocode
void bar(X& __result)
{
    X xx;
    
    // 编译器所产生的default constructor调用操作
    xx.X::X();
    
    // .... 处理xx
    
    // 编译器所产生的copy constructor操作
    __result.X::X(xx);
    
    return;
}

现在编译器必须转换每个bar()调用操作,以反映其新定义:

X xx = bar();

// 上面一条将会被转换为下面两个指令句
// 注意:这里不必施行default constructor
X xx;
bar(xx);

Optimization at he User Level

方法:定义一个**“计算用”**的constructor。

即不再写下面这样的代码:

X bar(const T& y, const & z)
{
	X xx;
	// ... 以y 和 z 处理xx
    return xx;
}

上面的代码会要求xx被"member-wise"地拷贝到编译器产生的__result中。

我们的优化方案是,重新定义一个新的constructor,来计算xx的值,并返回:

X bar(const T& y, const & z)
{
	return X(y,z);
}

上面的代码__result直接被计算出来,而不是被copy constructor拷贝而得。

这种类型的优化主要考虑有更佳的效率,而不是以“支持抽象化“为优先。

Copy Constructor:要还是不要?

在没有四种导致nontrivial copy constructor 的情况下,是否需要explicit copy constructor呢?

视情况而定,分析member-wise的操作导致的bit-wise copy 是否会导致memory leak,是否会导致address aliasing。

如果class需要大量的member-wise初始化操作,比如以by value的方式传回objects,那么提供一个copy constructor 的 explicit inline函数的实例就很合理——如果编译器提供NRV优化。

2.4 成员初始化列表 Member initialization List

有两种方法在constructor中设定class members的初值:通过member initialization list或constructor 函数本体之内。

这一节主要阐述:何时使用initialization才有意义,list内部的真正操作,一些微妙的陷阱

为使程序顺利正常编译,而必须使用member initialization list的情况

  1. 初始化一个reference member时
  2. 初始化一个const member时
  3. 调用一个base class的constructor,而它有一组参数时
  4. 调用一个member class的constructor,而它有一组参数时

考虑下面的栗子:

class Word{
	String _name;
    int    _cnt;
public:
    // 没有错误,但是效率太低
    Word(){
        _name=0;
        _cnt=0;
    }
}

上面的栗子有很大的开销:首先Word constructor先产生一个临时性的String object,然后将其初始化,然后以一个operator=将临时性object指定给_name,然后调用临时object的析构函数。

但是用成员初始化列表就能将开销减少到只有一个_name 这个String object的constructor的调用。

member initialization list中到底发生了什么?

编译器会一一操作initialization list,然后以适当的顺序在constructor内安插初始化操作,并且在任何explicit user code之前

需要注意的是:list中初始化的顺序是class 中member声明顺序决定的,而不是initialization list中的排列顺序决定的。

因此在成员初始化列表中要特别注意初始化的顺序,特别是某个member的初始化用到了前面刚初始化的member的值,这个时候需要分析他们在class中定义的先后顺序。

小结:编译器会对initialization list一一处理并可能重新排序,以反映members的声明顺序。它可能会安插一些代码到constructor体内,并置于任何explicit user code之前。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值