C++继承中关于子类构造函数的写法
转载于:http://www.cnblogs.com/kaige/p/cplusplus_virtual_inheritance_derived_class_constructor.html
构造方法用来初始化类的对象,与父类的其它成员不同,它不能被子类继承(子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。
如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。
构造原则如下:
1. 如果子类没有定义构造方法,则调用父类的无参数的构造方法。
2. 如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。
3. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的默认无参构造函数。
4. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
5. 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。
6. 如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式,比如:
1 #include <iostream.h> 2 3 class animal 4 { 5 public: 6 animal(int height, int weight) 7 { 8 cout<<"animal construct"<<endl; 9 } 10 … 11 }; 12 13 class fish:public animal 14 { 15 public: 16 fish():animal(400,300) 17 { 18 cout<<"fish construct"<<endl; 19 } 20 … 21 }; 22 void main() 23 { 24 fish fh; 25 }
在fish类的构造函数后,加一个冒号(:),然后加上父类的带参数的构造函数。这样,在子类的构造函数被调用时,系统就会去调用父类的带参数的构造函数去构造对象。这种初始化方式,还常用来对类中的常量(const)成员进行初始化,如下面的代码所示:
1 class point 2 { 3 public: 4 point():x(0),y(0) 5 private: 6 const int x; 7 const int y; 8 };
当然,类中普通的成员变量也可以采取此种方式进行初始化,然而,这就没有必要了。
最近工作中某个软件功能出现了退化,追查下来发现是一个类的成员变量没有被正确的初始化。这个问题与C++存在虚继承的情况下派生类构造函数的写法有关。在此说明一下错误发生的原因,希望对更多的人有帮助。
我们代码中存在虚继承的类的继承结构与下图类似,并不是教科书中经典的菱形结构。从 Intermediate1 和 Intermediate3 到Base2 的继承是虚继承。Base1 和 Base2 包含一些成员变量,并提供了相应的构造函数接受指定的初始化值。Base2 还有一个缺省构造函数,把其成员变量都初始化为0。Intermediate1,2,3 也都提供了一个构造函数接受指定的初始化值,并在在初始化列表里调用Base1和Base2的构造函数完成初始化。
一位同事在做重构时,不小心把Final的代码改成了:
1 class Final : public Intermediate2, public Intermediate3 { 2 public: 3 Final (int a, int b, int c) 4 : Intermediate2(a, b, c), 5 Intermediate3(b, c) 6 { 7 8 } 9 10 };
1 class Intermediate1 : public Base1, virtual public Base2 { 2 public: 3 Intermediate1(int a, int b, int c) 4 : Base1(a), 5 Base2(b, c) 6 { 7 8 } 9 }; 10 11 class Intermediate2 : public Intermediate1 { 12 public: 13 Intermediate2(int a, int b, int c) 14 : Intermediate1(a, b, c), 15 Base2(b, c) 16 { 17 18 } 19 }; 20 21 class Intermediate3 : virtual public Base2 { 22 public: 23 Intermediate3(int b, int c) 24 : Base2(b, c) 25 { 26 27 } 28 };
看上去,Final的构造函数将调用Intermediate2 和 Intermediate3的构造函数分别将m_a, m_b 和 m_c初始化成指定的值。可是,运行时发现m_b和m_c的值是0!明显,这是调用了Base2的缺省构造函数。
原来,C++的规则是:如果在继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。我们可以显式调用虚基类的构造函数完成初始化。如果不显式调用虚基类的构造函数,则编译器会调用虚基类的缺省构造函数。如果不显式调用虚基类的构造函数,而虚基类没有定义缺省构造函数,则会出现编译错误。这条规则的原因是:如果不这样做,则虚基类部分会在存在的多个继承链条上被多次初始化。
很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们也要保证它们得到正确的初始化。
所以,如果我们要把m_b和m_c初始化成指定的值,Final的构造函数的正确写法应该是这样:
1 Final (int a, int b, int c) 2 : Base2(b, c), 3 Intermediate2(a, b, c), 4 Intermediate3(b, c) 5 { 6 7 }
完整的测试程序如下所示,有兴趣的同学可以自行编译运行一下。也可以在调试器中单步运行Final的构造函数,看看前后两种写法分别是调用了Base2的哪个构造函数。
1 #include "stdafx.h" 2 #include <iostream> 3 4 using namespace std; 5 6 class Base1 { 7 public: 8 Base1(int a): m_a(a) {} 9 10 protected: 11 int m_a; 12 }; 13 14 class Base2 { 15 public: 16 Base2(int b, int c): m_b(b), m_c(c) {} 17 Base2() : m_b(0), m_c(0) {} 18 19 protected: 20 int m_b; 21 int m_c; 22 }; 23 24 class Intermediate1 : public Base1, virtual public Base2 { 25 public: 26 Intermediate1(int a, int b, int c) 27 : Base1(a), 28 Base2(b, c) 29 { 30 31 } 32 }; 33 34 class Intermediate2 : public Intermediate1 { 35 public: 36 Intermediate2(int a, int b, int c) 37 : Intermediate1(a, b, c), 38 Base2(b, c) 39 { 40 41 } 42 }; 43 44 class Intermediate3 : virtual public Base2 { 45 public: 46 Intermediate3(int b, int c) 47 : Base2(b, c) 48 { 49 50 } 51 }; 52 53 class Final : public Intermediate2, public Intermediate3 { 54 public: 55 Final (int a, int b, int c) 56 : Base2(b, c), 57 Intermediate2(a, b, c), 58 Intermediate3(b, c) 59 { 60 61 } 62 63 void Print() { 64 cout<<m_a<<", "<<m_b<<", "<<m_c<<endl; 65 } 66 }; 67 68 69 int _tmain(int argc, _TCHAR* argv[]) 70 { 71 Final finalObj(1, 2, 3); 72 finalObj.Print(); 73 74 return 0; 75 }