看侯捷老师的C++内存模型时,讲到了虚继承。虚继承算是C++特有的知识了,特此记录下。
#include // std::cout std::endl
class base
{
public:
long long par=0;
void show(int par) const noexcept;
};
void base::show(int par) const noexcept
{
par=par;
std::cout << "par:" << par << std::endl;
}
class derived1 : public base{};
class derived2 : public base{};
class final_derived : public derived1, public derived2
{
public:
long long fpar=3;
};
int main(void)
{
final_derived object;
// 两个show()函数,编译器不知道调用哪个
object.show();
return 0;
}
首先需要明白,编译器寻找普通成员函数时,和this指针有关系:
this指针是一个指针常量,其地址不能改变,指向当前对象,做为成员函数的第一个默认缺省参数,由编译器管理。
this指针的两个作用:
其一,类似于模板函数的类型推导确定对象所属类型,所以不同的类调用同名函数,是不会出现问题的,并确定函数操作的数据块大小;其二,它的值就是对象object的地址;
因此,通过this指针,当存在多个同名函数时,编译可以根据对象推导参数类型,找到这个类对应的函数,并操作对应空间的数据。
接下里,剖析下多继承中菱形继承问题。
由于函数会占用内存中代码区的资源,所以如果子类不用修改父类中的某一个成员函数,那直接用父类的这个函数就好了:
#include
using namespace std;
class base
{
public:
long long par=0;
void show(int par) const noexcept;
};
void base::show(int par) const noexcept
{
par=par;
std::cout << "par:" << par << std::endl;
}
class derived1 : public base{
public:
void show(int par){
cout<<"show of derived1"<
dst_type pointer_cast(src_type src)
{
// 巧妙地转换:由于static_cast不能转换两个毫不相关的变量,利用void* 进行转换
return *static_cast(static_cast(&src));
}
int main(void)
{
base* p1 = pointer_cast (&base::show);
derived1* p2 = pointer_cast(&derived1::show);
derived2* p3 = pointer_cast(&derived2::show);
cout<
三个成员函数的地址为:0x401550 0x4159c0 0x401550
可以看出:由于derived2的show函数就是用的base父类的show函数,而没有新建show函数;说的直白点,就是derived1的成员函数show实际变成了show(derived* const this,int par),而derived2的成员函数show还是show(base* const this,int par)。
接下来,并在main函数加入如下代码:
final_derived object;
object.show(1);
发现编译器直接报错:show函数目标不明确
一开始我是这么以为的:
这是由于final_derived没有重写show函数,所以会调用父类的show函数。然而,final_derived类调用show函数时,既能匹配show(derived* const this,int par),也能匹配show(base* const this,int par),编译器不明确到底调用哪个。
然而,实事并不是这样,我把derived1中的show函数删除了,也就是只剩一个show(base* const this,int par)了,但仍然出现show函数目标不明确的报错。所以,实事就是编译器在进行语法分析时,发现final_derived有两个相同的show函数,直接就报错了。
为避免调用函数时,语法问题造成调用失败,有三种方法可以解决:
一、final_derived重写show函数:
class final_derived : public derived2,public derived1
{
public:
long long fpar=3;
void show(int par){
derived1::show(par);
}
};
但这样有个缺点,本来final_derived就是用的derived1的方法,且未作任何修改,按C++的设计思想,直接用父类derived1类的show方法就可以了,不应该用额外的内存。
二、调用的时候,指定具体的类,给this指针传更精确的类型:
int main(void)
{
final_derived object;
object.derived1::show(1);
object.derived2::show(2);
return 0;
}
这样就是写代码会很麻烦,别人还得知道你是怎么继承的。
三、虚继承
也就是在derived1类和derived2类继承base时,添加virtual关键字:
#include
using namespace std;
class base
{
public:
long long par=0;
void show(int par) const noexcept;
};
void base::show(int par) const noexcept
{
par=par;
std::cout << "par:" << par << std::endl;
}
class derived1 : virtual public base{
public:
void show(int par){
std::cout << "show of derived1"<< std::endl;
}
};
class derived2 : virtual public base{};
class final_derived : public derived2,public derived1
{
public:
long long fpar=3;
};
int main(void)
{
final_derived object;
object.show(1);
return 0;
}
这里输出的是show of derived1。
如果删除derived1的show函数,输出为par=1,也就是调用base的show函数
derived1 和 derived1 虚继承 base,会新建一个虚基类表,存储虚基类相对直接继承类的偏移量,并把指向虚基类表的虚基类指针存入类中。
这样,final_derived在调用show时,过程如下:
可以看出,虚继承并不能保证object.show(1)的合法调用,最好不要用多继承,就把虚继承这种机制当作语法糖吧。
删除上述代码的show函数,专注于成员变量par上,观察object对象中的成员变量分布,以及它的大小。
#include
using namespace std;
class base
{
public:
long long par=0;
void show(int par) const noexcept;
};
void base::show(int par) const noexcept
{
par=par;
std::cout << "par:" << par << std::endl;
}
class derived1 : public base{};
class derived2 : public base{};
class final_derived : public derived2,public derived1
{
public:
long long fpar=3;
};
int main(void)
{
final_derived object;
cout<
其大小为24bytes(64位机器下),成员变量分布为:
依次是从derived2类继承的par,从derived1类继承的par,以及自身的fpra。由于final_derived是先继承的 derived2 后继承 derived1,因此从derived2继承来的par也分布在前端。
那么现在问题来了:其一:这个类中有两个par变量,要怎么访问呢?其二:从上述代码来看,final_derived直接用base的par就可以了,用两个par变量不是浪费空间吗?
如果只解决第一个问题,可以通过指明具体类的方法:
cout<
但如果还要解决第二个问题,仍然得借助虚继承,为了更方便说明,我加了两个参数par2和par3:
#include
using namespace std;
class base
{
public:
long long par=0;
long long par2=1;
long long par3=2;
void show(int par) const noexcept;
};
void base::show(int par) const noexcept
{
par=par;
std::cout << "par:" << par << std::endl;
}
class derived1 : virtual public base{};
class derived2 : virtual public base{};
class final_derived : public derived2,public derived1
{
public:
long long fpar=3;
};
int main(void)
{
final_derived object;
cout<
输出结果为48bytes,内存分布从上到下依次为:derived2的虚基类指针,derived1的虚基类指针,final_derived自身的fpar,base的三个成员变量(单继承中,父类的成员变量是放前面的)。
访问成员变量类似于成员函数的调用,先看类本身是否存在这个变量,然后去父类中找,最后找父类的虚继承表。
题外话: 虽然我一直觉得组合比继承好,但这里用组合好像没啥办法省内存,算是继承的一个优点吧,但代价就是代码写起来很麻烦。