c++:构造函数(constructor)和析构函数(destructor)中编译器的隐式行为(implicit behavior)

【注:心急的可以通过目录直接看每段的结论更心急的可以直接跳到最后看总结但是这样的看法估计和教材里学到的差不多

上篇文章提到,c++为了保证向前的兼容性和新加入的各种花哨功能,引入了很多隐式行为。这都是本文作者所不喜的。但无奈这是谋生工具,所以还是得静心了解一番,总结于此。本文首发地址:http://blog.csdn.net/madongchunqiu/article/details/22420877


这篇文章主要是看看c++在继承的过程中,向基类和继承类的构造函数(constructor)、拷贝构造函数(copy constructor)和析构函数(destructor)里,都隐式的塞了些什么东西。

本文的结论由MAC OS X 10.9 + XCode 5.0.1来验证。

(据称Xcode从4.2开始支持C++11。本测试采用新建的OS X Application (Command Line Tools -> C++) Project,C++ Language Dialect的默认选项是“GNU++11”)


前置条件

1. 本文仅验证构造、析构在继承中的相关方面,别的不予考虑;

2. 本文不考虑多重继承相关事宜;

3. 本文例子中定义的基类(父类)以B开头:Bi(Base, implicit)表示隐式生成构造/析构函数的基类,Bu(Base,user-defined)表示用户定义了构造/析构函数的基类

4. 本文例子中定义的继承类(子类)以D开头:Di(Derived, implicit)表示隐式生成构造/析构函数的继承类,Du(Derived,user-defined)表示用户定义了构造/析构函数的继承类


B类定义如下:

class Bi
{
  public:
    int b;
    virtual ~Bi() //考虑到作为基类,还是有必要提供virtual dtor的
    {
        cout << Bi dtor b=" << b << endl;
    }
};

class Bu
{
  public:
    int b;
    Bu(int b_=1):b(b_) //注:可参看上一篇博文。另,这里若参数不带默认值的话,将会
    {                  //缺少默认构造函数,继承类隐式调用基类默认构造函数时,编译器会报错
        cout << "Bu ctor b=" << b << endl;
    }
    Bu(const Bu& rhs):b(rhs.b)
    {
        cout << "Bu copy ctor b=" << b << endl;
    }
    virtual ~Bu()
    {
        cout << "Bu dtor b=" << b << endl;
    }
};

D类定义如下

class Di:public B
{
  public:
    int d;
}

class Du: public B
{
  public:
    int d;
    Du(int d_=2):d(d_)  //同上一篇博文所述,此为带默认参数的构造函数
    {
        cout << "Du ctor b=" << b << " d=" << d << endl;
    }
    Du(const Du& rhs):B(rhs),d(rhs.d)  //用户定义的拷贝构造函数,手动调用了基类的拷贝构造函数
    {
        cout << "Du copy ctor b=" << b << " d=" << d << endl;
    }
    virtual ~Du()
    {
        cout << "Du dtor b=" << b << " d=" << d << endl;
    }
}

情况一:构造函数(constructor)在默认初始化(default-init)和数值初始化(value-init)中的情形

case 1: compare the "default initialization" and "value-initialization" in the constructor

继承类构造函数(constructor of derived class)的初始化顺序为:(来自某draft版C++11 12.6.2/10:

In a non-delegating constructor, initialization proceeds in the following order:
 ①First, and only for the constructor of the most derived class (1.8), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.
 ②Then, direct base classes are initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).
 ③Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
 ④Finally, the compound-statement of the constructor body is executed.

对此构造函数调用顺序进行验证的方法是:将上面的第二段代码中的基类由B分别改成Bi或者Bu,并分别进行如下的测试:

【注:由于heap可能会被初始化为0,因此需要采用先向heap中写入数值,再创建对象的方法,以确认置0的操作是否由程序完成。具体方法可参看上一篇博文。】


1.1 当基类是Bi时(即基类没有提供构造函数,隐式构造函数将由编译器提供):

默认初始化(default initialization)
即:new D
b的值 d的值 解释
Di* pDi = new Di;
基类没有定义构造函数
继承类没有定义构造函数
??? ??? 基类和继承类都没有构造函数时:
②隐式的调用基类默认构造函数,该函数由编译器生成,不执行初始化操作,因此b的值未知
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,该函数由编译器生成,不执行初始化操作,因此d的值未知
Du* pDu = new Du;
基类没有定义构造函数
继承类用户定义的构造函数
??? 2 继承类提供了构造函数,其:
②隐式的调用基类默认构造函数,该函数由编译器生成,不执行初始化操作,因此b的值未知
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,d被初始化为2

表1.1-1

数值初始化(value initialization)
即:new D()
b的值 d的值 解释
Di* pDi2 = new Di();
基类没有定义构造函数
继承类没有定义构造函数
0 0 继承类没有构造函数,因此执行了零值初始化(zero initialization)
并且,虽然隐式的默认构造函数不是trivial default constructor(因为我定义了一个virtual的dtor),需要被调用,但是此编译器生成的构造函数不执行任何初始化操作
②隐式的调用基类默认构造函数,该函数由编译器生成,不执行初始化操作,因此b的值未知
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,该函数由编译器生成,不执行初始化操作,因此d的值未知
Du* pDu2 = new Du();
基类没有定义构造函数
继承类用户定义的构造函数
??? 2 继承类构造函数时,调用默认构造函数:
②隐式的调用基类默认构造函数,该函数由编译器生成,不执行初始化操作,因此b的值未知
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,d被初始化为2
【注:这里的结果虽然很好的符合了标准,但是和“数值初始化”的语义是相违背的,因为“数值初始化”从名字来看,应该是希望所有的字段都被赋值了的,这应该算是标准里面的Bug了。可以从下面两方面来看待这个问题:
a. C++努力想要榨干程序运行效率的每个细节,心很大,也很好。代价就是可能就会出现这种无法预测的Bug;
b. C++既然在默认初始化方法之外另设了数值初始化,那么使用数值初始化的人应该有觉悟会付出一定的运行效率损失。那么,在这种默契之下,强制做一个零值初始化(而不是判断条件,仅在某些情况下才做零值初始化)又有何不可呢?
当然,上面说这么多,也是我管的太宽了。又有哪个在写基类时不提供构造函数呢。。。】

表1.1-2


1.2 当基类时Bu时(即基类中有用户提供的构造函数)

默认初始化(default initialization)
即:new D
b的值 d的值 解释
Di* pDi = new Di;
基类用户定义的构造函数
继承类没有定义构造函数
1 ? 基类定义了构造函数,但是继承类偷懒没定义时:
②隐式的调用基类默认构造函数,b被初始化为1
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,该函数由编译器生成,不执行初始化操作,因此d的值未知
Du* pDu = new Du;
基类用户定义的构造函数
继承类也用户定义的构造函数
1 2 基类和继承类都定义了构造函数时:
②隐式的调用基类默认构造函数,b被初始化为1
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,d被初始化为2

表1.2-1

数值初始化(value initialization)
即:new D()
b的值 d的值 解释
Di* pDi2 = new Di();
基类用户定义的构造函数
继承类没有定义构造函数
1 0 继承类没有构造函数时,因此执行了零值初始化(zero initialization)
并且,隐式的默认构造函数不是trivial default constructor(因为我定义了一个virtual的dtor),需要被调用:
②隐式的调用基类默认构造函数,b被初始化为1
③调用各成员的构造函数(本例无)
④执行本继承类构造函数,该函数由编译器生成,不执行初始化操作
【注:先执行了零值初始化(zero initialization)后,再执行的默认构造函数。这种行为符合数值初始化(value initialization)中的相关定义】
Du* pDu2 = new Du();
基类用户定义的构造函数
继承类也用户定义的构造函数
1 2 继承类构造函数时,调用默认构造函数,情况同默认初始化(default initialization)
请参看上表1.2-1中的相同项

表1.2-2


结论

1. 构造函数(constructor)阶段,标准定义的/编译器的隐式行为是:①②③(见本章节开头的Standard截取部分)。其中,相应的项如果用户手动调用了,就不会隐式调用了。相当于说,用户定义的构造函数是仅第④步,隐式行为发生在构造函数(constructor)之前。(请自行对比析构函数(destructor)的情况)

2. 运行结果符合标准(看起来像是废话),但是由于数值初始化(value initialization)的存在,符合标准的代码却“可能”运行出预料外的结果(见表1.1-2最后一栏的解释)。因此其启示是:基类的构造函数最好不要是隐式的,需要用户定义。当然,基类设计(甚至是否采用继承)是相当考量、花费不少心思的事情,又有哪个会不提供构造函数呢。。。


情况二:拷贝构造函数(copy constructor)的情形

case 2: implicit behavior in copy constructor

只查到隐式拷贝构造函数在标准中的定义(某draft版C++11 12.8/15):

The implicitly-defined copy/move constructor for a non-union class X performs a memberwise copy/move of its bases and members. The order of initialization is the same as the order of initialization of bases and members in a user-defined constructor (see 12.6.2). Let x be either the parameter of the constructor or, for the move constructor, an xvalue referring to the parameter. Each base or non-static data member is copied/moved in the manner appropriate to its type:
 — if the member is an array, each element is direct-initialized with the corresponding subobject of x;
 — if a member m has rvalue reference type T&&, it is direct-initialized with static_cast<T&&>(x.m);
 — otherwise, the base or member is direct-initialized with the corresponding base or member of x.

具体开工测试之前,还需要引入一个新的派生类Du2,它与Du的区别在于,Du手动调用了基类B的拷贝构造函数,而Du2没有。

如下:

class Du2: public B
{
  public:
    int d;
    Du2(int d_=2):d(d_)
    {
        cout << "Du2 ctor b=" << b << " d=" << d << endl;
    }
    Du2(const Du2& rhs):d(rhs.d)   //区别在此:没调用基类的拷贝构造函数
    {
        cout << "Du2 copy ctor b=" << b << " d=" << d << endl;
    }
    virtual ~Du2()
    {
        cout << "Du2 dtor b=" << b << " d=" << d << endl;
    }
}
测试时发现,测试基类为Bi(即拷贝构造函数为隐式生成)的条目没有提供足够的分析信息。因此下面仅以Bu(用户提供构造函数和拷贝构造函数)为例:

拷贝构造语句 输出信息 解释
Di di;
Di di2(di);
Bu copy ctor b=1 Di隐式生成的拷贝构造函数调用了基类的拷贝构造函数
符合标准定义
Du du;
Du du2(du)
Bu copy ctor b=1
Du copy ctor b=1 d=2
Du由用户提供的拷贝构造函数,其中调用了基类的拷贝构造函数
符合
Du2 du_;
Du2 du_2(du_)
Bu ctor b=1
Du2 copy ctor b=1 d=22
Du2由用户提供的拷贝构造函数,其中没有调用基类的拷贝构造函数(copy ctor),结果基类的默认构造函数(default ctor)被用于初始化基类的数据。
这个可以理解成:用户提供的拷贝构造函数(user-defined copy ctor)被当作特殊的构造函数(ctor)来对待(?),走“情况一”的流程了。

【注:拷贝构造函数(copy ctor)的难兄难弟赋值运算符(assignment operator)的情况是类似的,只不过如果用户定义的赋值运算符(assignment operator)忘了处理基类,那么就真的没有任何处理了。当然,如果用户不定义赋值运算符(assignment operator),编译器还是会自动构造一个的,大体上和拷贝构造函数相同,如下(某draft版C++11 12.8/28):】

The implicitly-defined copy/move assignment operator for a non-union class X performs memberwise copy/move assignment of its subobjects. The direct base classes of X are assigned first, in the order of their declaration in the base-specifier-list, and then the immediate non-static data members of X are assigned, in the order in which they were declared in the class definition. Let x be either the parameter of the function or, for the move operator, an xvalue referring to the parameter. Each subobject is assigned in the manner appropriate to its type:
 — if the subobject is of class type, as if by a call to operator= with the subobject as the object expression and the corresponding subobject of x as a single function argument (as if by explicit qualification; that is, ignoring any possible virtual overriding functions in more derived classes);
 — if the subobject is an array, each element is assigned, in the manner appropriate to the element type;
 — if the subobject is of scalar type, the built-in assignment operator is used.


结论:

1. 拷贝构造函数阶段(copy constructor)

  a. 若用户没有定义拷贝构造函数,这产生隐式的拷贝构造函数(implicit copy constructor),执行按对象拷贝(member-wise copy)行为;

  b. 若用户定义拷贝构造函数,则走构造函数的流程(?)。因此若用户没有手动调用基类的拷贝构造函数,会隐式的调用基类的默认构造函数。结论是:若定义拷贝构造函数(copy constructor),记得处理基类的初始化动作。(赋值运算符(assignment operator)同理)


情况三:析构函数(destructor)的情形

case 3: implicit behavior in destructor
继承类析构函数(destructor of derived class)的摧毁顺序为:(来自某draft版C++11 12.4
After
⑴ executing the body of the destructor and destroying any automatic objects allocated within the body,
⑵ a destructor for class X calls the destructors for X’s direct non-variant non-static data members,
⑶ the destructors for X’s direct base classes and,
⑷ if X is the type of the most derived class (12.6.2), its destructor calls the destructors for X’s virtual base classes.
All destructors are called as if they were referenced with a qualified name, that is, ignoring any possible virtual overriding destructors in more derived classes. Bases and members are destroyed in the reverse order of the completion of their constructor (see 12.6.2). A return statement (6.6.3) in a destructor might not directly return to the caller; before transferring control to the caller, the destructors for the members and bases are called. Destructors for elements of an array are called in reverse order of their construction (see 12.6).
看出来了,这个顺序和构造函数(constructor)正好相反。

测试代码中,遵循古老的传统,我们已经在基类中都定义了virtual destructor,所以测试时采用Bu或Bi作为基类都可以。继承类Di有隐式的析构函数(implicit destructor);而继承类Du有用户定义的析构函数(user defined destructor)。
析构函数语句 输出信息 解释
Di* pDi = new Di;
delete pDi;
Bu dtor b=1 隐式生成的析构函数(implicit destructor)
正确调用了基类的析构函数
Du* pDu = new Du;
delete pDu;
Du dtor b=1 d=2
Bu dtor b=1
用户定义的析构函数(user defined destructor)【注:内容为空】
正确调用了基类的析构函数

结论:

1. 析构函数(destructor)阶段,标准定义的/编译器的隐式行为是:⑵⑶⑷。相当于说,用户定义的析构函数是仅第⑴步,隐式行为发生在析构函数(destructor)之后。(请自行对比构造函数(constructor)的情况)。

2. 附上古老传统:基类的析构函数(destructor)一般会定义为virtual的(这里有个判断方法)。


情况N:其它的情形

当然,还有很多很扰人的小细节,要把这些细枝末节都弄清弄明真不是一件容易的事情。比如:

1. 编译器何时会隐式的生成各种ctor/dtor呢?C++11 compiler generated functions

2. 编译器隐式生成的dtor和用户定义的"空dtor"有何不同?Will an 'empty' destructor do the same thing as the generated destructor?

不过我觉得作为一个合格的使用者,了解前三种情况差不多够用了。


总结

1. 构造函数(constructor)除了初始化自己,也许会有初始化其直接基类的需求--调用其带参数的构造函数。其它的就让隐式动作来完成吧

2. 拷贝构造函数(copy constructor)如果用户定义了,就需要管理其直接基类的拷贝,以避免隐式的行为发生。赋值运算符(copy assignment operator)同样如此。

3. 析构函数(destructor)管好自己就行了,父祖自有父祖福。其它的就让隐式动作来完成吧


========================== 你可能很感兴趣的分割线 ========================== 

========================== 你可能不感兴趣的分割线 ==========================

同上篇博文,也来比比Objective-C的情况。

相对于C++的各种constructor和destructor的潜规则,Objective-C的初始化(init)和删除(dealloc)操作显得简洁优雅。究其原因,大概是C++没有把构造函数看成是一个interface,并且由于支持多重继承,也没有super这种好用的东东。而Objective-C仅仅将init和dealloc当作普通interface的一员来统一处理,因此只需要记住一点小小的“模板”就行了(XCode会帮助自动生成init和dealloc的模板)。

由于初始化(init)和删除(dealloc)函数都是从基类继承而来,因此如果继承类没有定义这两个函数(比如:仅含有基本数据类型,因此没有必要分配/删除对象),基类的init/dealloc会被调用,完全没有问题。

可美好的东西总是结束的比较快。ARC带来了一点点隐式的处理。所以,我还是想说我不太喜欢ARC(虽然还没试用过)。以下是代码:

没有ARC的版本:init和dealloc真是完美的对称。

- (id)init
{
    self = [super init];
    if (self) {
    }
    return self;
}
- (void)dealloc
{
    [super dealloc]; //没开ARC,必须调用super
}

了ARC之后(见参考[1]):dealloc多了个隐式行为,不那么讨喜了。

1. self = [super init],即返回对象必须赋值给self

2. dealloc中不能调用[super dealloc],继承链的处理将由编译器接管,隐式处理;

3. dealloc中一般也只处理ARC还没管到的东西:Core Foundation, file descriptors等资源。如果类中只含有基本数据类型和普通对象类型(NSString等),则dealloc都不必写了。(当然,作为好的习惯,我觉得还是应该写的,即使是个空函数。因为它告诉我们:这里是终点。)

- (id)init
{
    self = [super init]; //打开ARC,必须返回给self。否则编译器会报错(因此不记住也可以)
    if (self) {
    }
    return self;
}
- (void)dealloc
{
    // [super dealloc];  //打开ARC,继承链调用被接管,不能写。否则编译器会报错(因此不记住也可以)
}

总体而言,Objective-C里面的隐式动作还是比较少的。就算开启ARC后多了一些隐式动作,若代码不符合ARC,编译器也会报错,因此学习的成本小、需要记住的少,还是容易接受的。

参考

[1] Transition to ARC


你可能感兴趣的:(c++:构造函数(constructor)和析构函数(destructor)中编译器的隐式行为(implicit behavior))