2、 虚函数详解
2.1 虚函数表指针的位置
- 虚函数表指针位于对象内存的起始位置
// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <stdio.h>
class A
{
public:
int m_a;
virtual void testfunc()
{
}
private:
};
using namespace std;
int main()
{
std::cout << "Hello World!\n";
A a;
std::cout << "A 的占内存字节大小:" << sizeof(A) << std::endl;
char* pl = reinterpret_cast<char* > (&a); //类型转换,这属于硬转,a是对象首地址
char* p2 = reinterpret_cast< char* > (&(a.m_a));//
if (pl == p2) //说明a.m_a和a位置相同,则成员变量i在a对象内存的上面位置,那么虚函数表//指针在下面位置
{
cout << "虚函数表指针位于对象内存的末尾" << endl;
}
else
{
cout<<"虚函数表指针位于对象内存的开头"<<endl;//本条件会成立
}
}
2.2 虚函数如何手动调用
// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <stdio.h>
using namespace std;
class Base
{
public:
virtual void f1()
{
cout << "Base::f1()" << endl;
};
virtual void f2()
{
cout << "Base::f2()" << endl;
};
virtual void f3()
{
cout << "Base::f3()" << endl;
};
private:
};
class MyClass: public Base
{
public:
void f2()
{
cout << "MyClass::f2()" << endl;
}
private:
};
typedef void(*Func) (void);
int main()
{
std::cout << "Hello World!\n";
cout << "BASE LEN:" << sizeof(Base) << endl;
cout << "MyClass LEN:" << sizeof(MyClass) << endl;
Base* p0 = new Base();
long* pvptr0 = (long*)p0; //指向对象p0的指针转成1ong*型。大家注意,目前p0对象里只有虚函数表指针
long* vptr0 = (long*)(*pvptr0); //(*pvptr)表示pvptr指向的对象,也就是Base 对象本身这个对象4字节, 这个4字节是虚函数表地址。
for (int i=0; i<4;i++ )
{
printf("vptr0[%d] = 0x: %p\r\n", i, vptr0[i]);
}
Func f00 = (Func)vptr0[0]; //虚函数表的第一个
Func f01 = (Func)vptr0[1]; //虚函数表的第二个
Func f02 = (Func)vptr0[2]; //虚函数表的第三个
Func f03 = (Func)vptr0[3]; //虚函数表的第三个
f00();
f01();
f02();
//f3(); //异常
std::cout << "****************************************\n";
MyClass* p = new MyClass();
long* pvptr = (long*)p; //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
long* vptr = (long*)(*pvptr); //(*pvptr)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。
for (int i = 0; i < 4; i++)
{
printf("vptr[%d] = 0x: %p\r\n", i, vptr[i]);
}
Func f0 = (Func)vptr[0]; //虚函数表的第一个
Func f1 = (Func)vptr[1]; //虚函数表的第二个
Func f2 = (Func)vptr[2]; //虚函数表的第三个
Func f3 = (Func)vptr[3]; //虚函数表的第三个
f0();
f1();
f2();
}
结论点:
- 包含虚函数的类才有虚函数表,同属一个类的对象共享这个虚函数表,但是每一个对象都有自己的指向虚函数表的指针,指针不同但是指针指向的地址是相同的
- 父类中存在虚函数,子类一定有虚函数表,不管子类是否重写,不管子类是否去掉重写的 virtual
- 如果子类中 完全没有新的虚函数, 可以认为父类和子类的虚函数表的内容相同,是内容相同,两个虚函数表的内存位置是不同的, 即两个内容完全相同的表存在不同的位置
- 超出虚函数表的内存地址未知的 就是和空指针类似
// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <stdio.h>
using namespace std;
class Base
{
public:
virtual void f1()
{
cout << "Base::f1()" << endl;
};
virtual void f2()
{
cout << "Base::f2()" << endl;
};
virtual void f3()
{
cout << "Base::f3()" << endl;
};
private:
};
class MyClass: public Base
{
public:
void f2()
{
cout << "MyClass::f2()" << endl;
}
private:
};
typedef void(*Func) (void);
int main()
{
std::cout << "Hello World!\n";
cout << "BASE LEN:" << sizeof(Base) << endl;
cout << "MyClass LEN:" << sizeof(MyClass) << endl;
MyClass myclass;
long* pvptr_v = (long*)(&myclass); //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
long* vptr_v = (long*)(*pvptr_v); //(*pvptr_v)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。
for (int i = 0; i < 4; i++)
{
printf("vptr_v[%d] = 0x: %p\r\n", i, vptr_v[i]);
}
Func v0 = (Func)vptr_v[0]; //虚函数表的第一个
Func v1 = (Func)vptr_v[1]; //虚函数表的第二个
Func v2 = (Func)vptr_v[2]; //虚函数表的第三个
Func v3 = (Func)vptr_v[3]; //虚函数表的第三个
v0();
v1();
v2();
MyClass myclass2 = myclass;
long* pvptr_v2 = (long*)(&myclass2);
long* vptr_v2 = (long*)(*pvptr_v2);
for (int i = 0; i < 4; i++)
{
printf("vptr_v2[%d] = 0x: %p\r\n", i, vptr_v2[i]);
}
Base base = myclass;
long* pvptr_vbase = (long*)(&base);
long* vptr_vbase = (long*)(*pvptr_vbase);
for (int i = 0; i < 4; i++)
{
printf("vptr_vbase[%d] = 0x: %p\r\n", i, vptr_vbase[i]);
}
std::cout << "************** *************************\n";
}
- 83行 Base base = myclass; 复制后虚函数的指针变化,①生成一个base的对象 ②使用myclass 初始化base对象,但是并不覆盖base的虚函数指针
2.3 多重继承的虚函数表详解
// vfun2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include <iostream>
#include <stdio.h>
using namespace std;
class Base1
{
public:
virtual void f1()
{
cout << "Base1::f1()" << endl;
};
virtual void f2()
{
cout << "Base1::f2()" << endl;
};
//virtual void f3()
//{
// cout << "Base1::f3()" << endl;
//};
private:
};
class Base2
{
public:
virtual void f4()
{
cout << "Base2::f4()" << endl;
};
virtual void f5()
{
cout << "Base2::f5()" << endl;
};
private:
};
class MyClass : public Base1,public Base2
{
public:
virtual void f1()
{
cout << "MyClass::f1()" << endl;
}
virtual void f5()
{
cout << "MyClass::f5()" << endl;
}
virtual void mf1()
{
cout << "MyClass::mf1()" << endl;
}
virtual void mf2()
{
cout << "MyClass::mf2()" << endl;
}
virtual void mf3()
{
cout << "MyClass::mf3()" << endl;
}
private:
};
typedef void(*Func) (void);
int main()
{
std::cout << "Hello World!\n";
cout << "BASE1 LEN:" << sizeof(Base1) << endl;
cout << "BASE2 LEN:" << sizeof(Base2) << endl;
cout << "MyClass LEN:" << sizeof(MyClass) << endl;
MyClass myclass;
Base1& b1 = myclass;
Base2& b2 = myclass;
MyClass& p = myclass;
long* pvptr_myclass = (long*)(&myclass); //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
long* vptr_myclass = (long*)(*pvptr_myclass); //(*pvptr_v)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。
for (int i = 0; i < 7; i++)
{
printf("vptr_myclass[%d] = 0x: %p\r\n", i, vptr_myclass[i]);
}
Func v0 = (Func)vptr_myclass[0]; //虚函数表的第一个
Func v1 = (Func)vptr_myclass[1]; //虚函数表的第二个
Func v2 = (Func)vptr_myclass[2]; //虚函数表的第三个
Func v3 = (Func)vptr_myclass[3]; //虚函数表的第三个
Func v4 = (Func)vptr_myclass[4]; //虚函数表的第三个
Func v5 = (Func)vptr_myclass[5]; //虚函数表的第三个
Func v6 = (Func)vptr_myclass[6]; //虚函数表的第三个
Func v7 = (Func)vptr_myclass[7]; //虚函数表的第三个
v0();
v1();
v2();
v3();
v4();
//v5();
//v6();
//v7();
cout << "******************" << endl;
long* pvptr_2 = pvptr_myclass + 1; //这里是往后移动是个字节幺
long* vptr_2 = (long*)(*pvptr_2);
for (int i = 0; i < 3; i++)
{
printf("vptr_2[%d] = 0x: %p\r\n", i, vptr_2[i]);
}
Func f0 = (Func)vptr_2[0]; //虚函数表的第一个
Func f1 = (Func)vptr_2[1]; //虚函数表的第二个
Func f2 = (Func)vptr_2[2]; //虚函数表的第三个
f0();
f1();
//f2();
cout << "*********22222*********" << endl;
b1.f1();
b1.f2();
b2.f4();
p.mf1();
p.mf2();
p.mf3();
cout << "*********22222*********" << endl;
myclass.f1();
myclass.f5();
myclass.mf1();
}
上面的虚函数的内存分布大致如下:
- 一个对象继承多个多个基类,则可能出现多个虚函数表指针 是虚函数表指针,不是虚函数表
- 在多重继承的情况下,各个基类的vptr是按照继承顺序依次放到空间的, 并且子类的虚函数表和第一个基类共用一个vptr。
- 子类中的虚函数会覆盖父类中的同名函数
可以工具查看类的内存分布:
(1)区分cl和d1,一个是小写字母1,一个是数字1,两者看上去比较像。
(2)reportSingleClassLayout后面跟的是要查看的类名,
(3)最后面跟的是要查看的类所在的.cpp源文件名。
使用VS 的命令行可以查看
cl /d1 reportSingleClassLayout类名 xxxx.cpp
注意上图中的this adjustor ** 这个是this 指针在使用中的调整
- 虚函数表指针是什么时候创建的
虚函数表指针(vptr)是跟着对象走的,对象创建创建出来才有 虚函数表指针(vptr) 对有虚函数的类,在编译的时候编译器就会在构造函数中添加vptr的赋值语句。 - 虚函数表(vtbl)是什么时候创建的
编译器在编译的时候就为每个类确定好了对应虚函数表的内容,然后也是在编译期间在相应的类构造函数中添加给vptr赋值的语句,这样程序运行的时
候,当运行到生成类对象的代码时,会调用类的构造函数,执行到类的构造函数中的给vptr 赋值的语句时,这个类对象的vptr就有值了。
2.4、虚基类探讨
定义:在 C++ 中,虚基类(Virtual Base Class)是通过虚继承(Virtual Inheritance)来定义的。虚继承使用 virtual 关键字来确保在多重继承中只有一个基类的实例。这有助于解决钻石继承问题(Diamond Problem)。
2.4.1定义虚基类
虚基类的定义涉及到以下几个方面:
1.虚继承的语法:在继承基类时使用 virtual 关键字。
2.构造函数的调用顺序:虚基类的构造函数会在最开始被调用。
3.内存布局:虚基类的对象在派生类中只占用一次内存空间。
#include <iostream>
class Base {
public:
Base(int v) : value(v) {
std::cout << "Base constructor called with value: " << value << std::endl;
}
int value;
};
class Derived1 : virtual public Base {
public:
Derived1(int v) : Base(v), derived1Value(v * 2) {
std::cout << "Derived1 constructor called with value: " << derived1Value << std::endl;
}
int derived1Value;
};
class Derived2 : virtual public Base {
public:
Derived2(int v) : Base(v), derived2Value(v * 3) {
std::cout << "Derived2 constructor called with value: " << derived2Value << std::endl;
}
int derived2Value;
};
class FinalDerived : public Derived1, public Derived2 {
public:
FinalDerived(int v) : Base(v), Derived1(v), Derived2(v), finalDerivedValue(v * 4) {
std::cout << "FinalDerived constructor called with value: " << finalDerivedValue << std::endl;
}
int finalDerivedValue;
};
int main() {
FinalDerived obj(10);
return 0;
}
2.4.2虚基表的内存布局
假设 sizeof(int) 为 4 字节,sizeof(void*) 为 4字节,那么 FinalDerived 对象的内存布局可能如下:
深色版本FinalDerived 对象:
+----------------+----------------+----------------+----------------+----------------+----------------+
| Vbptr | derived1Value | derived2Value | finalDerivedValue | Base::value |
+----------------+----------------+----------------+----------------+----------------+----------------+
4 bytes 4 bytes 4 bytes 4 bytes 4 bytes
- Vbptr:指向虚基表的指针。
- derived1Value:Derived1 的成员变量。
- derived2Value:Derived2 的成员变量。
- finalDerivedValue:FinalDerived 的成员变量。
- Base::value:Base 的成员变量。
2.4.3虚基表的内容
假设虚基表的内容如下:
虚基表通常包含以下信息:
1.虚基类的偏移量:记录虚基类相对于派生类对象的偏移量。
2.虚基类的构造函数地址:用于在构造派生类对象时调用虚基类的构造函数。
3.虚基类的析构函数地址:用于在销毁派生类对象时调用虚基类的析构函数
Vbtable:
+----------------+----------------+----------------+----------------+
| Offset to Base | Constructor | Destructor | Other Entries |
+----------------+----------------+----------------+----------------+
8 bytes Function Pointer Function Pointer ...
- Offset to Base:记录 Base 类相对于 FinalDerived 对象的偏移量。
- Constructor:指向 Base 类的构造函数。
- Destructor:指向 Base 类的析构函数。
- Other Entries:可能包含其他辅助信息。
实测代码
// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Grand
{
public:
//int m_Grand;
private:
};
class A1: virtual public Grand
{
public:
private:
};
class A2 : virtual public Grand
{
public:
private:
};
class C1 : public A1,public A2
{
public:
private:
};
int main()
{
std::cout << "Hello World!\n";
cout << "Grand sizeof :" << sizeof(Grand) << endl;
cout << "A1 sizeof :" << sizeof(A1) << endl;
cout << "A2 sizeof :" << sizeof(A2) << endl;
cout << "C1 sizeof :" << sizeof(C1) << endl;
}
因为一个空类的大小为1,所有 grand大小为1, 但是后面A1、A2的为什么不是呢?是应为引入了虚基类表指针。
使用类的布局神器看一下 cl /d1 reportSingleClassLayout类名 xxxx.cpp
添加一个成员变量更容易看
// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Grand
{
public:
int m_Grand;
private:
};
class A1: virtual public Grand
{
public:
int m_a1;
private:
};
class A2 : virtual public Grand
{
public:
int m_a2;
private:
};
class C1 : public A1,public A2
{
public:
int m_C1;
private:
};
int main()
{
std::cout << "Hello World!\n";
cout << "Grand sizeof :" << sizeof(Grand) << endl;
cout << "A1 sizeof :" << sizeof(A1) << endl;
cout << "A2 sizeof :" << sizeof(A2) << endl;
cout << "C1 sizeof :" << sizeof(C1) << endl;
}
Grand的内存布局
A的内存布局
2.4.4 虚基类表内容详解(0~8字节)
// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Grand
{
public:
int m_Grand;
private:
};
class A1: virtual public Grand
{
public:
int m_a1;
private:
};
class A2 : virtual public Grand
{
public:
int m_a2;
private:
};
class C1 : public A1,public A2
{
public:
int m_C1;
private:
};
int main()
{
std::cout << "Hello World!\n";
cout << "Grand sizeof :" << sizeof(Grand) << endl;
cout << "A1 sizeof :" << sizeof(A1) << endl;
cout << "A2 sizeof :" << sizeof(A2) << endl;
cout << "C1 sizeof :" << sizeof(C1) << endl;
A1 a;
a.m_Grand = 10;
a.m_a1 = 16;
}
a.m_grand = 10 赋值
ECX 的值是8 和 00759D20 内存后面的直接一样 , 并且内存中也是在&a偏移8字节后内容变化。
并且虚基类表的5~ 8字节内容的含义是: 虚基类表指针 这个成员变量(vbptr)的首地址和虚基类子对象首地址之间的偏移量。
2.4、函数的静态绑定和动态绑定
2.4.1 静态绑定
在 C++ 中,静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的函数调用机制。它们决定了函数调用的解析时间和方式。下面是这两种绑定的详细解释和示例。
- 静态绑定(Static Binding)
静态绑定,也称为早期绑定(Early Binding),是指在编译时确定函数调用的具体实现。编译器在编译阶段就已经知道要调用哪个函数,因此可以在生成的机器码中直接插入函数地址。
特点
- 编译时解析:函数调用的解析在编译时完成。
- 性能较高:由于编译器可以直接生成调用特定函数的代码,因此执行效率较高。
- 不支持多态:静态绑定不支持运行时多态
- 动态绑定(Dynamic Binding)
动态绑定,也称为晚期绑定(Late Binding),是指在运行时确定函数调用的具体实现。编译器生成的代码在运行时通过虚函数表(Vtable)来查找并调用相应的函数。
特点
- 运行时解析:函数调用的解析在运行时完成。
- 支持多态:动态绑定支持运行时多态,即通过基类指针或引用调用派生类的函数。
- 性能较低:由于需要在运行时通过虚函数表查找函数地址,因此执行效率略低于静态绑定。
2.4.2 继承非虚函数注意事项
// classfunBugs.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Base1
{
public:
void f1()
{
cout << "Base1::f1()" << endl;
};
private:
};
class class1: public Base1
{
public:
void f1()
{
cout << "class1::f1()" << endl;
};
private:
};
int main()
{
std::cout << "Hello World!\n";
class1 mclass1;
class1* p = &mclass1;
p->f1();
Base1* pb = &mclass1;
pb->f1();
}
注意: 函数是静态绑定 直接执行的是基类的函数
// classfunBugs.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
class Base1
{
public:
void f1()
{
cout << "Base1::f1()" << endl;
};
virtual void vf1()
{
cout << "Base1::vf1()" << endl;
}
/// <summary>
/// 带有默认参数
/// </summary>
virtual void vf2(int value = 1)
{
cout << "Base1::vf2(),value:" <<value<< endl;
}
private:
};
class class1: public Base1
{
public:
void f1()
{
cout << "class1::f1()" << endl;
};
virtual void vf1()
{
cout << "class1::vf1()" << endl;
}
virtual void vf2(int value = 10)
{
cout << "class1::vf2(),value:" << value << endl;
}
private:
};
int main()
{
std::cout << "Hello World!\n";
class1 mclass1;
class1* p = &mclass1;
p->f1();
Base1* pb = &mclass1;
pb->f1();
cout << "!!!!!!!!!!!\r\n" << endl;
p->vf1();
pb->vf1();
p->vf2();
pb->vf2();
cout << "22222222\r\n" << endl;
Base1 base1;
pb = &base1;
pb->vf1();
pb->vf2();
}
虚函数时,函数调用时动态绑定,基类指针会调用到子类的函数,但是默认缺省值使用的缺失父类的。 原因是 默认参数值编译器是静态比绑定上去的。