C++中的继承(2)类的默认成员
在继承关系里面, 在派生类中如果没有显示定义这六个成员函数, 编译系统则会默认合成这六个默认的成员函数。
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&){}
总结:
- 是否可以将基类对象赋给派生类对象,答案是也许。如果派生类包含了转换构造函数,即对基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。
- 派生类对象可以赋给基类对象。
4、类的成员初始化列表
(1)类的成员变量总是在构造函数执行前创建完毕,但有此成员变量只能在初始化时赋值 – 如const型常量 和 引用;
(2)使用初始化表可以使指定构造函数中的参数或常量作为成员的初始值;
1 Derived::Derived(int i, int j): x(i), y(j) {}
(3)初始化表只能用于构造函数;
(4)必须使用初始化表来初始化const型常量和引用;
(5)成员初始化的顺序与它们出现在类声明中的位置有关,与初始化表中的顺序无关;
(6)C++11允许类内初始化,但初始化表会覆盖类内初始化:
1 class Derived { 2 private: 3 int x = 10; 4 int y = 20; 5 static const int num = 0; 6 };