目录
- 前言
- 普通类构造析构顺序
- 解析
- 依赖关系产生的错误
- 派生类构造析构顺序
- 解析
- 扩展菱形多继承场景
- 含虚基类的派生类构造析构顺序
- 解析
- 扩展菱形多继承场景(引入虚继承)
前言
C++规定“对象的析构过程必须与其构造过程相反”这一语法规则。
因此我们研究透彻了构造过程,那么析构过程自然就是相反的;
(因为一个类的成员初始化和析构,类似一个压栈与弹栈的过程)
普通类构造析构顺序
- 按照成员变量生命的顺序构造
#include<iostream>
int i = 1;
using namespace std;
class A
{
public:
A():a(i++),b(i++) {} //i为全局变量,初始值为1
int b;
int a;
};
int main()
{
A x;
cout << x.a <<' '<< x.b << endl; //运行结果2 1 ;
return 0;
}
可以看到,虽然我们在类型A默认构造中的初始化列表里指定了构造顺序: 先构造a成员,再构造b成员;
事实上,还是按照成员变量的声明顺序,先构造了b成员为1,再构造了a成员为2!
解析
对象的析构过程必须与其构造过程相反:
一个类有不止一个构造函数,但是只有一个析构函数!
如果这些构造函数对成员的初始化顺序各不相同,那么在析构这个类的对象时,应当遵循什么样的成员析构顺序呢?难到要对构造改类对象时的初始化顺序进行某种方式的记录吗?
实际上,析构函数是不会关心这个类的对象是使用哪个构造函数构造出来的,但同时又要匹配一个确定的析构顺序,那么显然编译器只能根据类声明中成员变量的出现顺序来制定成员变量的初始化顺序,紧接着相反的析构的顺序也就确定了;
依赖关系产生的错误
若成员的初始化存在某种依赖时,应当在类声明中给出注释以进行说明,而不是将正确顺序写在某个构造函数里,应当着重注意声明顺序,避免不必要的错误。
初始化定义的顺序是先a后b,但编译器实际上是按照声明顺序先b后a的,所以b(a)先被初始化为随机值,随后a才被初始化为1;
派生类构造析构顺序
-
派生类构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
-
类的构造,析构函数不能直接被继承,因此派生类构造函数总是 先 调用其直接基类构造函数 再 执行其他代码(其他代码遵循普通函数声明顺序进行构造);
(间接基类不能直接去调用,比如C继承B,B继承A,如果C(调用间接基类A)和B(调用直接基类A)的构造,那就是无意义的构造两次,浪费资源毫无益处)
#include<iostream>
using namespace std;
//多继承场景
class A//定义基类A
{
public:
A() { cout << "调用构造函数 A()" << endl; }
~A() { cout << "调用析构函数 ~A()" << endl; }
};
class B : public A
{
public:
B() { cout << "调用构造函数 B()" << endl; }
~B() { cout << "调用析构函数 ~B()" << endl; }
};
class C : public B
{
public:
C() { cout << "调用构造函数 C()" << endl; }
~C() { cout << "调用析构函数 ~C()" << endl; }
};
int main()
{
C c;
return 0;
}
运行结果:
解析
根据调试可以发现:
编译器为了初始化C,先调用了C的构造函数,紧接着C在定义其他变量前,有继承关系,则C的构造函数自动调用其直接基类B的构造函数,B在完成其构造函数前,有继承关系则B的构造函数又自动调用其直接基类A的构造函数;
那么最先完成构造函数的是最顶层A类,紧接着B类完成基类A的初始化,B类的构造也进一步完成,到了最底层C类,直接基类B和间接基类A(B也把他的构造调用了)都构造好了,然后才是C也完成了他的构造函数; (如果有其他变量,则在上述继承顺序构造的基础上,再加上遵循普通函数的声明顺序进行构造;)
析构顺序类似于压栈和弹栈,刚好相反;
扩展菱形多继承场景
//菱形 多继承场景
class A//定义基类A
{
public:
A() { cout << "调用构造函数 A()" << endl; }
~A() { cout << "调用析构函数 ~A()" << endl; }
};
class B : public A
{
public:
B() { cout << "调用构造函数 B()" << endl; }
~B() { cout << "调用析构函数 ~B()" << endl; }
};
class C : public A
{
public:
C() { cout << "调用构造函数 C()" << endl; }
~C() { cout << "调用析构函数 ~C()" << endl; }
};
class D : public B,public C
{
public:
D() { cout << "调用构造函数 D()" << endl; }
~D() { cout << "调用析构函数 ~D()" << endl; }
};
int main()
{
D d;
return 0;
}
那么执行结果是:
显然能看到,出现看了菱形继承 数据二义性的问题!
含虚基类的派生类构造析构顺序
class A//定义基类A
{
public:
A() {cout<<"调用A构造函数"<<endl;}
};
class B : virtual public A //A作为B的虚基类
{
public:
B() :A() {cout<<"调用B构造函数"<<endl;}
};
class C : public B //B作为C的直接基类
{
public:
C() :A() ,B(){cout<<"调用C构造函数"<<endl;}
};
int main()
{
C c;
return 0;
}
运行结果:
虽然运行结果类似,但通过调试可以看到,与上面普通的派生类调用构造规则不同!
这次编译器直接从C类的构造函数中,先执行了A类的构造函数再执行了B类的构造函数!显然,C不但执行了直接基类A的构造,也执行了间接基类B的构造;
解析
如果出现虚继承,某个类型含有虚基类时,最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对直接基类的虚基类初始化
C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类A的其他派生类(如类B)
对虚基类A的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
扩展菱形多继承场景(引入虚继承)
#include<iostream>
using namespace std;
//虚继承,菱形继承
class A//定义基类A
{
public:
A() { cout << "调用A构造函数" << endl; }
};
class B : virtual public A
{
public:
B() :A() { cout << "调用B构造函数" << endl; }
};
class C :virtual public A
{
public:
C() :A() { cout << "调用C构造函数" << endl; }
};
class D : public B,public C
{
public:
D() :A(),B(),C() { cout << "调用D构造函数" << endl; }
};
int main()
{
D d;
return 0;
}
(这不就是菱形继承解决了重复初始化造成的数据二义性的原理嘛~!)