爱技术 & 爱分享
爱蛋蛋 & 爱生活

Default Constructor(默认构造函数)的构造操作

这篇文章,我们来讨论一下什么情况下编译器会给一个类合成默认构造函数。

在 C++ Annotated Reference Manual(ARM) 中告诉我们 “default constructor 将在需要的时候被编译器合成出来”

我们来看一个灰常灰常简单的类

class Foo
{
public:
    int val;
}

然后看这样一个使用场景

Foo bar;
if(bar.val==0)  //本意希望 bar 的 val 被程序自动初始化为 0
{
    //do something...
}

先不说这个代码写的怎样,不过在这个例子中,正确的程序语言是要求 Foo 有一个 default constructor , 可以将它的member variable 初始为0。

上面这段代码可曾符合ARM所说的“在需要的时候”? 开玩笑,肯定不是呀。

其间的差别在于一个是程序需要,一个是编译器需要。程序需要那是coder (oh no ,please call them Software Engineer )自己的责任,也就是说需要写下这段代码的人自己控制,也就是说上面那段代码肯定不会有一个 default constructor 被合成出来。

听老哥你这么一说我就有点懵逼了,那么啥时候才会合成一个 default constructor 呢?

不好意思,还是那句话“当编译器需要它的时候”。而且,被合成出来的 constructor 只执行编译器所需的行动。

也就是说,即使为 class Foo 合成出一个 default constructor ,那个 constructor 也不会将 member variable 初始化为0。

为了让上一段代码正确执行,class Foo 的设计者 必须显示的提供一个 default constructor ,将member variable 初始化。

不过这里还有一点值得说明的是,Global object 的内存保证会在程序启动的时候被清0,除此之外不会再有任何情况自动清0对象内存,他们的内容将是内存上次使用之后的遗留数据。

好吧,当你看到这篇文章的时候 C++ Standard 已经修改了 ARM 中的说法。不过好像也没什么卵用,因为其行为事实上仍然是相同的。

对于 Class X 如果没有任何 user-declared constructor 那么会有一个 default constructor 被隐式声明出来,除以下几种情况,一个被隐式声明出来的 default constructor 将会是一个 trivial constructor(没任何卵用的…)

然后我们来看看 C++ Standard 中是如何描述 implicit default constructor 会被视为 trivial(无用的)。

一个 nontrivial(有用的) default constructor 在ARM 的术语中就是编译器所需要的那种,必要的话会被合成出来。

“带有 Default Constructor”的 Member Class Object

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

但是这里有一个有趣的问题:C++多个编译模块之间,如何避免合成多个 default constructor ?

解决方法是把合成的 default constructor,copy constructor,destructor,assignment copy constructor 都以 inline 的方式完成。一个 inline 函数有静态链接(static linkage),不会被文件以外者看到。如果函数实现太过复杂,不适合做成 inline , 就会合成出一个 explicit non-inline static 实例。

举个例子:

class Foo{ public: Foo() ,Foo(int) ·····};
class Bar{ public: Foo foo; char *str; };

void foo_bar()
{
    Bar bar;    //此处会初始化bar  以及内含的 foo 对象。 class Foo 拥有 default constructor。
    if(bar.str){} ···
}

被合成出来的 Bar default constructor 内含必要的代码,能够调用 class Foo 的default constructor 来处理 member object Bar::foo ,但它并不产生任何代码来初始化 Bar::str 。将 Bar::foo 初始化是编译器的责任,但是将 Bar::str 初始化是程序员需要做的事情。

//合成的代码看起来像这样
inline
Bar::Bar()
{
    //c++ 伪码
    foo.Foo::Foo();
}

这里还得注意一下: 被合成的 default constructor 只满足编译器的需要,而不是程序的需要。

那么为了满足程序的需求,你可能会定义如下的构造函数

//程序员自己定义的 default constructor
Bar::Bar()
{
    str=0;
}

现在程序的需求被满足了,但是 foo 成员变量怎么办呢? 因为已经手动定义了一个,编译器将不会自动合成第二个了。

恩,你可能会说,没毛病啊 ,平时代码不也是这写的? 好像也没出过啥错啊。

那是因为编译器采取了一下行动:

如果 class A 内含一个及以上的 member class objects 那么 class A 的每一个 constructor 必须调用每一个 member class 的 default constructor。编译器会扩张已存在的 constructor , 在其中安插一些代码,使得 user code 执行之前,先调用必要的 default constructor 。

//扩张后的 default constructor
//c++ 伪码
Bar::Bar()
{
    foo.Foo::Foo();
    str=0;
}

当然,为了方便,我们这里是没有讨论 this 指针的。

如果有多个 class member object 都要求 constructor初始化操作,将如何?

C++语言要求 以“member object 在 class 中的声明顺序” 来调用各个 constructor。这一点由编译器完成,他为每一个 constructor 安插代码。这些插进去的代码会被放在 explicit user code之前。换句话说就是 带有默认构造函数的成员变量会被以其在类中的声明顺序初始化,并且在所有用户代码之前完成。

考虑一下三个 classes

class Dopey{public : Dopey();···};
class Sneezy{public : Sneezy();Sneezy(int x);···};
class Bashful{public : Bashful();···};

以及一个class Snow_White

class Snow_White
{
public:
    Dopey dopey;
    Sneezy sneezy;
    Bashful bashful;
    //···
}

如果 Snow_White 没有定义default constructor, 那么就会有一个 nontrivial(有用的) constructor 被合成出来 依次调用各成员的 default constructor。

然而如果是这样子定义的话

Snow_White::Snow_White():sneezy(1024)
{
    //···
}

仍然是以对象的声明顺序初始化的,最后 default constructor 会被扩张为:

//c++ 伪码
Snow_White::Snow_White():sneezy(1024)
{
    //插入 member class object
    //调用其 constructor
    dopey.Dopey::Dopey();
    sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();

    //explict user code
    //···
}

“带有 Default Constructor”的 Base Class

类似的道理,如果一个没有任何 constructor 的 class 派生自一个”带有 default constructor”的 base class, 那么这个 drived class 的default constructor 会被视为 nontrivial,并因此被合成出来。它将调用上一层 base classes(没错,就是复数形式,因为可能有多个基类呀) 的 default constructor(根据声明顺序)。对一个后继派生的class而言,这个合成的 constructor 和一个”被显示提供的 default constructor” 并没有任何差异,因为子类构造之前要先构造父类,这个操作无论是自动合成的构造函数 还是手动提供的,这都没啥区别。

如果程序员提供多个 constructor 但是并没有一个是 default constructor 怎么办呢?编译器会扩张现有的每一个 constructor,将”用以调用所有必要的 default constructor的代码插入进去”。

是的编译器肯定不会合成一个新的 default constructor, 因为其他”user define constructor”存在的原因。

如果同时存在着”带有 default constructor “的member class object,那些 default constructor 也会被调用—–在所有 base class constructor 都被调用之后。这一点毫无疑问,因为永远都是先初始化父类再初始化之类的。

“带有 Virtual Function”的 Class

带有 virtual function 的 class 在被合成或扩张出来的 constructor 会干那些事情呢?

下面两个扩张行动会在编译期间发生

  • 一个 virtual function table(vtbl) 会被编译器生产出来,里面放置的是 class 的 virtual function 地址。
  • 在每一个 class object 中,一个额外的 pointer member (传说中的 vptr)会被编译器合成出来,内含相关 class vtbl 的地址(多态的关键)。

假设有这样一个类:

class Widget
{
public :
    virtual void Flip()=0;
}
void Func(const Widget& widget){widget.flip();}

//假设 Bell 和 Whistle 都继承自 Widget

void Foo()
{
    Bell b;
    Whistle w;
    Func(b);
    Func(w);
}

Func 中的 widget.flip()调用操作会被改写,以使用widget的vptr 所指向的vtbl中的 Flip 函数地址(而Flip 函数地址 在vtbl中的条目位置是固定的)。转换过后代码可能如下:

(*widget.vptr[1])(&widget)

参数 &widget 代表this指针的传递。

为了让这个机制发生作用,编译器必须为每一个 class object 的 vptr 设定初值,以存放正确的 virtual table 的地址。如果有 constructor 那么在此基础上进行扩张,如果还没有那么编译器会合成一个 default constructor,以便正确地初始化每一个 class object 的 vptr (这个时机会在任何用户定义的代码之前)。

“带有Virtual Base Class”的 class

这是一个比较让人迷惑的点…因为,Virtual base class 的实现方法在不同的编译器之间有极大,极大,极大的差异。

不过别担心,幸好还有一点让人松一口气,那就是每一种实现手法的共同点都是 必须使 virtual base class 在每一个 drived class object 中的位置能够于执行期准备妥当。

例如下面的代码:

class X
{
public:
    X() {};
    ~X() {};
    int i;
};


class A :virtual public X
{
public:
    A() {};
    ~A() {};
    int j;
};


class B:virtual public X
{
public:
    B() {};
    ~B() {};
    int d;
};


class C :public A, public B
{
public:
    C() {};
    ~C() {};
    int k;
};

//无法在编译时期推断出pa->X::i的位置
void foo(const A* pa)
{
    pa->i=1024;
}

int mian()
{
    foo(new A);
    foo(new C);
    return 0;
}


Diamond-Inheritance

编译器无法固定住 foo之中由 pa间接存取 X::i 的时机偏移位置,因为 pa 的真正类型是可以改变的。所以编译器必须改变”执行存取操作”的那些代码,使 X::i 可以 延迟至执行期才决定下来。

在 c++ 始祖级编译器 cfront 中是这样做的 “为 derived class object 的每一个 virtual base classes 安插一个指针。 所有经由 reference 或者 pointer 来存取 一个virtual base class 的操作都通过相关指针完成”

所以foo()可被改写成这样

void foo(const A* pa)
{
    pa->_vbcx->i=1024;//_vbcx表示编译器所产生的指针,执行 virtual base class X
}

(这里有一个问题就是,当传入的是 X 的指针的时候,这里如果被转换的话,那么肯定是会出问题的。除非将虚基类本身也一起安插代码 这个指针指向的其本身。)

所以,让我们回到主题,_vbcx(反正是编译器做出来的某些东西,我们暂且这么称呼它)是在class object 构造其间完成的。对于 class 所定义的每一个 constructor, 编译器会安插那些 “允许每一个virtual base class的执行期存取操作”的代码。如果class 没有声明任何 constructor,编译器必须为它合成一个 default constructor。

总结

有四种情况会造成编译器必须为未声明constructor的class 合成一个 default constructor。

分别是:

  • 带有 Default Constructor 的 Member Class Object
  • 带有 Default Constructor 的 Base Class
  • 带有 一个及以上 Virtual Function 的 Class
  • 带有 一个及以上 Virtual Base 的Class

C++ Standard 将这些合成的 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 subobject 和 member class object 会被初始化(不包括 virtual 机制)。所有其他的nonstatic data member 都不会被初始化。

误区

  • 任何 class 如果没有定义 default constructor 就会被合成一个出来
  • 编译器合成出来的 default constructor 会显式设定 class 内每一个 data member 的值。

喏 这些都是假的…..


1200

赞(0) 传送门
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。编程日志 » Default Constructor(默认构造函数)的构造操作
分享到: 更多 (0)

游戏 && 后端

传送门传送门