其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。 与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。 因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。 /** * @file vptr1.cpp * @brief C++虚函数vptr和vtable * 编译:g++ -g -o vptr vptr1.cpp -std=c++11 * @author = (void *)*(unsigned long *)obj; //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针 printf("vptr_addr :%p\n",vptr_addr); /** * @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*
https://blog.csdn.net/Simba888888/article/details/12621955 五条基本规则: 1、如果基类已经插入了vptr, 则派生类将继承和重用该 vptr。 vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的。 因为此时基类是空类1个字节,派生类有虚函数故有vptr 4个字节,基类“继承”的1个字节附在vptr下面,现在的p 实际上是指向了附属1字节,即operator delete(void*) 传递的指针值已经不是 将基类析构函数改成虚函数,fun() 最好也改成虚函数,只要有一个虚函数,基类大小就为一个vptr ,此时基类和派生类大小都是4个字节,p也指向派生类的首地址,问题解决,参考规则3。
五条基本规则: 1、如果基类已经插入了vptr, 则派生类将继承和重用该vptr。 vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的。 2、在遇到通过基类指针或引用调用虚函数的语句时,首先根据指针或引用的静态类型来判断所调函数是否属于该class或者它的某个public 基类,如果 属于再进行调用语句的改写: (*(p->_vptr[slotNum 因为此时基类是空类1个字节,派生类有虚函数故有vptr 4个字节,基类“继承”的1个字节附在vptr下面,现在的p 实际上是指向了附属1字节,即operator delete(void*) 传递的指针值已经不是 将基类析构函数改成虚函数,fun() 最好也改成虚函数,只要有一个虚函数,基类大小就为一个vptr ,此时基类和派生类大小都是4个字节,p也指向派生类的首地址,问题解决,参考规则3。
虚函数指针(vptr)每个包含虚函数的对象,都会隐含一个虚函数指针(vptr),用于指向其所属类的vtable。 vptr的初始化与维护由编译器自动完成: 初始化时机:对象构造过程中,在基类构造函数执行前(或执行中,依编译器实现),vptr被设置为指向当前类的vtable; 存储位置:通常位于对象内存布局的起始位置 (如64位系统中,对象首8字节为vptr),但不同编译器可能有差异(如存在虚基类时位置可能调整); 隐藏性:vptr是编译器自动添加的隐藏成员,无法在用户代码中直接访问,但可通过调试工具或指针操作间接观察 例如,一个包含vptr的对象内存布局(64位系统)通常为: +----------------+ // 起始地址| vptr (8字节) | // 指向类的vtable+---------- vptr解引用+固定索引访问,性能接近直接函数调用; 内存开销可控:对象仅增加一个vptr(8字节),vtable为类级共享,不占用对象内存。
"%s:%d\n", __FUNCTION__, __LINE__); } }; int main(int argc, char* argv[]) { void (foo_a::*vptr1 )() = &foo_a::info; void (foo_b::*vptr2)() = &foo_b::info; void (foo_b::*ptr)() = &foo_b::print ); printf("sizeof vptr = %d\n", (int)(sizeof(vptr1))); (a. *vptr1)(); (b.*vptr2)(); (c.*vptr1)(); (c.*vptr2)(); (c. *vptr2)()的代码。因为(c.vptr1)()生成的和单继承一样。而由于它们最终都转向vcall,所以vptr2的时候调整了虚表指针为c的第二个虚表。顺便把this指针地址调整了。
)) alignof(cv2.vptr) + sizeof(cv2.vptr) = ceil(0 / 4) 4 + 4 = 4 sizeof’(cv2, 2) = ceil(sizeof’(cv2, 1 )) alignof(b1.vptr) + sizeof(b1.vptr) = ceil(0 / 4) 4 + 4 = 4 (b1的虚函数表指针,I会复用该指针) sizeof’(I, 2) = ceil )) alignof(b2.vptr) + sizeof(b2.vptr) = ceil(0 / 4) 4 + 4 = 4 (b2的虚函数表指针,I会复用该指针) sizeof’(I, 2) = ceil )) alignof(b3.vptr) + sizeof(b3.vptr) = ceil(9 / 4) 4 + 4 = 16 (b3的虚函数表指针) sizeof’(I, 5) = ceil(sizeof )和虚基类表指针(vbptr), 或者说GCC只使用了vptr来实现虚函数的重载和虚基类的索引,方法是通过正向索引vptr来定位虚函数(vptr + offset),通过负向索引vptr来定位虚基类(vptr
derived; // 派生类对象,包含Base部分和Derived部分Base base = derived; // 对象切片:只拷贝Base部分// 内存布局对比:// derived: [vptr 拷贝了vptr(虚函数表指针),但它只有Base类的大小,无法容纳Derived类的数据。 如果通过这个vptr调用Derived的虚函数,可能会访问到不存在的内存区域。 __vptr->callVirtual(&base); // 灾难! // Derived::callVirtual期望的this指针布局:// [vptr|base_data|derived_data]// 但实际传递的this指针布局:// [vptr|base_data
构造函数 的 作用就是 创建对象 , 构造函数 最后 一行代码 执行完成 , 才意味着 对象构建完成 , 对象构建完成后 , 才会将 vptr 指针 指向 虚函数表 ; 如果在 构造函数 中 调用 虚函数 , 则 没有 多态效果 ; 一、vptr 指针初始化问题 1、vptr 指针与虚函数表 " 虚函数表 " 由 C++ 编译器 负责 创建 与 维护 , 被 virtual 关键字 修饰的 虚函数 , 会自动 被 C++ 编译器 存储到 " 虚函数表 " 中 , 类中会自动添加一个 " vptr 指针 " 成员变量 指向 虚函数表 ; 2、vptr 指针初始化时机 对象中的 vptr 指针 指向 虚函数表 , 在 对象 被 创建时 , 由 C++ 编译器 对 对象中的 vptr 指针进行初始化操作 , 对象 创建完成 后 , 也就是 虚函数 整理完毕 , 全部放到 虚函数表 中后 , vptr 指针 才会指向 虚函数表 的首地址 ; 父类 对象 的 vptr 指针 指向 父类 的 虚函数表 首地址 ; 子类 对象 的 vptr 指针 指向 子类 的 虚函数表 首地址 ; 3、构造函数 中 调用 虚函数 -
addl $8, %eax //eax=vptr+8 movl (%eax), %eax //eax=vptr[2], virtual机制 movl 8(%ebp), %eax //eax=this movl %edx, (%eax) //[this]=vptr4X movl 8(%ebp), %eax //eax=this movl %edx, (%eax) //[this]=vptr movl 8(%ebp), %eax //eax=this movl (%eax), %eax subl $12, %eax //eax=vptr-12, movl %edx, (%eax) //[this]=vptr4C movl $_ZTV1C+36, %edx movl 8(%ebp
要实现多态,必须使用指针或者引用 因为默认的赋值运算符并不会操作虚函数表 验证如下:[ Print C++ vtables using GDB] 1.1 vptr 理解成指针 因为不知道vptr 父类A::_vptr.A 内容是: 0x400c9e <A::print()> 1.3 打印 B b1;//child 执行构造函数:A() -> B() 初始化_vptr (gdb) p b1 $12 = (B) { = { _vptr.A = 0x400df0 <vtable for B+16> }, } (gdb) x/4x 0x400df0 0x400df0 <_ZTV1B+16>: 这说明对象b1.vptr 记录虚函数入口地址 0x400cc8 <B::print()> 只要a1.vptr 指向 b1. vptr 即可 1.4 a1=b1 调用 A::operator= ? a1 _vptr 没有发生变化 不可以 是不是复制操作有问题这个别人已经验证了 A& operator = (const B& b) { (int )this=(int )&b; return
在对象构造的舞蹈中,编译器是严谨的编舞者:分配内存:首先为整个对象分配足够的内存空间设置vptr:在进入构造函数体之前,编译器插入代码设置当前类的vptr构造基类:调用基类构造函数,此时vptr指向基类的虚函数表更新 vptr:基类构造完成后,vptr被更新为指向派生类的虚函数表构造成员:初始化派生类的数据成员执行构造函数体:最后执行我们在代码中编写的构造函数逻辑这意味着在基类构造函数执行期间,对象的“类型身份”仍然是基类 展开代码语言:C++AI代码解释Derivedd1,d2,d3;//d1,d2,d3各自有自己的vptr//但它们的vptr都指向同一个Derived类的vtable这种设计巧妙平衡了空间效率和时间效率 派生类不会继承基类的vptr。 相反:当创建派生类对象时,编译器会确保对象中包含一个vptr这个vptr在构造过程中会变化:先指向基类的vtable,然后指向派生类的vtable如果存在多层继承,每个完整的对象仍然只有一个vptr(在单继承情况下
; 生成虚函数表的前提是 至少有 1 个虚函数 ; 如果 没有虚函数 , 就不会生成虚函数表 ; 如果 类 中有 virtual 虚函数 , 则 该类的 每个对象 中 , 都有一个 指向 虚函数表的 vptr , 具体存储的都是 指向 类中的虚函数 的指针 ; 如果 子类 中 , 重写了 父类的 virtual 虚函数 , 那么 C++ 编译器会在 子类 虚函数表 中放入该 子类虚函数的 函数指针 ; 4、vptr virtual 虚函数 , 在创建对象时 , 会生成 虚函数表 Virtual Function Table , 简称 vtable ; C++ 编译器 编译 代码时 , 会自动为该类 添加 一个 vptr 指针 成员变量 指向 虚函数表 首地址 ; // 创建 Child 子类对象时 // 发现有 virtual 虚函数 会创建 虚函数表 // 在对象中使用 vptr 指针指向 虚函数表 首地址 指针 指向的 虚函数表 调用 对应的 虚函数 ; 父类对象 和 子类对象 中 都有一个 vptr 指针 成员变量 , 当调用 虚函数 时 , 会根据对象中的 vptr 指针查找 虚函数表 中的 对应
= (long*)(*tmp); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } 同理 ------" << endl; for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } return 0 = (long*)(*tmp1); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr1[i]); } Func a = (Func)vptr1[0]; Func b = (Func)vptr1[1]; Func c = (Func)vptr1[2]; a(); b(); c(); Derive* Func)vptr[0]; Func e = (Func)vptr[1]; Func f = (Func)vptr[2]; d(); e(); f(); return 0; }
if memberwise initialization copies the values of one object to another, 问题2 通过用子类来拷贝构造 一个父类,为什么父类的vptr 不能访问子类的须函数 ZooAnimal za = b; why is za's vptr not addressing Bear's virtual table回答2 The answer to the 译成中文就是,编译器必须要确保如果一个对象有一个或多个vptr,这些vptr不是由原对象来初始化或改变的。 也就是说:当使用赋值的方式或拷贝构造的方式创建一个对象时,这个对象的vptr与源对象无关。 初始化语义 The Semantics of the vptr Initialization 第5.2章节] 补充内容 错误理解3 vptr是是在构造函数内初始化的, 正确理解: 在父类构造函数之后,子类构造函数之前 上述完成之后, 对象的vptr会被初始化, 指向相关的虚表 如果有成员初始化列表的话, 将在构造函数体内扩展开来; 这必须在vptr被 设定之后才进行, 以免有一个虚成员函数被调用 最后执行程序员所提供的代码
C++的三大特性之一的多态是基于虚函数实现的,而大部分编译器是采用虚函数表来实现虚函数,虚函数表(VTAB)存在于可执行文件的只读数据段中,指向VTAB的虚表指针(VPTR)是包含在类的每一个实例当中。 当使用引用或指针调用虚函数时,首先通过VPTR找到VTAB,然后通过偏移量找到虚函数地址并调用。 < "D::bar" << endl;} 20 }; 21 22 typedef void (*Func)(); 23 int main() { 24 D tt; 25 Func* vptr1 = *(Func**)&tt; 26 Func* vptr2 = *((Func**)&tt + 1); 27 28 vptr1[0](); 29 vptr1[1](); 30 vptr1[2](); 31 cout<<"\\\\\\\\\\\\"<<endl; 32 vptr2[0](); 33 vptr2[1](); 34 35
= (long*)(*tmp); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } 同理 ------" << endl; for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } return 0 = (long*)(*tmp1); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr1[i]); } Func a = (Func)vptr1[0]; Func b = (Func)vptr1[1]; Func c = (Func)vptr1[2]; a(); b(); c(); Derive* Func)vptr[0]; Func e = (Func)vptr[1]; Func f = (Func)vptr[2]; d(); e(); f(); return 0; }
. */ readn(int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nread = read(fd, ptr, nleft)) < 0) { unp.h" ssize_t /* Write "n" bytes to a descriptor. */ writen(int fd, const void *vptr , size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; , size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen
对比 定义了 虚函数 的类 与 没有定义虚函数的类 的大小 , 其它成员都相同 , 定义了虚函数的类多出了 4 字节 , 多出的 4 字节就是 vptr 指针占用的内存空间 ; 一、验证指向 虚函数表 的 vptr 指针 是否存在 1、虚函数表与 vptr 指针由来 " 虚函数表 " 由 C++ 编译器 负责 创建 与 维护 , 被 virtual 关键字 修饰的 虚函数 , 会自动 被 C++ 编译器 ; 生成虚函数表的前提是 至少有 1 个虚函数 ; 如果 没有虚函数 , 就不会生成虚函数表 ; 如果 类 中有 virtual 虚函数 , 则 该类的 每个对象 中 , 都有一个 指向 虚函数表的 vptr virtual 虚函数 , 在创建对象时 , 会生成 虚函数表 Virtual Function Table , 简称 vtable ; C++ 编译器 编译 代码时 , 会自动为该类 添加 一个 vptr 指针 成员变量 , 该指针 会指向 虚函数表 ; 2、虚函数类与普通函数类对比 - 多出了 vptr 指针的大小 下面的代码中 , 定义了 2 个类 , 区别是 一个定义了 virtual 虚函数 ,
与C语言一致 没有继承的时候,存在虚函数则需要加上虚指针vptr(+4个字节),如果有多个也只需要加上一个,因为只有一个虚指针。 示例 #include <iostream> using namespace std; /** * 8=4(x)+4 (vptr) */ class A { public: int x; virtual void func() { } }; /** * 16=4(x)+4 (vptr)+4(y)+4(vbPtr) */ class B : virtual public sizeof(B):x的大小,y的大小,存在虚函数则有vptr;虚继承,则还存在指向虚基类指针。 sizeof(C):x的大小,y的大小,m的大小,存在虚函数则有vptr;虚继承,则还存在指向虚基类指针。而基类也是虚继承,基类中也有一个指向虚基类指针。
= (long*)(*tmp); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } 同理 ------" << endl; for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr[i]); } return 0 = (long*)(*tmp1); for (int i = 0; i < 3; i++) { printf("vptr[%d] : %p\n", i, vptr1[i]); } Func a = (Func)vptr1[0]; Func b = (Func)vptr1[1]; Func c = (Func)vptr1[2]; a(); b(); c(); Derive* p )vptr[0]; Func e = (Func)vptr[1]; Func f = (Func)vptr[2]; d(); e(); f(); return 0; }