C++中的继承(2)派生类的默认成员函数

时间:2022-09-08 00:13:22

在继承关系里面, 在派生类中如果没有显示定义这六个成员函数, 编译系统则会默认合成这六个默认的成员函数。

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 }

输出结果为:

C++中的继承(2)派生类的默认成员函数 

代码中,我们利用派生类Derived,创建了一个对象d,根据输出结果看到,貌似创建对象d的过程是:先调用基类的构造函数,再调用子类的构造函数;而析构对象时先调用子类的析构函数,再调用基类的析构函数。但是我们不能被表象所迷惑,我们转到反汇编来看看具体是怎么实现的:C++中的继承(2)派生类的默认成员函数

我们看到创建对象d的时候是先调用Derived类的构造函数D(),但是在cout << "D() " << endl;这句代码执行之前,编译器还做了一堆的其他工作,其中最重要的是  call        Base::Base (0E41041h)这条指令它跳转到了基类中,如图示:

C++中的继承(2)派生类的默认成员函数

屏幕上输出:

C++中的继承(2)派生类的默认成员函数

B() 、D()即先执行cout << "B() " << endl;语句,再执行cout << "D() " << endl;语句,所以,才有屏幕上的结果。在析构对象d的时候先调用~D()但是没那么简单,再看图中C++中的继承(2)派生类的默认成员函数

在语句cout << "~D() " << endl;执行完之后用掉call        Base::~Base (0E410D7h)指令跳转到~B()中 C++中的继承(2)派生类的默认成员函数

C++中的继承(2)派生类的默认成员函数

此时~D()输出,接着~B()输出,然后,~D()才算执行完。

C++中的继承(2)派生类的默认成员函数

  分析:基类是派生类的一部分,创建派生类对象时必须调用派生类构造函数,而派生类构造函数必须使用基类的构造函数。程序首先创建基类对象,所以基类对象在程序进入派生类构造函数之前被创建。实际上C++使用成员初始化列表语法来完成这项工作,即B()相当于在函数D()的初始化列表中被使用,如果不调用基类构造函数,程序将使用默认的基类构造函数。在执行完B()的函数体之后,继承的数据成员被初始化,执行D()函数体初始化新增的数据成员。析构对象时,先调用派生类的析构函数,执行完函数体析构完新增部分之后,使用基类的析构函数析构继承自基类的部分。所以才有上述现象。调用关系总结如下图:C++中的继承(2)派生类的默认成员函数

  总结:创建派生类对象时程序调用派生类构造函数,然后在初始化列表部分调用基类构造函数初始化继承的数据成员,而派生类构造函数主要初始化新增的数据成员。派生类总是调用一个基类构造函数。可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将先调用派生类析构函数,在函数体执行完之后调用基类析构函数。(可以看到,继承的数据成员生命周期长, 新增的数据成员生命周期短。)

构造函数带参情况

  构造派生对象时,派生类构造函数默认调用参数缺省的基类构造函数,若基类构造函数带有参数,则派生类中必须显式定义构造函数,并在初始化列表中传参。本例中,若B()带有参数,则D()中必须显式定义构造函数并传参;

C++中的继承(2)派生类的默认成员函数

如图,B()带有参数,则D()中构造函数无参时编译不能通过。

C++中的继承(2)派生类的默认成员函数

传参之后可以编译用过。

  当基类中显示定义构造函数,而派生类中没有定义构造函数,则使用默认合成的派生类构造函数,并在默认合成的派生类构造函数调用基类构造函数。即基类Base显式定义了构造函数,而派生类Derived中没有定义,则用默认合成的构造函数D()实例化对象,且在初始化参数列表部分调用基类构造函数B()。C++中的继承(2)派生类的默认成员函数

同理当派生类中显示定义构造函数,而基类中没有定义构造函数,则在派生类构造函数中调用默认合成的基类构造函数。

2、拷贝构造函数

使用拷贝构造函数的情况有:

  1. 将新的对象初始化为一个同类对象
  2. 按值将对象传递给函数
  3. 函数按值返回对象
  4. 编译器生成临时对象

如果程序没有显式定义拷贝构造函数,编译器将自动生成一个。当然,如果想在派生类中构造基类对象,那么不仅仅可以用构造函数,也可以用拷贝构造函数

 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 }

运行成功,输出结果为:

C++中的继承(2)派生类的默认成员函数

  这里我没有给基类定义拷贝构造函数,但是编译器自动给基类生成了一个拷贝构造函数,因为我基类中定义的没有指针成员,所以浅拷贝可以满足我的要求,但是如果在基类成员中有指针变量,必须要进行显式定义拷贝构造函数,即进行深拷贝。不然会造成同一块内存空间被析构两次的问题。

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);//左边的为基类对象

简而言之,可以将派生对象赋给基类对象,但这只涉及到基类的成员。如图示

 C++中的继承(2)派生类的默认成员函数

基类对象赋给派生类对象。

 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&){}
C++中的继承(2)派生类的默认成员函数
总结:
    • 是否可以将基类对象赋给派生类对象,答案是也许。如果派生类包含了转换构造函数,即对基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。
    • 派生类对象可以赋给基类对象。