在面向对象中,常常存在这样的事情,一个派生类它有两个或两个以上的基类,这种行为称作多重继承,示意图如下:
如果在多重继承中Class A 和Class B存在同名数据成员,则对Class C而言这个同名的数据成员容易产生二义性问题。这里的二义性是指无法直接通过变量名进行读取,需要通过域(::)成员运算符进行区分。例如:
//基类A
class A
{
public:
A() :m_data(1), m_a(1)
{
}
~A(){}
public:
int m_data; //同名变量,类型无要求
int m_a;
};
//基类B
class B
{
public:
B() :m_data(1), m_b(1)
{
}
~B(){}
public:
int m_data; //同名变量,类型无要求
int m_b;
};
class C : public A, public B
{
};
int _tmain(int argc, _TCHAR* argv[])
{
C Data;
//Data.m_data = 10; //错误, 提示指向不明确
//通过域成员运算符才可以访问,使用不方便
Data.A::m_data = 10.1;
Data.B::m_data = 20;
std::cout << Data.A::m_data << " " << Data.B::m_data << std::endl;
return 0;
}
分析
我们可以通过域(::)运算符对同名变量进行读写操作,但是不是很方便,不能直接通过.变量的形式进行操作,形如Data.m_data
。
在多重继承中,存在一个很特殊的继承方式,即菱形继承。比如一个类C通过继承类A和类B,但是类A和类B又同时继承于公共基类N。示意图如下:
这种继承方式也存在数据的二义性,这里的二义性是由于他们间接都有相同的基类导致的。 这种菱形继承除了带来二义性之外,还会浪费内存空间。
代码如下:
//公共基类
class N
{
public:
N(int data1, int data2, int data3) :
m_data1(data1),
m_data2(data2),
m_data3(data3)
{
std::cout << "call common constructor" << std::endl;
}
virtual ~N(){}
void display()
{
std::cout << m_data1 << std::endl;
}
public :
int m_data1;
int m_data2;
int m_data3;
};
class A : /*virtual*/ public N
{
public:
A() :N(11, 12, 13), m_a(1)
{
std::cout << "call class A constructor" << std::endl;
}
~A(){}
public :
int m_a;
};
class B : /*virtual*/ public N
{
public:
B() :N(21, 22, 23),m_b(2)
{
std::cout << "call class B constructor" << std::endl;
}
~B(){}
public :
int m_b;
};
class C : public A , public B
{
public:
//负责对基类的初始化
C() : A(), B(),
m_c(3)
{
std::cout << "call class C constructor" << std::endl;
}
void show()
{
std::cout << "m_c=" << m_c << std::endl;
}
public :
int m_c;
};
我们通过vs自带的内存分析模型工具,得到如下的内存分布模型:
我们发现在类C中存在 两份的基类N,分别存在类A和类B中,如果数据多则严重浪费空间,也不利于维护, 我们引用基类N中的数据还需要通过域运算符进行区分。例如:
C data;
data.A::m_data1 = 10;
data.B::m_data1 = 10;
为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是 在间接继承共同基类时只保留一份基类成员,虚基类的声明如下:
class A//A 基类
{ ... };
//类B是类A的公用派生类, 类A是类B的虚基类
class B : virtual public A
{ ... };
//类C是类A的公用派生类, 类A是类C的虚基类
class C : virtual public A
{ ... };
虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式声明的。
虚继承是声明类时的一种继承方式,在继承属性前面添加virtual关键字。
//此时基类A并不是虚基类,因为声明继承类D时没有指定虚继承方式
class D : public N
{
...
};
虚基类的初始化
这里直接说明结论,对于虚基类的初始化是由最后的派生类中负责初始化。
在最后的派生类中不仅要对直接基类进行初始化,还要负责对虚基类初始化。
例如:
// 类A和类B是虚继承方式
class C : public A, public B
{
public:
//负责对直接基类的初始化 以及虚基类的初始化
C() : A(), B(), N(31,32,33) ,
m_c(3)
{
std::cout << "call class C constructor" << std::endl;
}
void show()
{
std::cout << "m_c=" << m_c << std::endl;
}
public:
int m_c;
};
虚基类构造次数
C++编译系统只执行最后的派生类对基类的构造函数调用,而忽略其他派生类对虚基类的构造函数调用。从而避免对基类数据成员重复初始化。因此,虚基类只会构造一次。
若不是普通的继承,则存在重复构造,左边普通继承,右边虚继承,看下图:
有虚基类的派生类中,虚基类只有一个构造函数输出"call common constructor
虚基类的内存分析
我们通过内存模型分析工具,得到最后派生类的内存模型对比图:
右边是菱形继承的内存分析,左图是有虚基类的派生类C的内存分析:
我们发现虚继承的派生类C的内存比非虚继承的派生类C内存要小8个字节,并且虚基类在派生类中只存在一份,虚基类有virtual关键字标识。
因此我们在派生类中引用基类N的数据,不需要使用域成员运算符,因为只有一个基类对象。例如:
C data;
data.m_data1 = 10;
//公共基类
class N
{
public:
N(int data1, int data2, int data3) :
m_data1(data1),
m_data2(data2),
m_data3(data3)
{
std::cout << "call common constructor" << std::endl;
}
virtual ~N(){}
void display()
{
std::cout << m_data1 << std::endl;
}
public:
int m_data1;
int m_data2;
int m_data3;
};
//虚继承方式
class A : virtual public N
{
public:
A() :N(11, 12, 13), m_a(1)
{
std::cout << "call class A constructor" << std::endl;
}
~A(){}
public:
int m_a;
};
//虚继承方式
class B : virtual public N
{
public:
B() :N(21, 22, 23), m_b(2)
{
std::cout << "call class B constructor" << std::endl;
}
~B(){}
public:
int m_b;
};
// 类A和类B是虚继承方式
class C : public A, public B
{
public:
//负责对直接基类的初始化 以及虚基类的初始化
C() : A(), B(), N(31,32,33),
m_c(3)
{
std::cout << "call class C constructor" << std::endl;
}
void show()
{
std::cout << "m_c=" << m_c << std::endl;
}
public:
int m_c;
};
//此时基类N不是虚基类
class D : public N
{
public:
//负责对基类的初始化
D() :N(41, 42, 43),
m_d(4)
{
std::cout << "call class D constructor" << std::endl;
}
void show()
{
std::cout << "m_d=" << m_d << std::endl;
}
public:
int m_d;
};
int _tmain(int argc, _TCHAR* argv[])
{
C data;
//直接使用基类数据
data.m_data1 = 10;
return 0;
}