在继承关系里面, 在派生类中如果没有显示定义这六个成员函数, 编译系统则会默认合成这六个默认的成员函数。
1、构造与析构函数的调用关系
调用关系先看一段代码:
1 class Base 2 { 3 public : 4 Base() 5 { 6 cout << "B() " << endl; 7 } 8 ~Base() 9 { 10 cout << "~B() " << endl; 11 } 12 private: 13 int _pri; 14 protected: 15 int _pro; 16 public: 17 int _pub; 18 }; 19 class Derived : public Base 20 { 21 public : 22 Derived() 23 { 24 cout << "D() " << endl; 25 } 26 ~Derived() 27 { 28 cout << "~D() " << endl; 29 } 30 private: 31 int _d_pri; 32 protected: 33 int _d_pro; 34 public: 35 int _d_pub; 36 }; 37 void Test() 38 { 39 Derived d; 40 } 41 int main() 42 { 43 Test(); 44 getchar(); 45 return 0; 46 }
输出结果为:
代码中,我们利用派生类Derived,创建了一个对象d,根据输出结果看到,貌似创建对象d的过程是:先调用基类的构造函数,再调用子类的构造函数;而析构对象时先调用子类的析构函数,再调用基类的析构函数。但是我们不能被表象所迷惑,我们转到反汇编来看看具体是怎么实现的:
我们看到创建对象d的时候是先调用Derived类的构造函数D(),但是在cout << "D() " << endl;这句代码执行之前,编译器还做了一堆的其他工作,其中最重要的是 call Base::Base (0E41041h)这条指令它跳转到了基类中,如图示:
屏幕上输出:
B() 、D()即先执行cout << "B() " << endl;语句,再执行cout << "D() " << endl;语句,所以,才有屏幕上的结果。在析构对象d的时候先调用~D()但是没那么简单,再看图中
在语句cout << "~D() " << endl;执行完之后用掉call Base::~Base (0E410D7h)指令跳转到~B()中
此时~D()输出,接着~B()输出,然后,~D()才算执行完。
分析:基类是派生类的一部分,创建派生类对象时必须调用派生类构造函数,而派生类构造函数必须使用基类的构造函数。程序首先创建基类对象,所以基类对象在程序进入派生类构造函数之前被创建。实际上C++使用成员初始化列表语法来完成这项工作,即B()相当于在函数D()的初始化列表中被使用,如果不调用基类构造函数,程序将使用默认的基类构造函数。在执行完B()的函数体之后,继承的数据成员被初始化,执行D()函数体初始化新增的数据成员。析构对象时,先调用派生类的析构函数,执行完函数体析构完新增部分之后,使用基类的析构函数析构继承自基类的部分。所以才有上述现象。调用关系总结如下图:
总结:创建派生类对象时程序调用派生类构造函数,然后在初始化列表部分调用基类构造函数初始化继承的数据成员,而派生类构造函数主要初始化新增的数据成员。派生类总是调用一个基类构造函数。可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将先调用派生类析构函数,在函数体执行完之后调用基类析构函数。(可以看到,继承的数据成员生命周期长, 新增的数据成员生命周期短。)
构造函数带参情况
构造派生对象时,派生类构造函数默认调用参数缺省的基类构造函数,若基类构造函数带有参数,则派生类中必须显式定义构造函数,并在初始化列表中传参。本例中,若B()带有参数,则D()中必须显式定义构造函数并传参;
如图,B()带有参数,则D()中构造函数无参时编译不能通过。
传参之后可以编译用过。
当基类中显示定义构造函数,而派生类中没有定义构造函数,则使用默认合成的派生类构造函数,并在默认合成的派生类构造函数调用基类构造函数。即基类Base显式定义了构造函数,而派生类Derived中没有定义,则用默认合成的构造函数D()实例化对象,且在初始化参数列表部分调用基类构造函数B()。
同理当派生类中显示定义构造函数,而基类中没有定义构造函数,则在派生类构造函数中调用默认合成的基类构造函数。
2、拷贝构造函数
使用拷贝构造函数的情况有:
- 将新的对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
如果程序没有显式定义拷贝构造函数,编译器将自动生成一个。当然,如果想在派生类中构造基类对象,那么不仅仅可以用构造函数,也可以用拷贝构造函数
1 class Base 2 { 3 public : 4 Base() 5 { 6 cout << "B() " << endl; 7 } 8 ~Base() 9 { 10 cout << "~B() " << endl; 11 } 12 private: 13 int _pri; 14 protected: 15 int _pro; 16 public: 17 int _pub; 18 }; 19 class Derived : public Base 20 { 21 public : 22 Derived() 23 { 24 cout << "D() " << endl; 25 } 26 Derived(const Derived &tp) 27 :Base(tp)//拷贝构造函数 28 { 29 cout << "Derive()" << endl; 30 } 31 ~Derived() 32 { 33 cout << "~D() " << endl; 34 } 35 private: 36 int _d_pri; 37 protected: 38 int _d_pro; 39 public: 40 int _pub; 41 }; 42 void Test() 43 { 44 Derived d; 45 Derived i(d); 46 } 47 int main() 48 { 49 Test(); 50 getchar(); 51 return 0; 52 }
运行成功,输出结果为:
这里我没有给基类定义拷贝构造函数,但是编译器自动给基类生成了一个拷贝构造函数,因为我基类中定义的没有指针成员,所以浅拷贝可以满足我的要求,但是如果在基类成员中有指针变量,必须要进行显式定义拷贝构造函数,即进行深拷贝。不然会造成同一块内存空间被析构两次的问题。
3、赋值操作符
1 class Base 2 {}; 3 int main() 4 { 5 Base a; 6 Base b = a;//初始化 7 Base c; 8 c = a;//赋值 9 }
默认的赋值操作符用于处理同类对象之间的赋值,赋值不是初始化,如果语句创建新的对象,则使用初始化,如果语句修改已有对象的值,则为赋值。 赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征与基类完全相同,但赋值操作符的特征随类而异,因为它包含一个类型为其所属类的形参。
如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值操作符。这个操作符的默认版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。
如果对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值,如果显示的为基类提供了赋值操作符,将使用该操作符。
注意:赋值运算和拷贝构造是不同的,赋值是赋值给一个已有对象,拷贝构造是构造一个全新的对象
将派生类对象赋给基类对象时:
1 class Base 2 { 3 public: 4 int data; 5 }; 6 class Derive:public Base 7 { 8 public: 9 int d; 10 }; 11 int main() 12 { 13 Base a; 14 Derive dd; 15 a = dd; 16 }
上面的a=dd;语句将使用谁的赋值操作符呢。 实际上,赋值语句将被转换成左边的对象调用的一个方法
a.operator=(dd);//左边的为基类对象
简而言之,可以将派生对象赋给基类对象,但这只涉及到基类的成员。如图示
基类对象赋给派生类对象。
1 class Base 2 { 3 public: 4 int data; 5 }; 6 class Derive:public Base 7 { 8 public: 9 int d; 10 }; 11 int main() 12 { 13 Base a; 14 Derive dd; 15 dd = a; 16 }
上述赋值语句将被转换为:
d.operator=(a); //Derive::operator=(const Derive&)
左边的对象为派生类对象,不过派生类引用不能自动引用基类对象,所以上述代码不能运行。或者运行出错。除非有函数Derive(const Base&){}
总结:
- 是否可以将基类对象赋给派生类对象,答案是也许。如果派生类包含了转换构造函数,即对基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。
- 派生类对象可以赋给基类对象。