多继承中的二义性问题
在一个表达式中,对函数或变量的引用必须是明确的,无二义性的。对于一个独立的类而言,其成员的标识是唯一的,对其访问不会有二义性问题。但是当类之间具有继承关系时,子类成员可能与父类成员重名;在多继承的情况下,多个父类之间也可能重名。一个引用了基类成员的表达式可能无法确认引用的是哪个基类成员,这时我们就说这个表达式具有二义性。
作用域分辨操作符与支配规则
/*作用域分辨操作符示例*/
#include <iostream>
using namespace std;
class A{
public:
void f(){
cout<<this<<"A类的f()调用完毕"<<endl;
}
};
class B{
public:
void f(){
cout<<this<<"B类的f()调用完毕"<<endl;
}
void g(){
cout<<this<<"B类的g()调用完毕"<<endl;
}
};
class C:public A,public B{
public:
void g(){
cout<<this<<"C类的g()调用完毕"<<endl;
}
void h(){
cout<<this<<"C类的h()调用完毕"<<endl;
}
};
int main(void)
{
C c;
//c.f(); 产生二义性__A,B类中均存在f()
c.A::f();
c.g();
c.B::g();
return 0;
}
分析:
- c.f() 该表达式具有二义性,因为类C中有两个f();分别是从A和B继承来的,他们的作用域不存在包含关系,因此编译器无法决定调用哪个。表达式c.A::f()则通过成员名限定消除了二义性。
- C中有两个g(),一个继承自B,一个是自己新增的,他们的作用域存在包含关系,自己新增的g()处于内层,会遮挡外层的同名成员(也叫支配规则,子类中的名字支配父类中的名字),因此表达式c.g()没有二义性。如果确实需要调用被遮挡的外层成员,依然可以通过成员名限定来进行解决,例如c.B::g()。
- 作用域分辨符的一般形式是: 类名::类成员标识符
虚继承与虚基类
子类继承父类便有了父类的特性,如果再继承一遍,显然有些 “荒谬”
在C++中:
class B:public A,public B
这样的定于是无法通过的,也就是说子类不可能直接多次继承同一个父类。但是重复继承还是有可能间接发生的,如下图所示:
因此,也就是说,在多继承时,当派生类的多个直接基类又是从另一个共同的基类派生而来时,这些直接基类都拥有上层共同基类的成员,并将导致下层的派生类成员发生重复。
间接二义性举例:
#include <iostream>
#include <string>
using namespace std;
class Person{
private:
string name;
public:
Person(){
name = "空白";
cout<<this<<"Person类的缺省样式的构造函数调用完毕"<<endl;
}
Person(string Name):name(Name){
cout<<this<<" Person类的带参构造函数调用完毕"<<endl;
}
~Person(){
cout<<this<<" Person类的析构函数调用完毕"<<endl;
}
string getName(){
return name;
}
};
class Doctor:public Person{
private:
string title;
public:
Doctor(){
title = "医师";
cout<<this<<" Doctor类的缺省样式构造函数调用完毕"<<endl;
}
Doctor(string Name,string Title):Person(Name),title(Title){
cout<<this<<" Doctor类的带参构造函数调用完毕"<<endl;
}
~Doctor(){
cout<<this<<" Doctor类的析构函数调用完毕"<<endl;
}
string getTitle(){
return title;
}
};
class Armyman:public Person{
private:
string militaryRank;
public:
Armyman(){
militaryRank = "上尉";
cout<<this<<"Armyman类的缺省样式构造函数调用完毕"<<endl;
}
Armyman(string Name,string MilitaryRank):Person(Name),militaryRank(MilitaryRank){
cout<<this<<"Armyman类的带参构造函数调用完毕"<<endl;
}
~Armyman(){
cout<<this<<"Armyman类的析构函数调用完毕"<<endl;
}
string getMilitaryRank(){
return militaryRank;
}
};
class ArmySurgeon:public Doctor,public Armyman{
private:
public:
ArmySurgeon(){
cout<<this<<"ArmySurgeon类的缺省样式构造函数调用完毕"<<endl;
}
ArmySurgeon(string Name1,string Title,string Name2,string MilitaryRank):Doctor(Name1,Title),Armyman(Name2,MilitaryRank){
cout<<this<<"ArmySurgeon类的带参构造函数调用完毕"<<endl;
}
~ArmySurgeon(){
cout<<this<<"ArmySurgeon类的析构函数调用完毕"<<endl;
}
void show(){
cout<<Doctor::getName()<<","<<getTitle()<<","<<Armyman::getName()<<","<<getMilitaryRank()<<endl;
}
};
int main(void)
{
cout<<"------------开始------------"<<endl;
ArmySurgeon as("张三","主治医师","李四","上校");
cout<<"as:";
as.show();
cout<<"----------准备结束---------"<<endl;
return 0;
}
运行结果如下:
在ArmySugeon这个类中,共同基类成员的重复导致我们无法直接使用getName()这个名字,则只能通过作用域分辨符来消除二义性。对于数据成员也一样,从不同途径继承来的Doctor::name和Armyman::name不仅浪费空间,还有可能导致数据不一致(同一名军医有张三和李四两个名字)。由于ArmySugeon类中并没有给name赋予新的含义,Doctor::name和Armyman::name的含义应该都是人的名字,如果能只存储一份,其标识和一致性问题都能够得到解决,为此,C++引入了虚继承和虚基类的概念。
虚基类的概念是伴随虚继承的定义过程产生的,虚继承的定义格式如下:
class 派生类名:virtual 继承方式 基类名
其中:
(1) virtue是关键字,声明继承方式为虚继承,其作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。为了便于表述,我们将该基类称为派生类的虚基类。
(2) 声明了虚基类之后,(2a)编译器确保在后继的进一步派生过程中只保存一份虚基类的成员,(2b)但需要虚基类与这些间接派生类共同维护这份虚基类成员。
(3) 同样为了便于表述,当后续的间接派生类要创建对象时,我们称之为最远派生类。注意这是一个相对的概念,哪个子类要创建对象,他就是最远派生类。
将上述代码稍作修改:在继承方式中加virtue关键字
class Doctor:virtual public Person
class Armyman:virtual public Person
去掉getName前的作用域分辨符:
void show(){
cout<<getName()<<","<<getTitle()<<","<<getName()<<","<<getMilitaryRank()<<endl;
}
运行结果如下:
从运行结果来看,声明了虚继承之后,在最远派生类(ArmySurgeon)中没有出现二义性和name的不一致,的确实现了(2a),这是virtual的第一层含义。但name并没有按照希望的值初始化,而是通过Person的缺省样式构造函数初始化为“空白”了,这说明带参构造函数调用链在通往虚基类的地方(也就是写virtual处)断裂了。这恰恰说明了virtual的另一层含义。
虚基类的成员的构造和析构
由前面的分析可知,虚基类的所有间接派生类在创建对象时(此时该类就是最远派生类),构造函数调用链在虚基类之前断裂,所以需要在最远派生类的构造函数中"越级"调用虚基类的构造函数。具体分为三种情况:
- 若虚基类没有构造函数,则系统自动补上虚基类的缺省构造函数。
- 若虚基类定义了缺省样式的构造函数,系统也自动补上虚基类的缺省样式的构造函数。
- 若虚基类定义了带参构造函数,虚基类所有间接派生类都要在其构造函数的初始化列表中显式列出虚基类的构造函数。这就是上面(2b)处所说的"共同维护"。
我们通过下面的例子来体会一下:
/*虚继承时的构造函数*/
#include <iostream>
#include <string>
using namespace std;
class Person{
private:
string name;
public:
Person(){
name = "空白";
cout<<this<<"Person类的缺省样式的构造函数调用完毕!"<<endl;
}
Person(string Name):name(Name){
cout<<this<<"Person类的带参构造函数调用完毕"<<endl;
}
~Person(){
cout<<this<<"Person类的析构函数调用完毕"<<endl;
}
string getName(){
return name;
}
};
class Doctor:virtual public Person{
private:
string title;
public:
Doctor(){
title = "医师";
cout<<this<<"Doctor类的缺省样式的构造函数调用完毕!"<<endl;
}
Doctor(string Name,string Title):Person(Name),title(Title){
cout<<this<<"带参构造函数调用完毕!"<<endl;
}
~Doctor(){
cout<<this<<"Doctor类的析构函数调用完毕"<<endl;
}
string getTitle(){
return title;
}
};
class Armyman:virtual public Person{
private:
string militaryRank;
public:
Armyman(){
militaryRank = "上尉";
cout<<this<<"Armyman类的缺省样式的构造函数调用完毕!"<<endl;
}
Armyman(string Name,string MilitaryRank):Person(Name),militaryRank(MilitaryRank){
cout<<this<<"Armyman类的带参样式构造函数调用完毕!"<<endl;
}
~Armyman(){
cout<<this<<"Armyman类的析构函数调用完毕!"<<endl;
}
string getMilitaryRank(){
return militaryRank;
}
};
class ArmySurgeon:public Doctor,public Armyman{
public:
ArmySurgeon(){
cout<<this<<"ArmySurgeon类的缺省样式的构造函数调用完毕"<<endl;
}
ArmySurgeon(string Name,string Title,string MilitaryRank):Doctor(Name,Title),Armyman(Name,MilitaryRank),Person(Name){
cout<<"ArmySurgeon类的带参构造函数调用完毕!"<<endl;
}
~ArmySurgeon(){
cout<<this<<"ArmySurgeon类的析构函数调用完毕!"<<endl;
}
void show(){
cout<<getName()<<getTitle()<<getMilitaryRank()<<endl;
}
};
int main(void)
{
cout<<"--------------开始--------------"<<endl;
ArmySurgeon as("张三","主治医生","上校");
cout<<"as:"<<endl;
as.show();
cout<<"--------------结束--------------"<<endl;
return 0;
}
运行结果如下:
通过在ArmySurgeon构造函数的初始化列表中加上Person(Name),就将断裂的构造函数链接上了。在此例中,虚基类只有一层间接派生类。当存在多层间接派生类时,每一层都进行这样的修补,会不会导致虚基类被多次初始化呢?我们通过下一个例子来进行说明!
/*虚基类的构造函数示例2*/
#include <iostream>
using namespace std;
class A{
private:
int a;
public:
A(int a){
this->a = a;
cout<<this<<"A类的带参构造函数调用完毕"<<endl;
}
~A(){
cout<<this<<"A类的析构函数调用完毕!"<<endl;
}
int getA(){
return a;
}
};
class B:public A{
private:
int b;
public:
B(int a,int b):A(a){
this->b = b;
cout<<this<<"B类的带参构造函数调用完毕!"<<endl;
}
~B(){
cout<<this<<"B类的析构函数调用完毕"<<endl;
}
int getB(){
return b;
}
};
class C{
private:
int c;
public:
C(int c){
this->c = c;
cout<<this<<"C类的构造函数调用完毕"<<endl;
}
~C(){
cout<<this<<"C类的析构函数调用完毕"<<endl;
}
int getC(){
return c;
}
};
class D:virtual public B{
private:
int d;
public:
D(int a,int b,int d):B(a,b){
this->d = d;
cout<<this<<"D类的带参构造函数调用完毕"<<endl;
}
~D(){
cout<<this<<"D类的析构函数调用完毕"<<endl;
}
int getD(){
return d;
}
};
class E:virtual public B{
private:
int e;
public:
E(int a,int b,int e):B(a,b){
this->e = e;
cout<<this<<"E类的带参构造函数调用完毕"<<endl;
}
~E(){
cout<<this<<"E类的析构函数调用完毕"<<endl;
}
int getE(){
return e;
}
};
class F:public D{
private:
int f;
public:
F(int a,int b,int d,int f):D(a,b,d),B(a,b){
this->f = f;
cout<<this<<"F类的带参构造函数调用完毕"<<endl;
}
~F(){
cout<<this<<"F类的析构函数调用完毕"<<endl;
}
int getF(){
return f;
}
};
class G:public C,public F,public E{
private:
int g;
public:
G(int a,int b,int c,int d,int e,int f,int g):C(c),F(a,b,d,f),E(a,b,e),B(a,b){
this->g = g;
cout<<this<<"G类的带参构造函数调用完毕"<<endl;
}
~G(){
cout<<this<<"G类的析构函数调用完毕"<<endl;
}
int getG(){
return g;
}
};
void f1(){
cout<<"------------f1准备创建------------"<<endl;
F f1(1,2,4,6);
cout<<"f1:"<<f1.getA()<<','<<f1.getB()<<','<<f1.getD()<<','<<f1.getF()<<endl;
cout<<"------------f1准备销毁------------"<<endl;
}
void g1(){
cout<<"------------g1准备创建------------"<<endl;
G g1(1,2,3,4,5,6,7);
cout<<"g1:"<<g1.getA()<<','<<g1.getB()<<','<<g1.getC()<<','<<g1.getD()<<','<<g1.getE()<<','<<g1.getF()<<','<<g1.getG()<<','<<endl;
cout<<"------------g1准备销毁------------"<<endl;
}
int main(void){
f1();
g1();
return 0;
}
运行结果如下:
NOTICE:
C++规定:
*只有最远派生类(也就是当前要创建对象的类)的构造函数才真正调用虚基类的构造函数,而该派生类上层的其他非虚基类的构造函数中所列出的对虚基类构造函数的调用将被忽略,从而保证了对虚基类成员只进行一次初始化。
- 当初始化列表中同时存在对虚基类和非虚基类构造函数的调用时,虚基类的构造函数优先执行。
- 虚继承时,最远派生类对象的析构顺序与构造顺序正好相反,先析构最远派生类自身,最后析构虚基类及其上层基类。并且虚基类也只虚构一次。