派生类构造函数和多重继承的二义性问题
转载:https://blog.csdn.net/zhangchen1003/article/details/48242393
一、派生类构造函数的写法
(1)冒号前面是派生类构造函数的头部,这和我们以前介绍的构造函数的形式一样,但它的形参列表包括了初始化基类和派生类的成员变量所需的数据;冒号后面是对基类构造函数的调用,这和普通构造函数的参数初始化表非常类似。
(2)需要注意的是:冒号后面是对基类构造函数的调用,而不是声明,所以括号里的参数是实参
二、基类构造函数调用规则
(1)通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。也就是说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。
(2)如果基类有默认构造函数,那么在派生类构造函数中可以不指明,系统会默认调用;如果没有,那么必须要指明,否则系统不知道如何调用基类的构造函数。
三、构造函数的调用顺序
(1)当创建派生类对象时,先调用基类构造函数,再调用派生类构造函数。如果继承关系有好几层的话,例如:
A –> B –> C
那么则创建C类对象时,构造函数的执行顺序为:
A类构造函数 –> B类构造函数 –> C类构造函数
(2)构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
四、有子类对象的派生类
(1)即一个类中,如果有对象被定义为一个用户自定义型,那么就称该类含有子对象,比如
class person{
public:
person();
private:
int n;
class student:public person{
public:
student();
private:
person stu2;//称为含有子对象
int k;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
(2)初始化方法
派生类构造函数的任务应该包括3个部分:对基类数据成员初始化;对子对象数据成员初始化;对派生类数据成员初始化。比如:
student(int x,int y,int w,int h,int z):person(x,y),stu2(w,h),k(z){}
- 1
在上面的构造函数中有5个形参,前两个作为基类构造函数的参数,第3、第4个作为子对象构造函数的参数,第5个是用作派生类数据成员初始化的。基类构造函数和子对象的次序可以是任意的,如上面的派生类构造函数首部可以写成
student(int x,int y,int w,int h,int z):stu2(w,h),person(x,y),k(z){}
- 1
编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们的传递关系的。但是习惯上一般先写基类构造函数
(3)定义派生类构造函数的一般形式为:
派生类构造函数名(总参数表列): 基类构造函数名(参数表列), 子对象名(参数表列)
{
派生类中新增数成员据成员初始化语句
}
- 1
- 2
- 3
- 4
(4)执行派生类构造函数的顺序是:调用基类构造函数,对基类数据成员初始化;调用子对象构造函数,对子对象数据成员初始化;再执行派生类构造函数本身,对派生类数据成员初始化。
五、多层派生的构造函数
(1)多层派生定义
一个类不仅可以派生出一个派生类,派生类还可以继续派生,形成派生的层次结构;
(2)不要列出每一层派生类的构造函数,只需写出其上一层派生类(即它的直接基类)的构造函数即可;比如:
class Base{
public:
Base(int x):n(x){}
private:
int n;
};
class D1:public Base{
public:
D1(int x,int y):Base(x),m(y){}
private:
int m;
};
class D2:public D1{
piblic:
D2(int x,int y,int z):D1(x,y),k(z){}
private:
int k;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
那么写D2的构造函数,只需要写清楚其上层的构造函数即可D2(int x,int y,int z):D1(x,y),k(z){}
(3)执行顺序
在声明D2类对象时,调用D2构造函数;在执行D2构造函数时,先调用D1构造函数;在执行D1构造函数时,先调用基类Base构造函数。
初始化的顺序是:先初始化基类的数据成员n;再初始化D1的数据成员m;最后再初始化D2的数据成员k。
六、注意事项
(1)如果在基类中既定义无参的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数。(因为有默认的,可以不写明)
(2)如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在派生类构造函数中写出基类或子对象类型的构造函数及其参数表。
(3)如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数。
七、多继承的构造函数
(1)多继承声明形式
class D: public A, private B, protected C{
}
- 1
- 2
- 3
(2)多继承的构造函数声明形式
D类构造函数名(总参数表列): A构造函数(实参表列), B类构造函数(实参表列), C类构造函数(实参表列){
新增成员初始化语句
}
- 1
- 2
- 3
(3)派生类构造函数执行顺序
派生类构造函数的执行顺序同样为:先调用基类的构造函数,再调用派生类构造函数。基类构造函数的调用顺序是按照声明派生类时基类出现的顺序。(即: class D: public A, private B, protected C 中A、B、C出现的顺序)
(4)命名冲突
考虑下列代码:
class Base1{
public:
Base1(int x):n(x){}
void display(){
cout<<"Base1"<<endl;
}
private:
int n;
};
class Base2{
public:
Base2(int y):m(y){}
void display(){
cout<<"Base2"<<endl;
}
private:
int m;
};
class D:public Base1,public Base2{
public:
D(int x,int y,int z):Base1(x),Base2(y),k(z){}
private:
int k;
};
int main(){
D d(1,2,3);
d.display();//错误,不知道调用哪个
d.Base1::display();//正确,调用Base1中的display()
d.Base2::display();//正确,调用Base2中的diaplay()
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
从上面代码中可以知道,Base1和Base2中都有display();D的对象访问时必须加域名操作符;(注意:即使以private继承Base2,那么派生类外部,不能访问继承过来的display(),此时,若D d; d.display()还是会造成不能区分,必须加::指明哪个基类中的函数)
八、多重继承的二义性问题
(1)二义性问题的定义
由多次继承,导致成员同名而产生的二义性(ambiguous)问题
(2)二义性问题分为以几类
-
两个基类有同名成员
那么派生类继承这两个基类,派生类对象调用这个同名成员,编译器就不知道到底是调用哪个基类的函数,必须加域名操作符限定 -
两个基类和派生类三者都有同名成员
假设这三个类都有display()函数,那么通过派生类对象去调用display()函数,能不能通过编译?
此时,程序能通过编译,也可以正常运行。请问:执行时访问的是哪一个类中的成员?答案是:访问的是派生类中的成员。规则是:基类的同名成员在派生类中被屏蔽,成为“不可见”的,或者说,派生类新增加的同名成员覆盖了基类中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。请注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。
若要访问基类的同名成员函数,则要加域名操作符限定 -
类A和类B是从同一个基类派生的
类A和类B是从同一个基类Base派生的,然后类C又继承类A和类B;
class N
{
public:
int a;
void display(){ cout<<"A::a="<<a<<endl; }
};
class A: public N
{
public:
int al;
};
class B: public N
{
public:
int a2;
};
class C: public A, public B
{
public:
int a3;
void show(){ cout<<"a3="<<a3<<endl; }
}
int main()
{
C cl; //定义C类对象cl
// 其他代码
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
在类A和类B中虽然没有定义数据成员a和成员函数display,但是它们分别从类N继承了数据成员a和成员函数display,这样在类A和类B中同时存在着两个同名的数据成员a和成员函数display。在程序中可以通过类A和类B的构造函数去调用基类N的构造函数,分别对类A和类B的数据成员a初始化。怎样才能访问类A中从基类N继承下来的成员呢?显然不能用
cl.a = 3; cl.display();
或
cl.N::a = 3; cl. N::display();
因为这样依然无法区别是类A中从基类N继承下来的成员,还是类B中从基类N继承下来的成员。应当通过类N的直接派生类名来指出要访问的是类N的哪一个派生类中的基类成员。如
cl.A::a=3; cl.A::display(); //要访问的是类N的派生类A中的基类成员
(3)虚基类
类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自 A–>B–>D 这一路,另一份来自 A–>C–>D 这一条路;
为了解决命名冲突,C++定义了虚基类,声明虚基类只需要在继承方式前面加上 virtual 关键字
#include <iostream>
using namespace std;
class A{
protected:
int a;
public:
A(int a):a(a){}
};
class B: virtual public A{ //声明虚基类
protected:
int b;
public:
B(int a, int b):A(a),b(b){}
};
class C: virtual public A{ //声明虚基类
protected:
int c;
public:
C(int a, int c):A(a),c(c){}
};
class D: virtual public B, virtual public C{ //声明虚基类
private:
int d;
public:
D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}//**注意这里的构造函数和以往的不同**
void display();
};
void D::display(){
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
cout<<"c="<<c<<endl;
cout<<"d="<<d<<endl;
}
int main(){
(new D(1, 2, 3, 4)) -> display();
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
注意以下几点:
- 派生类D的构造函数,与以往的用法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
- C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
- 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。
- 只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承