一、继承和多态
首先说明的是,代码示例是在X64位中编译的。
说起对象的布局,肯定不能离开大牛的代表著作《深度探索c++对象模型》,译者是侯捷先生,双剑齐出,不同凡响。那么c++的类和C中的结构体有什么不同呢?这里抛砖引玉,先看一个简单的例程:
class Base
{
public:
Base() {}
~Base() {}
public:
int SetBaseData(int data)
{
this->data_ = data;
return this->data_;
}
private:
int data_ = 0;
};
class Derived :public Base
{
public:
Derived() {}
~Derived() {}
public:
int SetBaseData(int data)
{
this->data_ = data;
return this->data_;
}
private:
int data_ = 0;
};
void Test()
{
Base b;
Derived d;
int len_b = sizeof b;
int len_d = sizeof d;
std::cout << "Base size:" << len_b << " Derived size:" << len_d << std::endl;
}
int main()
{
Test();
}
它的运行结果是:
Base size:4 Derived size:8
很容易发现,基类和继承类中各自只有一个int类型的数据,从运行结果判断,他们各自占了四个字节,所以子类继承了父类,就得占8个字节。之所以用这个简单的例子,目的是可以清晰的看出二者的关系,而不用考虑字节对齐的情况,能够更明白的分析父子类的内存总局情况。
从这里回到继承的分析,简单的看来,如果一个继承状态只包含普通数据类型(去除静态等),那么父子类对象就是简单的包容关系,这在上面提到的书中也提到过这个方式,它只是c++对象模型的一种解决方式。但实际上c++对象模型并没有使用这种简单的机制,为什么呢?因为多态,多态已经讲过很多遍,就是动态的判定指针和引用的实际类型,以期进行正确的运行目标。
二、虚表
多态是如何实现的呢?是通过虚表,什么是虚表?这里先不说明,继续看上面的例子,进行一下简单的修改:
class Base
{
public:
Base() {}
~Base() {}
public:
//这里增加了一个关键字virtual
virtual int SetBaseData(int data)
{
this->data_ = data;
return this->data_;
}
private:
int data_ = 0;
};
代码改动非常小,只在基类中唯一的函数前面加了一个virtual的关键字,然后重新编译后再次运行:
Base size:16 Derived size:24
增加了一个关键字,父子类就多了不少字节。多的啥呢?就是虚表的指针。在32位系统上指针占用4个字节,在64位的系统上指针占用8个字节,然后再以8字节对齐,就发现符合运行结果了。那么虚表到底是什么呢?虚表就是c++的对象模型为了保证编译过程中在效率和空间上取得平衡的一种对象模型机制的实现方式。《深度探索c++对象模型》中提到:“每个类都产生一堆指向virtual function的指针,放在表格之中,这个表格就叫做virtual table(vptl)",“每个类对象都添加了一个指针指向相关表格,这个指针叫做vptr,它由类自动完成”。虚表有很多种的称呼,如果看到下面的情况,其实都是说的虚表:
1、virtual method table(VMT)
2、virtual function table(vftable)
3、virtual call table
4、dispatch table
5、vtable
它的模型基本是下图这样的:
有一点需要说明,模型原理是一回事,编译器实现是另外一回事,这里又体现了出来,按照书中所讲,vptr没有必须放到第一位,但是在目前在编译器实现中,基本都是放到了第一位,所以一定要明白原因。
三、实例分析
深入剖析一下上面这个例子,由小看大,其实没啥区别。首先,简单的在代码里加上offsetof()这个函数来看一下变量的偏移位置,此时需要把变量从私有改为公有,否则无法通过编译。即:
std::cout << offsetof(Base,data_) << std::endl;
std::cout << offsetof(Derived, data_) <<std::endl;
其运行结果为:
8 16
同样,为了更加清晰的表现虚表,再增加一个继承类,同时将所有的对象改成指针对象:
class Derived1 :public Base
{
public:
Derived1() {}
~Derived1() {}
public:
int SetBaseData(int data)
{
this->data_ = data;
return this->data_;
}
public:
int data_ = 0;
};
void Test()
{
Base * b = new Base();
Derived * d = new Derived();
Derived1* d1 = new Derived1();
std::cout << offsetof(Base,data_) << std::endl;
std::cout << offsetof(Derived, data_) <<std::endl;
......
delete d;
delete dd;
delete d1;
delete d11;
}
在VS2019中下断点看一下效果:
在网上找了在线反编译的网站编译了一下不同的情况,下图是对比:
既然虚表的位置在对象的第一位,那么就可以通过下面的代码来找到它:
typedef int( * Fun)(int);
int** pV = (int**)d;
Fun p = (Fun)pV[0][0];
std::cout << p << std::endl;
但是这里有数据的写入和回传,直接调用会引起保护机制。
四、总结
上面其实是单继承的方式,在C++中是支持多继承的,它们的原理是一样的,都通过间接的指针来访问具体的函数,只不过在访问的时候儿更复杂了一步,如果有兴趣可以在代码中试一试,没有什么特别的不同。不过,一直不推荐在工程中使用多继承,所以这里就不再展开了。
虚表的最终目的,其实就是为了编译和加载时能够正确的进行多态的运行,明白了这一点,对照着资料再看,就比较好明白虚表所做的工作了。