虚基类

1、使用virtual修饰

基类是虚的时候静止信息通过中间类传递给基类

需要显示的调用所需的基类构造函数

1.为什么要引入虚基类?
在类的继承中,如果我们遇到这种情况:
“B和C同时继承A,而B和C都被D继承”
在此时,假如A中有一个函数fun()当然同时被B和C继承,而D按理说继承了B和C,同时也应该能调用fun()函数。这一调用就有问题了,到底是要调用B中的fun()函数还是调用C中的fun()函数呢?在C++中,有两种方法实现调用:
(注意:这两种方法效果是不同的)

使用作用域标识符来唯一表示它们比如:B::fun()
另一种方法是定义虚基类,使派生类中只保留一份拷贝。
作用域标识符表示
例子:

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
#include<iostream>
using namespace std;
class base{
public:
base(){a=5;cout<<"base="<<a<<endl;}
procted:
int a;
}**;**
class base1:public base{
public:
base1()
{a=a+10;cout<<"base1="<<a<<endl;}
};
class base2:public base{
public:
base2(){a=a+20;cout<<"base2="<<a<<endl;}
};
class derived:public base1, public base2{
public:
derived(){
cout<<"base1::a="<<base1::a<<endl;
cout<<"base2::a="base2::a<<endl;
}
};
int main()
{derived obj;
return 0;
}

这是第一种方法的典型例子。写的时候新手要注意几个易敲错的点:

1.多继承定义的时候是一个权限名对应一个基类,class derived:public base1, public base2 不能是class derived:public base1,base2

2.注意相邻两个基类的说明是用逗号分隔,不要再忘了。

3.老生常谈的问题吧,不要忘记类定义最后的那个分号!!!!!
(我自己真的老是忘记)

这段程序的调用顺序一定要学会熟练分析:

1.开始定义base1,而base1继承了base类,所以base1的定义又要回到base的定义,所以先执行base的构造函数base(){a=5;cout<<”base=”<<a<<endl;}这时显示第一条base a=5.
2.随后,调用base1的构造函数,显示base1 a=15 这时base1定义完毕。
3.开始调用base2,而base2同样继承了base类,所以base2的定义又要再次回到base的构造函数所以这时输出的是base a=5 。
4.随后再调用base2的构造函数,输出base2 a=25 。
5.最后在derived中分作用域调用a,虽然是同样名称的变量a,但在base1的作用域中表现为a=15,在base2作用域中表现为a=25。

所以这里最后的答案为:

1
2
3
4
5
6
base a=5
base1 a=15
base a=5
base2 a=25
base1::a=15
base2::a=25

实际上构造函数调用可以通过树状图来写,特别是对于多级继承关系,可以写出每一级里面继承的基类,而每一层最后一个树枝是该类的构造函数,而每一个基类又可以用同样的方法展开,直到分离到最后完全没有继承关系的基类为止。

虚基类的调用:

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
#include<iostream>
using namespace std;
class base{
public:
base(){
a=5;cout<<"base="<<a<<endl;
}
protected:
int a;
};
class base1:virtual public base{
public:
base1(){
a+=10;cout<<"base1="<<a<<endl;
}
};
class base2:virtual public base{
public:
base2(){a+=20;cout<<"base2="<<a<<endl;}
};
class derived:public base1,public base2{
public:
derived(){cout<<"derived a ="<<a<<endl; }
};

int main(){
derived obj;
return 0;
}

在定义了虚基类后,就等于告诉了系统,这里的a是base1和base2所共有的,对于调用base1和base2构造函数的修改都是针对同一个a而言(也就是基类和两个派生类所共有的)。而对于第一个例子中针对作用域的,相当于在继承时把a拷贝给了base1和base2,而彼此之间的a是无关联的。
这个过程最后为:
1.设定为虚基类后,系统知道base1和base2都是由base派生出的,所以它就统一先构造base,调用base的构造函数。
2.再按照顺序调用base1和base2的构造函数,只不过在此时,大家在构造时操作的都是同一个a。
所以在虚基类中,其构造顺序的思路是反着来的:

虚基类的另一种理解:虚基类的核心在于这个“虚”字,base1和base2本身作为虚基类相当于算是基类base的两个延伸(就相当于是base的一个外挂),而对于derived类来说,最本质的基类还是base,而基类base与虚基类base1和base2组成一个基类体系,或者一个基类生态,通过对这个生态中不同虚基类的继承,就可以形成不同的接口,生成不同的派生类。

虚基类的初始化:

(1)如果在虚基类中定义有带形参的构造函数,并且没有定义缺省形参的构造函数,则整个继承结构中,所有直接或者间接的派生类都必须在构造函数的成员初始化表中列出对虚基类构造函数的调用。

这句话是什么意思呢?我们改造上面的代码:

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
 #include<iostream>
using namespace std;
class base{
public:
base(int s){
a=s;cout<<"base="<<a<<endl;}

protected:
int a;
};//注意点1:base()构造函数里面有定义形参,所以此时下面的base1,base2
//虚基类的构造函数在定义时要列出对该基类构造函数的调用。

class base1:virtual public base{
public:
base1(int s,int h):base(s){a+=h;cout<<"base1="<<a<<endl;}
};//注意点2:虚基类base1的第一个括号内是**“总表**”也就是里面既要有输入上基
//类的构造函数的参数,又要包括自己独有的参数

class base2:virtual public base{
public:
base2(int s):base(s){a+=20;cout<<"base2="<<a<<endl;}
};
class derived:public base1,public base2{
public:
derived(int s,int h,int d):base(s),base1(s,h),base2(s){cout<<"derived a ="<<a+d<<endl; }//注意点3:此处也一样,前面的括号里是总表,不要忘记基类的形参int s。注
//意,此时base基类一定是先放第一个的,之后才是虚基类,而虚基类间顺序没有要求。
};
int main(){
derived obj(5,8,9);//注意点4:此处的填数顺序和derived的构造函数的参数顺序一样,相当于在derived的构造函数中,冒号前的括号在接收数据,冒号后是在将接收到的数据分配到各个构造函数。
return 0;
}

注意点1:基类构造函数里面有定义形参,所以此时下面的base1,base2虚基类的构造函数在定义时要列出对该基类构造函数的调用。

注意点2:虚基类base1的第一个括号内是“总表”也就是里面既要有输入上基类的构造函数的参数,又要包括自己独有的参数。

注意点3:此处也一样,前面的括号里是总表,不要忘记基类的形参int s。注意,此时基类构造函数一定是先放第一个的,之后才是虚基类,而虚基类间顺序没有要求。

注意点4:在主函数定义变量时的填数顺序和derived的构造函数的参数顺序一样,相当于在derived的构造函数中,冒号前的括号在接收数据,冒号后是在将接收到的数据分配到各个构造函数。

(2)如果一个虚基类派生出了多个派生类,那么决定虚基类成员的,是那个最远的派生类所调用的构造函数,而其他派生类调用的构造函数会被自动忽略。如果是同级的话(一样远),那就按照最后一个派生类调用的构造函数为准(比如图中以子类1.1.1.1.1的调用为准,因为最远)