引言
大概有接近一年的时间没有怎么用过c++了, 大多数时间都在学习关于linux以及linux C的知识, 一直想总结一些c++的知识却总不知怎么写, 从哪里入手, 前几月重新整理了一些关于c++类的内存布局的验证实验就想着正好可以用于切入口。
环境
这些实验都是在vs2017 x86环境下实现的, 自己也在x64以及g++中尝试过, 只能通过编译, 运行就会段错误,索性我就只用vs来实现了。为了更加直观的看到类的内存布局, 请先对vs进行必要的配置vs中c++类的内存分布调试环境
对象模型
在深度探索c++对象模型的书中提到过关于c++对象模型有简单对象模型, 表格驱动对象模型, c++对象模型三种。
简单对象模型现主要应用于指向成员的指针, 也就类似于虚表的作用。
表格驱动对象模型主要应用于虚函数, 即虚指针。
c++对象模型应用于c++类的内存布局。
虚指针的内存布局
在类中关于虚指针的存放位置有两种,根据编译器的不同有可能存放位置就不同, 一种是存放在类的前端(与c不兼容), 另一种是存放在类的尾端(与c兼容)。 前者是一般编译器都是采用的, 我使用的环境都是前者。
虚表
类中只存放了一个虚指针用来指向内存中虚表的位置, 而虚函数都是存放在虚表中, 这样不管类中有多少个虚函数就只需要一个虚指针就行了。虚表就是一个数组指针(说是二维数组还是有点欠缺, 毕竟数组指针跟二位数组还是有区别), 数组中每个指针就指向一个虚函数的地址。
类内存布局的验证
现在我们就来简单的对类的内存分布在vs中做一个简单的验证。
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <functional>
using std::cin; using std::cout; using std::string; using std::endl;
typedef int value_type;
typedef class Point2
{
public:
Point2(value_type x = 0, value_type y = 0) : x(x), y(y) {}
virtual void print1() { cout << "printf1" << endl; }
virtual void print2() { cout << "printf2" << endl; }
void print() { cout << x << endl; }
private:
virtual void print3() { cout << "printf3" << endl; }
private:
value_type x, y;
static value_type num;
}Point2;
value_type Point2::num = 0;
int main()
{
system("pause");
}
类的布局情况我们能够清楚的明了, 虚指针放在了类的前端接着才存放类的其他成员变量, 同样也能清楚该类的大小为什么是8字节, 注意静态成员并不存放在类中; 接着再来看下面的虚表,vpt[0]指向print1虚函数, vpt[1]指向print2虚函数。
现在我们就验证了虚函数在类中的布局, 同时也证明了虚表的存在。
直接调用虚函数
上面我们验证了虚函数在类中的布局, 现在我们直接通过一个函数指针来调用虚函数, 甚至我们还可以直接调用私有函数, 私有成员。
将上面的代码添加以下代码即可 :
typedef void(*Fun)(void);
int main()
{
Point2 point(1, 1);
std::function<void(void)> fun;
fun = (Fun)*((int*)*(int*)(&point) + 0);
fun(); // printf1
fun = (Fun)*((int*)*(int*)(&point) + 1);
fun(); // printf2
// 直接调用私有的虚函数
fun = (Fun)*((int*)*(int*)(&point) + 2);
fun(); // printf3
point.print(); // 1
value_type x;
x = *((int*)&point + 1) = 2;
point.print(); // 2
cout << x << endl; // 2
system("pause");
exit(0);
}
从运行结果证实了我们通过一个普通指针访问了printf3
这个私有虚函数, 还修改了x
这个私有成员变量的值。
如果你对上面的(Fun)*((int*)*(int*)(&point) + 0)
写法很难明了的话, 前面说了虚表就是一个数值指针, 那么也可以这样修改代码。这样就清楚明了了。
typedef void(*Fun)(void);
int **vptr = NULL;
int main()
{
Point2 point(1, 1);
vptr = (int**)&point;
std::function<void(void)> fun;
fun = (Fun)vptr[0][0];
fun();
fun = (Fun)vptr[0][1];
fun();
// 直接调用私有的虚函数
fun = (Fun)vptr[0][2];
fun();
point.print();
value_type x;
x = *((int*)&point + 1) = 2;
point.print();
cout << x << endl;
system("pause");
exit(0);
}
总结
本节之所以涉及到修改类中的私有成员, 只是为了让人对类的内存布局有一个更深的印象而已, 但是这种做法无法在太多编译器中实现, 因为这样做就完全忽视了封装性原则, 所以64和g++都是我发运行的。
本节也只是对c++的虚函数和虚表有了一个认识了解, 还有好几个点都没有去涉及, 比如类大小的计算, 静态成员为什么不算在类的内存布局中, 还有多态又是怎样通过虚表实现的… 这些都是下面我们来分析的问题了。