继承专题

【1】继承及访问权限

(1)理论知识

<1> 基类与派生类。基类、父类、超类均是指被继承的类;派生类、子类是指继承于基类(父类、超类)的类。

<2> 在C++中使用冒号表示继承。如下代码片段:

1 class A : public B // 表示派生类A从基类B继承而来。
2 {
3 };

<3> 派生类包含基类的所有成员,且还包括自已特有的成员。派生类内部成员函数和派生类对象访问基类中的成员就像访问自已的成员一样,可以直接使用,不需加任何操作符。但是,派生类永远无法访问基类中的私有成员。

<4> 在C++中派生类可以同时从多个基类继承(Java 不允许这种多重继承),当继承多个基类时,使用逗号将基类隔开。

<5> 基类访问控制符:

1 class A : public B  //基类以公有方式被继承;
2 class A : private B  //基类以私有方式被继承;
3 class A : protected B //基类以受保护方式被继承;

如果没有(或忘记写)访问控制符呢?切记:默认为私有继承。

<6> protected 受保护的访问权限:使用protected 保护权限表明这个成员是受保护的,但在派生类中可以访问基类中的受保护成员。但是,派生类的对象就不能访问基类受保护的成员了。

<7> 如果基类以public公有方式被继承:那么基类的所有公有成员都会成为派生类的公有成员,受保护的基类成员成为派生类的受保护成员。

<8> 如果基类以private私有方式被继承:那么基类的所有公有成员都会成为派生类的私有成员,基类的受保护成员也成为派生类的私有成员。

<9> 如果基类以protected受保护方式被继承:那么基类的所有公有和受保护成员都会变成派生类的受保护成员。

<10> 不管基类以何种方式被继承,基类的私有成员,始终永久保有其私有性,派生的任何代子类都不能访问基类的私有成员。

(2)示例代码

<1> 三种访问控制符

 1 /*
 2  *三种访问控制符
 3  */
 4 class A
 5 {
 6 private:  //私有成员
 7     int a;
 8 protected:  //保护成员
 9     int b;
10 public:     //公有成员
11     int c;
12     A()
13     {
14         a = b = c = 1;
15     }
16 };

<2> 公有方式继承

 1 class B:public A
 2 {
 3 public:
 4     int d;
 5     B()
 6     {
 7 //      a = 2;   //error!派生类中不可以访问基类的私有成员
 8         b = 2;   //可以在派生类中访问基类的受保护成员。权限仍为受保护成员
 9         c = 2;   //可以在派生类中访问基类的公有成员。权限仍为公有成员
10     }
11 };

<3> 受保护方式继承

 1 class C : protected A
 2 {
 3 public:
 4     int e;
 5     C()
 6     {
 7 //    a = 3;        //error!派生类中不可以访问基类的私有成员
 8       b = c = e = 3;    //可以在派生类中访问基类的公有及受保护成员(权限都为受保护成员)
 9     }
10 };

<4> 私有方式继承

1 class D : private A
2 {
3 public:
4     D()
5     {
6         b = c = 4;   //基类中的公有和受保护成员均成了派生类的私有成员
7     }
8 };

<5> 验证受保护和私有的访问权限

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class A
 5 {
 6 private:
 7     int a;
 8 protected:
 9     int b;
10 public:
11     int c;
12     A()
13     {
14         a = b = c = 1;
15     }
16 };
17 
18 class B : public A
19 {
20 public:
21     int d;
22     B()
23     {
24 //    a = 2; //error!派生类中不可以访问基类的私有成员
25       b = 2; //可以在派生类中访问基类的受保护成员。权限仍为受保护成员
26       c = 2; //可以在派生类中访问基类的公有成员。权限仍为公有成员
27     }
28 };
29 
30 class C : protected A
31 {
32 public:
33     int e;
34     C()
35     {
36 //    a = 3;        //error!派生类中不可以访问基类的私有成员
37       b = c = e = 3;    //可以在派生类中访问基类的受保护以及公有成员。权限都为受保护成员
38     }
39 };
40 
41 class D : private A
42 {
43 public:
44     D()
45     {
46         b = c = 4;   //基类中的公有和受保护成员均成了派生类的私有成员
47     }
48 };
49 
50 class C1 : public C
51 {
52 public:
53     C1()
54     {
55         b = c = e = 4;  //类A中的成员b和c在类C中均为受保护成员,此时仍为C1的受保护成员
56     }
57 };
58 
59 class D1 : public D   //类A中的成员b和c在类D中均为私有成员,此时仍为D1的私有成员
60 {
61 public:
62     D1()
63     {
64 //      b = 5; //error!派生类中不可以访问基类的私有成员
65 //      c = 5; //error!派生类中不可以访问基类的私有成员
66     }
67 };
68 
69 void main()
70 {
71     A m1; B m2; C m3; D m4;
72 //  cout << m1.b << m2.b << m3.b; //不能用类对象访问类的受保护成员。只有在类的成员函数中才可以访问的
73     cout << m1.c;   //可以用类的对象访问类的公有成员
74     cout << m2.c;   //可以用类的对象访问类的公有成员
75 //  cout << m3.c;   //error!不能用类的对象访问类的受保护成员
76 //  cout << m4.c;   //error!不能用类的对象访问类的私有成员
77 }

【2】覆盖和隐藏基类成员(变量或函数)

<1> 基类的成员(变量或函数)被覆盖:

如果派生类覆盖了基类中的成员(变量或函数):

当用派生类对象调用此同名成员(变量或函数)时调用的是派生类中的版本;

当用基类对象调用此同名成员(变量或函数)时调用的是基类中的版本。

<2> 隐藏基类成员函数的情况:

如果在派生类中定义了一个与基类同名的函数,不管这个函数的参数列表是不是与基类中的函数相同,这个同名函数都会把基类中所有同名函数及所有重载版本隐藏掉,这时并不是在派生类中重载基类的同名成员函数,而是所谓的隐藏。比如:类A中有函数f(int i, int j) 和 f(int  i)两个版本,当从 A 派生出的类 B 中定义了基类的同名f()函数版本时,类中的f(int i) 和 f(int i, int j)就被隐藏掉了,也就是说由类 B 创建的对象比如为objB,不能直接访问类 A 中的f(int i) 和 f(int i, int j)版本,即就是用语句objB.f(2)时会发生错误。

关于<1> 和 <2>两点,请参见示例如下代码:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6     int a; // 注意:默认为私有成员变量
 7 protected:
 8     int b;
 9 public:
10     int c;
11     int d;
12     A()
13     {
14         a = b = c = d = 1;
15     }
16     void f(int i)
17     {
18         cout << "class A::f(int i)" << "[param Value:" << i << "]" << endl;
19     }
20 };
21 
22 class B : public A
23 {
24 public:
25     int d;     //覆盖基类中成员变量d
26     B()
27     {
28         b = c = d = 2; //这个d是指派生类B中的成员变量d,而不是基类中的成员变量d
29         A::d = 3;   //给基类中被覆盖的成员变量d赋值。注意类中访问被覆盖成员的方式(用双冒号,作用域解析运算符)
30     }
31     void f()  //在派生类中重定义基类中的同名函数。虽然参数列不同,但同样会隐藏基类中的同名函数
32     {
33         cout << "class B::f()" << endl;
34         A::f(100);//在函数中调用基类中被隐藏函数的方式,使用作用域解析运算符
35 //      f(1);   //错误,因为基类中的函数被派生类的同名函数隐藏了,在这里派生类不知道有一个带参数的函数f(int i)
36     }
37 };
38 
39 void main()
40 {
41     B m;
42     cout << m.d << endl;    //输出的是派生类中的成员变量d的值,注意派生类中覆盖了基类成员d
43     cout << m.A::d << endl; //输出基类中成员变量d的值,注意这是使用对象访问被覆盖的基类成员的方式
44     m.f();                      //调用派生类中不带参数的函数f()
45 //  m.f(2);                  //error!!因为基类中带一个参数的函数f被派生类的同名函数隐藏掉了,不能这样访问,需用作用域解析运算符来访问
46     m.A::f(200);                 //使用派生类对象访问基类中被隐藏函数的方式
47 }
48 
49 /* 执行输出:
50 2
51 3
52 class B::f()
53 class A::f(int i)[param Value:100]
54 class A::f(int i)[param Value:200]
55 */

<3> 怎样使用派生类的对象访问基类中被派生类 覆盖或隐藏 的函数或变量:

1:使用作用域运算符双冒号。在使用对象调用基类中的函数或变量时使用作用域运算符即语句m.A::f(2),这时就能访问基类中的函数或变量版本。

注意:访问基类中被派生类覆盖了的成员变量只能用这种方法。

2:使用using:该方法只适用于被隐藏或覆盖的基类函数,在派生类的类定义中使用语句using 把基类的名字包含进来,比如using A::f;就是将基类中的函数f()的所有重载版本包含进来,重载版本被包含到子类之后,这些重载的函数版本就相当于是子类的一部分,这时就可以用派生类的对象直接调用被派生类隐藏了的基类版本,比如m.f(2)。但是,使用这种语句还是没法调用基类在派生类中被覆盖了的基类的函数,比如m.f() 调用的是派生类中定义的函数f,要调用被覆盖的基类中的版本要使用语句m.A::f()才行。

<4> 在派生类的函数中调用基类中的成员变量和函数的方法:就是在函数中使用的被派生类覆盖的基类成员变量或函数前用作用域解析符加上基类的类名,即A::f()就是在派生类的函数中调用基类中被派生类覆盖了的函数f()的方法。

关于<3> 和 <4>两点,请参见示例如下代码:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6     int a;
 7 protected:
 8     int b;
 9 public:
10     int c;
11     int d;
12     void f()
13     {
14         cout << "class A::f()" << endl;
15     }
16     void f(int i)
17     {
18         cout << "class A::f(int i)" << "[param Value:" << i << "]" << endl;
19     }
20     A()
21     {
22         a = b = c = d = 1;
23     }
24 };
25 
26 class B : public A
27 {
28 public:
29     int d;   //覆盖基类中成员变量d
30     B()
31     {
32         b = c = d = 2;   //这个d是指类B中的d,而不是基类中的d
33         A::d = 3;    //给基类中被覆盖的成员d赋值。注意类中访问被覆盖成员的方式
34     }
35     using A::f; //使用语句using把类A中的函数f包含进来,以便可以直接访问被隐藏了的函数,注意函数f没有括号
36     void f()    //在子类中重定义基类中的同名函数,虽然参数列不同,但同样会隐藏基类中的同名函数
37     {
38         cout << "class B::f()" << endl;//在子类中覆盖基类中的同名函数。注意这里是覆盖,同时会隐藏基类中的其他同名重载函数
39         A::f(100);  //在函数中调用基类中的被隐藏同名函数的方法,使用作用域解析运算符
40         f(200);     //正确,因为使用了using语句
41         A ma;
42         ma.f();   //正确,在子类中创建的基类对象,可以直接用对象名调用基类中被子类覆盖或隐藏了的函数,因为这时不会出现二义性。
43         ma.f(300);  //正确,在子类中创建的基类对象,可以直接用对象名调用基类中被子类覆盖或隐藏了的函数,因为这时不会出现二义性。
44     }
45     void g()
46     {
47         cout << "this  class B::g()" << endl;
48         f();//正确,但该语句访问的是子类中的不带参数函数,虽然在类中使用了语句,但直接调用
49             //被子类覆盖了的基类函数时不能使用这种方法
50         A::f();//正确,调用被派生类覆盖了的基类中的函数,注意,虽然使用了但要访问被子类覆盖了的函数,只能这样访问。
51     }
52 };
53 
54 void main()
55 {
56     B m;
57     m.f();    //调用子类中不带参数的函数f()
58     m.A::f(400);//使用子类对象访问基类中被隐藏的函数的方法
59     m.f(500);   //调用基类重载的函数,注意这里可以不用运算符,因为在子类中使用了,只要子类没有覆盖基类中的方法,都可以这
60                 //样直接调用。
61     m.A::f(600);//当然,使用了后,也可以使用这种方法
62 
63     A k;
64     k.f();
65 }
66 /* 执行输出:
67 class B::f()
68 class A::f(int i)[param Value:100]
69 class A::f(int i)[param Value:200]
70 class A::f()
71 class A::f(int i)[param Value:300]
72 class A::f(int i)[param Value:400]
73 class A::f(int i)[param Value:500]
74 class A::f(int i)[param Value:600]
75 class A::f()
76 */

<5> 基类以私有方式被继承时,改变基类中的公有成员为派生类中公有的方式:

[1] 使用::作用域运算符。不提倡用这种方法,在派生类的public权限后面用作用域运算符把基类的公有成员包含进来,这样基类的成员就会成为派生类中的公有成员了。注意一点,如果是函数的话后面不能加括号:如 A::f; 如果 f 是函数名称,不能有括号。

[2] 使用using 语句,现在一般用这种方法,也是在派生类的public权限后使用using 把基类成员包含进来,如using A::f

关于<5>内容,请参见如下示例代码:

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a, b;
 8     void f()
 9     {
10         cout << "class A::f()" << endl;
11     }
12     void g()
13     {
14         cout << "class A::g()" << endl;
15     }
16 };
17 
18 class B : private A
19 {
20 public:
21     A::f;
22     A::a;  //使用作用域运算符使基类中的成员成为公有
23     using A::g;  //使用语句使基类中的成员函数成为派生类中的公有成员,注意函数名后不能有括号。
24 };
25 
26 void main()
27 {
28     B m;
29 //  m.b = 1;//错误。因为类B是以私有方式继承的,基类A中的成员变量b在派生类B中是私有的,不能通过对象访问私有成员。
30     m.f();
31     m.g();
32     m.a = 1;
33 }
34 
35 /* 执行输出:
36 class A::f()
37 class A::g()
38 */

【3】继承时的构造函数和析构函数问题

(1)理论知识

<1> 在继承中,基类的构造函数构建对象的基类部分,派生类的构造函数构建对象的派生类部分。

<2> 当创建派生类对象时,先用派生类的构造函数调用基类的构造函数构建基类,然后再执行派生类构造函数构造出完整的派生类对象。

即先构造基类再构造派生类的顺序。执行析构函数的顺序与此相反。

<3> 调用基类带参数的构造函数的方法:

在派生类的构造函数中使用初始化列表的形式就可以调用基类带参数的构造函数初始化基类成员。如下类B 是类A 的派生类:

 1 class  A
 2 {
 3     int m_nA;
 4 public:
 5      A(int m = 100) : m_nA(m)
 6      {}
 7 };
 8 
 9 class B : public A
10 {
11     int m_nB;
12 public:
13     B(int n = 200) : A(n), m_nB(n)  // 类B是类A的派生类
14     {}
15 };

<4> 派生类的构造函数调用基类的构造函数的方法为:

1:如果派生类没有显式用初始化列表调用基类的构造函数时,这时就会用派生类的构造函数调用基类的默认构造函数,构造完基类后,才会执行派生类的构造函数函数体,以保证先执行基类构造函数再执行派生类构造函数的顺序,如果基类没有默认构造函数就会出错。

2:如果派生类用显式的初始化列表调用基类的构造函数时,这时就会检测派生类的初始化列表。当检测到显式调用基类的构造函数时,就调用基类的构造函数构造基类,然后再构造派生类,以保证先执行基类构造函数再执行派生类构造函数的顺序。如果基类没有定义派生类构造函数初始化列表所调用的构造函数版本就会出错。

<5> 如果在基类中没有定义默认构造函数,但定义了其他构造函数版本,这时派生类中定义了几个构造函数的不同版本,只要派生类有一个构造函数没有显式调用基类中定义的构造函数版本就会发生错误。因为编译器会首先检查派生类构造函数调用基类构造函数的匹配情况,如果发现不匹配就会出错。即使没有创建任何类的对象都会出错,而不管这个派生类的对象有没有调用派生类的这个构造函数。比如:基类有一个构造函数版本A(int i)而没有定义默认构造函数,派生类B有这几个版本的构造函数B() : A(4){},B(int i) : A(5){},再有语句B(int i, int j){} 没有显式调用基类定义的构造函数而是调用基类的默认构造函数,如果创建了B m 和语句B m(1)时都会提示没有可用的基类默认构造函数的错误。虽然这时类B 的对象m 没有调用派生类B 的带有两个形参的构造函数,但同样会出错。

<6> 同样的道理,如果基类中定义了默认构造函数,却没有其他版本的构造函数,而这时派生类却显式调用了基类构造函数的其他版本,这时就会出错。不管你有没有创建类的对象,因为编译器会先在创建对象前就检查构造函数的匹配问题。

关于<4>、<5>、<6>三点,请参见如下示例代码:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a, b;
 8     A(int i)
 9     {
10         a = b = 1;
11         cout << "Construction  A1" << endl;
12     }
13     A(const A & J)
14     {
15         a = b = 2;
16         cout << "Copy Construction" << endl;
17     }
18     ~A()
19     {
20         cout << "Destroy  A" << endl;
21     }
22 };
23 
24 class B : public A
25 {
26 public:
27     int c, d;
28 //  B()  //error!!此语句将要调用基类默认的构造函数,而基类不存在那样的函数
29 //  {
30 //      c = d = 3;
31 //      cout << "Constrution  B" << endl;
32 //  }
33 //  B(int i, int j) //error!!此语句将要调用基类默认的构造函数,而基类不存在那样的函数
34 //  {
35 //      c = d = 4;
36 //  }
37     B(int i) : A(2)   //显式调用基类带一个形参的构造函数。注意语法
38     {
39         c = d = 5;
40         cout << "Constrution  B1" << endl;
41     }
42     ~B()
43     {
44         cout << "Destroy  B" << endl;
45     }
46 };
47 
48 int main()
49 {
50     B m(0);
51     cout << "m.a = " << m.a << " m.b = " << m.b << endl;
52     B m1(m); //调用基类的拷贝构造函数,调用派生类的默认拷贝构造函数
53     cout << "m1.a = " << m1.a << " m1.b = " << m1.b << endl;
54 }
55 
56 /*
57 Construction  A1
58 Constrution  B1
59 m.a = 1 m.b = 1
60 Copy Construction
61 m1.a = 2 m1.b = 2
62 Destroy  B
63 Destroy  A
64 Destroy  B
65 Destroy  A
66 */

<7> 派生类只能初始化它的直接基类。比如类C 是类B 的子类,而类B 又是类A 的子类,如下示例代码:

 1 class  A
 2 {
 3     int m_nA;
 4 public:
 5      A(int m = 100) : m_nA(m)
 6      {}
 7 };
 8 
 9 class B : public A
10 {
11     int m_nB;
12 public:
13     B(int n = 200) : A(n), m_nB(n)  // 类B是类A的派生类
14     {}
15 };
16 
17 class C : public B
18 {
19     int m_nC;
20 public:
21     C(int k = 300) : A(k), m_nC(k)  // 非法成员初始化:“A”不是基或成员
22     {}
23 };

将会出错:“非法成员初始化”。该语句试图显式调用类B 的基类类A 的构造函数,这时会出现类A不是类C的基类的错误。

<8> 继承中的拷贝构造函数和构造函数一样,基类的拷贝构造函数复制基类部分,派生类的拷贝构造函数复制派生类部分。

<9> 派生类拷贝构造函数调用基类拷贝构造函数的方法为:A(const A & m) : B(m){ } 其中B 是基类,A 是派生类。

<10> 如果在派生类中定义了拷贝构造函数而没有用初始化列表显式调用基类的拷贝构造函数,这时不管基类是否定义了拷贝构造函数,若出现派生类对象的复制初始化情况时就将调用基类中的默认构造函数初始化基类的成员变量。注意是默认构造函数不是默认拷贝构造函数,如果基类没有默认构造函数就会出错。也就是说,派生类的拷贝构造函数的默认隐藏形式是B(const B & j) : A(){}这里B 是A 的派生类;也就是说,如果不显式用初始化列表形式调用基类的拷贝构造函数时,默认情况下是用初始化列表的形式调用的是基类的默认构造函数。

<11> 当在派生类中定义了拷贝构造函数且显式调用基类的拷贝构造函数,而基类却没有定义拷贝构造函数时,若出现派生类对象复制初始化情况就将调用基类中的默认拷贝构造函数初始化基类部分,调用派生类的拷贝构造函数初始化派生类部分。因为拷贝构造函数只有一种形式,即A(const A & m){},比如当出现调用时A(const A & m) : B(m){} 如果这时基类B 没有定义拷贝构造函数,则该语句将会调用派生类A 的默认拷贝构造函数。

<12> 如果基类定义了拷贝构造函数,而派生类没有定义时,则会调用基类的拷贝构造函数初始化基类部分,调用派生类的默认拷贝构造函数初始化派生类部分。

关于拷贝构造函数,示例代码如下:

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a, b;
 8     A()
 9     {
10         a = b = 1;
11         cout << "Construction  A" << endl;
12     }
13     A(int i)
14     {
15         a = b = i;
16         cout << "Construction  A1" << endl;
17     }
18     A(const A & obj)
19     {
20         a = obj.a;
21         b = obj.b;
22         cout << "Copy Construction" << endl;
23     }
24     ~A()
25     {
26         cout << "Destroy  A" << endl;
27     }
28 };
29 
30 class B : public A
31 {
32 public:
33     int c, d;
34     B()  //此语句意味将要调用基类默认的构造函数A()
35     {
36        c = d = 4;
37        cout << "Constrution  B" << endl;
38     }
39     B(int i, int j) //此语句意味将要调用基类默认的构造函数A()
40     {
41        c = i;
42        d = j;
43     }
44     B(int i) : A(i) //显式调用基类带一个形参的构造函数。注意语法
45     {
46         c = d = i;
47         cout << "Constrution  B1" << endl;
48     }
49     B(const B & obj) //这里没有显式调用基类的拷贝构造函数,用默认的拷贝构造函数形式调用基类中的默认构造函数
50     {
51         c = obj.c;
52         d = obj.d;
53         cout << "Copy Construction B" << endl;
54     }
55     ~B()
56     {
57         cout << "Destroy  B" << endl;
58     }
59 };
60 
61 void main()
62 {
63     B m(0);
64     cout << "m.a = " << m.a << " m.b = " << m.b << endl;
65     cout << "m.c = " << m.c << " m.d = " << m.d << endl;
66     B m1(m); //先调用基类的默认构造函数将A初始化,再调用B类的拷贝构造函数对B类进行初始化
67     cout << "m1.a = " << m1.a << " m1.b = " << m1.b << endl;
68     cout << "m1.c = " << m1.c << " m1.d = " << m1.d << endl;
69     B m2(10, 20);
70     cout << "m2.a = " << m2.a << " m2.b = " << m2.b << endl;
71     cout << "m2.c = " << m2.c << " m2.d = " << m2.d << endl;
72 }
73 
74 /*
75 Construction  A1
76 Constrution  B1
77 m.a = 0 m.b = 0
78 m.c = 0 m.d = 0
79 Construction  A
80 Copy Construction B
81 m1.a = 1 m1.b = 1
82 m1.c = 0 m1.d = 0
83 Construction  A
84 m2.a = 1 m2.b = 1
85 m2.c = 10 m2.d = 20
86 Destroy  B
87 Destroy  A
88 Destroy  B
89 Destroy  A
90 Destroy  B
91 Destroy  A
92 */

【4】多重继承与虚基类

(1)理论知识

<1> C++允许一个派生类从多个基类继承,这种继承方式称为多重继承。当从多个基类继承时每个基类之间用逗号隔开,比如class A : public B, public C { }就表示派生类A从基类B 和C 继承而来。

<2> 多重继承的构造函数和析构函数:多重继承中初始化的次序是按继承的次序来调用构造函数的而不是按初始化列表的次序,比如有class A : public B, public C{ }那么在定义类A 的对象A m 时将首先由类A 的构造函数调用类B 的构造函数初始化B,然后再调用类C 的构造函数初始化C,最后再初始化对象A,这与在类A 中的初始化列表次序无关。

<3> 多重继承中的二义性问题:

1:成员名重复:

比如类A 从类B 和C 继承而来,而类B 和C 中都包含有一个名字为f 的成员函数,这时派生类A 创建一个对象,比如A m; 语句m.f()将调用类B 中的f 函数呢还是类C 中的f 函数呢?

2:多个基类副本:

比如类C 和B 都从类D 继承而来,这时class A : public B, public C { } 类A 从类C 和类B 同时继承而来,这时类A 中就有两个类D 的副本,一个是由类B 继承而来的,一个是由类C 继承而来的,当类A 的对象比如A m;要访问类D 中的成员函数f 时,语句m.f()就会出现二义性,这个f 函数是调用的类B 继承来的 f 还是访问类C 继承来的函数f 呢。

3:在第2 种情况下还有种情况,语句class A : public B, public C { },因为类A 首先使用类B 的构造函数调用共同基类D 的构造函数构造第一个类D的副本,然后再使用类C 的构造函数调用共同基类D的构造函数构造第二个类D的副本。类A的对象m 总共有两个共享基类D的副本,这时如果类D中有一个公共成员变量d,则语句m.B::d 和m.D::d 都是访问的同一变量,类B 和类D 都共享同一个副本,既如果有语句m.D::d=3 则m.B::d 也将是3。这时m.C::d 的值不受影响而是原来的值。为什么会这样呢?因为类A 的对象m 总共只有两个类D 的副本,所以类A 的对象m 就会从A 继承来的两个直接基类B 和C 中,把从共同基类D 中最先构造的第一个副本作为类A 的副本,即类B 构造的D 的副本。因为class A:public B,public C{}最先使用B 的构造函数调用共同基类类D 创造D 的第一个副本,所以类B 和类D 共享同一个副本。

二义性问题示例代码如下:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a;
 8     A(int i)
 9     {
10         a = i;
11         cout << "A" << endl;
12     }
13 };
14 
15 class B : public A
16 {
17 public:
18     int b;
19     B() : A(4)
20     {
21         cout << "B" << endl;
22     }
23 };
24 
25 class C : public A
26 {
27 public:
28     int c;
29     C() : A(5)
30     {
31         cout << "C" << endl;
32     }
33 };
34 
35 class D : public B, public C
36 {
37 public:
38     int d;
39     D() : C(), B()
40     {
41         cout << "D" << endl;
42     }
43 };
44 
45 int main()
46 {
47     D m;         //ABACD,注意这里的构造顺序
48 //  m.a = 1;      //error!!a成员出现二义性
49     m.B::a = 1;
50     cout << "m.B::a = " << m.B::a << endl; //1
51     m.A::a = 3;
52     cout << "m.B::a = " << m.B::a << " m.A::a = " << m.A::a << endl;//33
53     m.C::a = 2;
54     cout << "m.C::a = " << m.C::a << endl;
55 }
56 
57 /* 执行结果:
58 A
59 B
60 A
61 C
62 D
63 m.B::a = 1
64 m.B::a = 3 m.A::a = 3
65 m.C::a = 2
66 */

4:解决方法:对于第1 和第2 种情况都可以使用作用域运算符“冒号”来限定要访问的类名来解决二义性。

但对于第二种情况一般不允许出现两个基类的副本,这时可以使用虚基类来解决这个问题,一旦定义了虚基类,就只会有一个基类的副本。

<4> 虚基类:

方法是使用virtual 关键字,比如class B : public virtual D { },class C : virtual public D { } 注意关键字virtual 的次序无关紧要。类B 和类C 以虚基类的方式从类D 继承,这样的话从类B 和类C 同时继承的类时就会只创建一个类 D的副本,比如class A:public B, public C{ } 这时类A 的对象就只会有一个类D 的副本,类A 类B 类C 类D 四个类 都共享一个类D 的副本,比如类D 有一个公有成员变量d,则m.d 和m.A::d,m.B::d,m.C::d,m.D::d 都将访问的是同一个变量。这样类A 的对象调用类D 中的成员时就不会出现二义性了。

<5> 虚基类的构造函数:

比如class B : public virtual D{};

class C : virtual public D{};

class A : public B, public C{};

这时当创建类A 的对象A m 时初始化虚基类D 将会使用类A 的构造函数直接调用虚基类的构造函数初始化虚基类部分,而不会使用类B 或者类C 的构造函数调用虚基类的构造函数初始化虚基类部分,这样就保证了只有一个虚基类的副本。 但是当创建一个类B 和类C 的对象时仍然会使用类B 和类C 中的构造函数调用虚基类的构造函数初始化虚基类。

虚基类及其构造函数示例代码:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a;
 8     A()
 9     {
10         a = 10;
11         cout << "Default Construction" << endl;
12     }
13     A(int i)
14     {
15         a = i;
16         cout << "A" << endl;
17     }
18 };
19 
20 class B : public virtual A   //表示以虚基类的方式继承
21 {
22 public:
23     int b;
24     B(int i) : A(4)
25     {
26         cout << "B" << endl;
27     }
28 };
29 
30 class C : public virtual A //表示以虚基类的方式继承
31 {
32 public:
33     int c;
34     C() : A(5)
35     {
36         cout << "C" << endl;
37     }
38 };
39 
40 class D : public B, public C
41 {
42 public:
43     int d;
44     D() : A(6), C(), B(2)
45     {
46         cout << "D" << endl;
47     }
48 };
49 
50 class E : public B, public C
51 {
52 public:
53     int e;
54     E() : C(), B(2)
55     {
56         cout << "E" << endl;
57     }
58 };
59 
60 //因为类A是虚基类,所以类D会直接调用虚基类的构造函数构造虚基类部分,
61 //而不会使用类B或者类C的构造函数来调用虚基类的构造函数初始化虚基类
62 //部分。要调用虚基类中的带参数的构造函数必须在这里显式调用,如果不
63 //显式调用就将调用虚基类的默认构造函数。
64 
65 void main()
66 {
67     D m;  //ABCD, 注意没有重复的虚基类副本A
68     cout << m.a << endl; //4
69     m.B::a = 1;
70     cout << m.a << ";" << m.A::a << ";" << m.B::a << ";" << m.C::a << endl; //四个类公用一个虚基类的副本,不存在二义性
71     E m1;
72     cout << m1.a << ";" << m1.A::a << ";" << m1.B::a << ";" << m1.C::a; //四个类公用一个虚基类的副本,不存在二义性
73 }
74 
75 /*
76 A
77 B
78 C
79 D
80 6
81 1;1;1;1
82 Default Construction
83 B
84 C
85 E
86 10;10;10;10
87 */

希望有所收获,仅此而已。

 

作者:kaizen
声明:本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此声明,且在文章明显位置给出本文链接,否则保留追究法律责任的权利。
签名:顺序 选择 循环
原文地址:https://www.cnblogs.com/Braveliu/p/2843916.html