C++ 虚函数机制分析

原文:C++ 虚函数机制分析
作者:Breaker <breaker.zy_AT_gmail>


C++ 中的虚函数调用机制通常是靠虚函数表 (vtbl) 和虚表指针 (vptr) 实现的,调用行为称为 晚绑定 (later binding)、动态绑定 (dynamic binding) 或 运行时绑定 (runtime binding),Bjarne Stroustrup 的《C++ 程序设计语言》章节 2.5.5 和 第 15 章(类层次结构)中提到虚函数表机制,但没有做详细的分析,他说虚函数的调用机制和 C++ 语言实现有关(指编译器),虚函数的实现方式也不一定是虚函数表

好书:"Inside the C++ Object Model" by Stanley Lippman,中文《深度探索 C++ 对象模型》,侯捷 译,在 C++ 程序员圈子中口碑很好,内容专注于 C++ 语言的底层机制,也包括虚函数、虚继承等 C++ 多态行为

这是自己用 g++ 编译、gdb 调试、分析的 C++ 虚函数工作机制,感觉有些收获,比单纯看书踏实。未涉及:包括多继承,但不包括虚继承(钻石继承)

关键字:调试分析, 字节存储, 继承, 多继承, 动态绑定, 虚函数, 虚函数表 vtbl, 虚表指针 vptr

DIY: Do It Yourself

目录


实验平台^

系统:Windows XP Pro
编译工具:Cygwin g++ 4.3.4
调试工具:Cygwin gdb 6.8.0

测试程序框架^
  1. 测试类声明

    程序中有 5 个类:Shape、Shape 的子类 RectCircleColor、Color 和 Circle 的子类ColorCircle,类声明如下:

    #define MY_MAX_BUF 64 #define VIRTUAL virtual class Shape { public: Shape(const char* name = NULL); ~Shape(); VIRTUAL void print_name(); VIRTUAL void draw(); VIRTUAL void rotate(); private: void init(const char* name); public: static char s_DefaultName[MY_MAX_BUF]; protected: char* m_name; }; class Rect : public Shape { public: Rect(const char* name = NULL); ~Rect(); VIRTUAL void print_name(); VIRTUAL void draw(); public: static char s_DefaultName[MY_MAX_BUF]; }; class Circle : public Shape { public: Circle(const char* name = NULL); ~Circle(); VIRTUAL void print_name(); VIRTUAL void draw(); public: static char s_DefaultName[MY_MAX_BUF]; }; class Color { public: Color(); VIRTUAL void reverse(); VIRTUAL void lighten(); VIRTUAL void darken(); }; class ColorCircle : public Circle, public Color { public: ColorCircle(const char* name = NULL); };

    我将 VIRTUAL 定义为关键字 virtual 的宏,用于方便对比测试:使用和不使用虚函数时的调用方式

  2. 测试类成员定义

    在 Shape 类的继承格中,子类 Circle 和 Rect 只覆盖了 print_name()、draw() 两个父类成员函数,没有覆盖父类的 rotate() 函数,并且覆盖了父类 Shape 的类静态数据成员 s_DefaultName。在 ColorCircle 类的继承格中,ColorCircle 使用直接父类 Color 和 Circle 的成员,没有进行覆盖。这 5 个类的成员定义如下:

    // Basic Type typedef unsigned int UINT; typedef UINT* PUINT; typedef unsigned char BYTE; typedef BYTE* PBYTE; union BYTES_8 { BYTE bytes[8]; }; // Shape class char Shape::s_DefaultName[MY_MAX_BUF] = "Anonymous Shape"; void Shape::init(const char* name) { if (name != NULL) { size_t len = strlen(name); if (len == 0 || len > MY_MAX_BUF) { fprintf(stderr, "Shape::Shape(const char*): param name error"); m_name = s_DefaultName; } else m_name = strdup(name); } else m_name = s_DefaultName; } Shape::Shape(const char* name /*= NULL*/) { init(name); } Shape::~Shape() { if (m_name != NULL && m_name != s_DefaultName) free(m_name); } void Shape::print_name() { printf("Shape obj (0x%08X) name: %s\n", reinterpret_cast<UINT>(this), m_name); } void Shape::draw() { printf("draw Shape\n"); } void Shape::rotate() { printf("rotate Shape\n"); } // Circle class char Circle::s_DefaultName[MY_MAX_BUF] = "Anonymous Circle"; Circle::Circle(const char* name /*= NULL*/) : Shape(name) { if (m_name == Shape::s_DefaultName) m_name = s_DefaultName; } Circle::~Circle() { if (m_name == s_DefaultName) m_name = Shape::s_DefaultName; } void Circle::print_name() { printf("Circle obj (0x%08X) name: %s\n", reinterpret_cast<UINT>(this), m_name); } void Circle::draw() { printf("draw Circle\n"); } // Rect class char Rect::s_DefaultName[MY_MAX_BUF] = "Anonymous Rect"; Rect::Rect(const char* name /*= NULL*/) : Shape(name) { if (m_name == Shape::s_DefaultName) m_name = s_DefaultName; } Rect::~Rect() { if (m_name == s_DefaultName) m_name = Shape::s_DefaultName; } void Rect::print_name() { printf("Rect obj (0x%08X) name: %s\n", reinterpret_cast<UINT>(this), m_name); } void Rect::draw() { printf("draw Rect\n"); } // Color class Color::Color() { } void Color::reverse() { printf("reverse Color obj (0x%08X)\n", reinterpret_cast<UINT>(this)); } void Color::lighten() { printf("lighten Color obj (0x%08X)\n", reinterpret_cast<UINT>(this)); } void Color::darken() { printf("darken Color obj (0x%08X)\n", reinterpret_cast<UINT>(this)); } // ColorCircle class ColorCircle::ColorCircle(const char* name /*= NULL*/) : Circle(name) { }
  3. 测试代码

    用于测试虚函数调用机制的基本代码如下:

    // Global Definition #define MIMIC_STATIC_BINDING 1 #define MIMIC_DYNAMIC_BINDING 0 #define PRINT_VTBL 0 // Type Definition // 注意: // 要用子类的成员函数类型,因为父类的成员函数可以很容易的赋值给子类的成员函数 // 指针变量,反之则不行,这种特性叫做“反变”(contra-variance),参考: // "The C++ Programming Language" 章节 15.5.1 // 后来因为:兄弟类(如:Circle、Rect)之间无法完成反变,而且如果使用末梢子类的话, // 会引起成员函数赋值歧义,所以改为使用位模式强制转换的办法(reinterpret_cast), // 将成员函数地址转换到泛型类 Void 的成员函数指针类型 Void_PFunc class Void { void void_func() {}; }; typedef void (Void::*Void_PFunc)(); typedef void (Shape::*Shape_PFunc)(); typedef void (Color::*Color_PFunc)(); // 函数指针不能直接转换到数据指针,依靠下面联合体转换 union PFuncConvertor { Void_PFunc pf_void; Shape_PFunc pf_shape; Color_PFunc pf_color; UINT uint[sizeof(Void_PFunc)/sizeof(UINT)]; }; // Function Declaration void dump_mem(void* pbeg, size_t size); void print_vtbl(void* pobj); void print_func_addr(Void_PFunc pf, const char* fname); // Function Definition int main() { Shape s Shape* ps = NULL; Circle c; Rect r; Color clr; ColorCircle clrcc; // 打印类静态数据 // BEGIN // // 子类从父类继承的静态数据,和父类共享同一块存储空间 printf("+++++ Shape class static data member +++++n\n"); dump_mem(s.s_DefaultName, sizeof(s.s_DefaultName)); putchar('\n'); printf("+++++ Circle class static data member +++++n\n"); dump_mem(c.s_DefaultName, sizeof(c.s_DefaultName)); putchar('\n'); // // END // 打印类对象数据,包括 vptr(如果是多态对象)和类对象数据成员 // BEGIN // printf("+++++ Shape object (data member) +++++n\n"); dump_mem(&s, sizeof(s)); putchar('\n'); printf("+++++ Circle object (data member) +++++n\n"); dump_mem(&c, sizeof(c)); putchar('\n'); printf("+++++ ColorCircle object (data member) +++++n\n"); dump_mem(&clrcc, sizeof(clrcc)); putchar('\n'); // // END #if PRINT_VTBL // 打印虚表 // BEGIN // printf("+++++ Shape object vtbl +++++n\n"); print_vtbl(&s); printf("+++++ Circle object vtbl +++++n\n"); print_vtbl(&c); printf("+++++ Color object vtbl +++++n\n"); print_vtbl(&clr); printf("+++++ ColorCircle object vtbl +++++n\n"); print_vtbl(&clrcc); // // END #endif // PRINT_VTBL defined // 打印成员函数地址 // BEGIN // // 经测试,Cygwin GCC 4 中,普通函数(非成员函数)指针占用 4 byte // 成员函数指针占用 8 byte,高 4 byte 通常为 0,低 4 byte 存储有效数据 // (如果是非虚函数存储函数地址,如果是虚函数存储虚函数序号)。成员函数 // 指针比普通函数指针多占用一个机器字,导致:成员函数地址无法赋值给普通 // 函数指针(即使使用位模式强转reinterpret_cast,或C形式的强转也无法完成) // 另外成员函数地址也无法赋值给泛型指针(void*)和其它数据类型指针(即使 // 使用强转,并且数据类型长度大于等于 8 byte 也无法完成) // 可以使用强转将一个类的成员函数地址赋值给另一个类的成员函数指针 // 猜测:成员函数指针多出来的高 4 byte 可能用来存储对象本体地址(this), // 由对象本体和成员函数地址组合,才能构成一次完整的成员函数调用,这种 // 组合动作就在使用 obj.*pfunc 和 pobj->*pfunc 这种以成员函数指针方式调用 // 成员函数时触发 // 普通函数地址到 void* 的强转 可以 // 普通函数地址到 UINT* 的强转 可以 // 普通函数地址到 BYTES_8* 的强转 可以 // 成员函数地址到 void* 的强转 不可以 // 成员函数地址到 UINT* 的强转 不可以 // 成员函数地址到 BYTES_8* 的强转 不可以 // 一个类的成员函数地址到 Void_PFunc 的强转 可以 printf("sizeof PFuncConvertor: %u\n", sizeof(PFuncConvertor)); printf("sizeof Void_PFunc: %u\n", sizeof(Void_PFunc)); putchar('\n'); printf("+++++ Shape object member function address +++++n\n"); print_func_addr(reinterpret_cast<Void_PFunc>(&Shape::print_name), "Shape::print_name"); print_func_addr(reinterpret_cast<Void_PFunc>(&Shape::draw), "Shape::draw"); print_func_addr(reinterpret_cast<Void_PFunc>(&Shape::rotate), "Shape::rotate"); putchar('\n'); printf("+++++ Circle object member function address +++++n\n"); print_func_addr(reinterpret_cast<Void_PFunc>(&Circle::print_name), "Circle::print_name"); print_func_addr(reinterpret_cast<Void_PFunc>(&Circle::draw), "Circle::draw"); print_func_addr(reinterpret_cast<Void_PFunc>(&Circle::rotate), "Circle::rotate"); putchar('\n'); printf("+++++ Rect object member function address +++++n\n"); print_func_addr(reinterpret_cast<Void_PFunc>(&Rect::print_name), "Rect::print_name"); print_func_addr(reinterpret_cast<Void_PFunc>(&Rect::draw), "Rect::draw"); print_func_addr(reinterpret_cast<Void_PFunc>(&Rect::rotate), "Rect::rotate"); putchar('\n'); printf("+++++ Color object member function address +++++n\n"); print_func_addr(reinterpret_cast<Void_PFunc>(&Color::reverse), "Color::reverse"); print_func_addr(reinterpret_cast<Void_PFunc>(&Color::lighten), "Color::lighten"); print_func_addr(reinterpret_cast<Void_PFunc>(&Color::darken), "Color::darken"); putchar('\n'); printf("+++++ ColorCircle object member function address +++++n\n"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::print_name), "ColorCircle::print_name"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::draw), "ColorCircle::draw"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::rotate), "ColorCircle::rotate"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::reverse), "ColorCircle::reverse"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::lighten), "ColorCircle::lighten"); print_func_addr(reinterpret_cast<Void_PFunc>(&ColorCircle::darken), "ColorCircle::darken"); putchar('\n'); // // END // 测试静态/动态绑定 // BEGIN // printf("+++++ Test static/dynamic binding +++++n\n"); ps = &s; ps->print_name(); ps = &c; ps->print_name(); ps = &r; ps->print_name(); putchar('\n'); // // END #if MIMIC_DYNAMIC_BINDING // 模拟动态绑定 // BEGIN // printf("+++++ Mimic dynamic binding +++++n\n"); PFuncConvertor con_2 = {0}; con_2.uint[0] = 1; (clrcc.*con_2.pf_shape)(); (clrcc.*con_2.pf_color)(); putchar('\n'); // // END #endif // MIMIC_DYNAMIC_BINDING defined #if MIMIC_STATIC_BINDING // 模拟静态绑定 // BEGIN // printf("+++++ Mimic static binding +++++n\n"); PFuncConvertor con[MY_MAX_BUF] = {{0}}; con[0].uint[0] = 0x0040121A; // Shape::print_name con[1].uint[0] = 0x004013FA; // Shape::draw con[2].uint[0] = 0x004013E6; // Shape::rotate con[3].uint[0] = 0x004011F6; // Circle::print_name con[4].uint[0] = 0x004013D2; // Circle::draw con[5].uint[0] = 0x004013E6; // Circle::rotate con[6].uint[0] = 0x004011D2; // Rect::print_name con[7].uint[0] = 0x004013BE; // Rect::draw con[8].uint[0] = 0x004013E6; // Rect::rotate for (int i = 0; i < 3; i++) { (ps->*con[i].pf_shape)(); (ps->*con[i + 3].pf_shape)(); (ps->*con[i + 6].pf_shape)(); putchar('\n'); } // // END #endif // MIMIC_STATIC_BINDING defined return 0; } void print_func_addr(Void_PFunc pf, const char* fname) { PFuncConvertor con = {0}; con.pf_void = pf; printf("addr %s: 0x%08X%08X\n", fname, con.uint[1], con.uint[0]); } // 假设对象地址 pobj 的第一个机器字中保存的是 vptr(如果该对象是多态对象的话) // 打印该对象类的 vtbl,输出的 vtbl 中的机器字个数到第一个 0 的为止 void print_vtbl(void* pobj) { int i = 0; PUINT vptr = NULL; // 虚表指针 vptr PUINT pvptr = NULL; // 虚表指针的地址 pvptr pvptr = reinterpret_cast<PUINT>(pobj); vptr = reinterpret_cast<PUINT>(pvptr[0]); // 最多查看 128 个机器字 for (i = 0; i < 128; i++) { if (vptr[i] == 0) break; } // vptr 值为 vtbl 的起始地址 dump_mem(vptr, (i + 1) * sizeof(UINT)); putchar('\n'); } void dump_mem(void* pbeg, size_t size) { PBYTE pbyte = reinterpret_cast<PBYTE>(pbeg); printf("dump bytes begin: 0x%08X, bytes size: %un\n", reinterpret_cast<UINT>(pbeg), size); if (size == 0) return; puts("offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"); puts("-----------------------------------------------------------"); int col = 0; for (size_t i = 0; true; i++) { if (col == 0) printf("0x%08X: ", reinterpret_cast<UINT>(pbyte + i)); printf("%02X", pbyte[i]); if (i == size - 1) break; if (col == 15) { putchar('\n'); col = 0; } else { putchar(' '); col++; } } putchar('\n'); }
普通非虚成员函数的调用^

在测试代码中,将下面的宏定义如下:

#define VIRTUAL #define MIMIC_STATIC_BINDING 1 #define MIMIC_DYNAMIC_BINDING 0 #define PRINT_VTBL 0

以激活对普通非虚成员函数调用的测试代码,普通成员函数使用 this call 调用约定的静态绑定(或早绑定)方式调用。所谓静态绑定就是在编译时已经确定了被调函数的地址,我在测试代码中找到这些成员函数地址,然后显式地写在程序中以模拟静态绑定行为,实验结果说明手工指定地址和编译产生的程序行为是一样的。下面是输出结果和相关的注释:

  1. 类静态数据成员

  2. 类对象的存储

    类对象中只存储类定义中非静态数据成员,以及虚表指针 vptr(如果该类是多态类)

    类定义中的静态数据成员、成员函数都是类自身的信息,和具体的类实例对象无关。对于成员函数:

    • 非静态的成员函数:使用 this call 调用约定,这意味着在调用时才会需要对象的信息,函数的实现代码是类自身的信息
    • 静态的成员函数:根据声明可以使用 cdec call、std call 等调用约定,而不使用 this call 调用约定。静态成员函数的调用、实现代码都和对象无关,所以它和全局函数的调用机制是相同的
    • 虚函数:使用 this call 调用约定,和动态绑定机制实现,虚表指针保存在对象本体 (this) 中。因此,一个函数不可能即是静态的,又是虚的,几乎所有的 C++ 编译器都能发现这种错误

    下面是测试中的对象存储:

  3. 静态绑定测试

  4. 模拟静态绑定

    将上面静态绑定测试中得到的函数地址,手工输入到源代码中,测试调用行为:

虚函数的调用^

测试代码中定义如下宏:

#define VIRTUAL virtual #define MIMIC_STATIC_BINDING 0 #define MIMIC_DYNAMIC_BINDING 1 #define PRINT_VTBL 1

以激活对虚函数调用的测试代码。下面是输出结果和相关的注释:

  1. 多态对象的存储

  2. 虚函数表

  3. 动态绑定测试

  4. 模拟动态绑定

    模拟代码中,使用虚函数序号 1,即对应虚函数表偏移为 0 的函数地址,相关代码如下:

    ColorCircle clrcc; PFuncConvertor con_2 = {0}; con_2.uint[0] = 1; (clrcc.*con_2.pf_shape)(); (clrcc.*con_2.pf_color)();

    运行结果如下:

    从上面结果可知,在多继承情况下,虽然虚函数序号都为 1,但在运行时,动态绑定机制会根据具体的 RTTI 信息(本例中成员函数指针的类型关联了 RTTI)选择合适的 vptr,即 vtbl 中合适的虚函数偏移起始地址,最后计算出类型相关的实际调用地址

动态绑定机制总结

虚函数的动态绑定机制,和 3 个东西相关:

  • 虚函数表 vtbl:它是多态类自身的信息,和具体的对象无关。在多继承情况下,虚表中罗列继承自每个父类的虚函数地址。如果子类直接使用父类中的虚函数定义,不进行覆盖,则子类、父类的虚函数地址就会相同;如果子类进行覆盖虚函数,vtbl 中对应的虚函数地址就会替换成子类自己的函数地址

  • 虚表指针 vptr:它保存 vtbl 的地址,每个多态类的对象存储中都会有 vptr。并且在多继承情况下,会存在多个 vptr。多继承子类对象,是按续构造父类对象后的拼接对象,再加上子类特有的存储

  • 虚函数序号:它是配合 vtbl 实现动态绑定的重要元素,它在编译时就确定了。在运行时,会根据 RTTI 来选择合适的 vptr,配合虚函数序号,计算 vtbl 中的存储虚函数地址的位置,最后找到虚函数的实际调用地址,进行 this call 调用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值