目录
一、虚函数表和多态的原理
1.1 - 虚函数表
1.2 - 多态的原理
二、单继承和多继承关系中的虚函数表
2.1 - 单继承关系中的虚函数表
2.2 - 多继承关系中的虚函数表
三、纯虚函数和抽象类
一、虚函数表和多态的原理
1.1 - 虚函数表
-
问:
sizeof(b)
是多少?#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1()" << endl; } virtual void func2() { cout << "Base::func2()" << endl; } void func3() { cout << "Base::func3()" << endl; } protected: int _i = 1; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }
通过调试可以发现,在 b 对象内存模型中,除了
_i
成员,还有一个名为_vfptr
的成员,它是虚函数表指针,所以sizeof(b)
是 8 或 16 字节。一个含有虚函数的类对象至少有一个指向虚函数表的指针,虚函数表本质上是一个存放虚函数地址的函数指针数组,一般情况下在这个数组的最后面还放了一个
nullptr
。虚函数表可以简称为虚表。因为 func3 不是虚函数,所以没有放进虚表中。
-
提一个很容易混淆的问题:虚函数存在哪里?虚表又存在哪里?虚函数和普通函数一样,都是存在代码段的;而虚表存在哪里可以通过以下代码得知:
#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1()" << endl; } virtual void func2() { cout << "Base::func2()" << endl; } void func3() { cout << "Base::func3()" << endl; } protected: int _i = 1; }; typedef void(*VFPTR)(); int main() { int m = 0; printf("栈:%p\n", &m); int* p1 = new int; printf("堆:%p\n", p1); static int n = 0; printf("静态区:%p\n", &n); const char* str = "abcdef"; printf("常量区:%p\n", str); Base b; // 通过对象的地址获取虚函数表的地址 VFPTR* p2 = (VFPTR*)*(int*)&b; printf("虚表:%p\n", p2); return 0; }
根据输出结果,我们有理由相信虚表也是存在代码段的。
-
让派生类 Derive 继承自 Base,然后在派生类中重写基类虚函数 func1:
#include <iostream> using namespace std; class Base { public: virtual void func1() { cout << "Base::func1()" << endl; } virtual void func2() { cout << "Base::func2()" << endl; } void func3() { cout << "Base::func3()" << endl; } protected: int _i = 1; }; class Derive : public Base { public: virtual void func1() { cout << "Derived::func1()" << endl; } protected: int _j = 2; }; int main() { Base b; Derive d; return 0; }
因为在派生类中重写了基类的虚函数 func1,所以基类对象 b 和派生类对象 d 的虚表是不一样的。
d 的虚表中存的是重写的 Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法上的叫法,覆盖是原理层的叫法。
1.2 - 多态的原理
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
void func3() { cout << "Base::func3()" << endl; }
protected:
int _i = 1;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
protected:
int _j = 2;
};
int main()
{
Base b;
Base* pb = &b;
pb->func1(); // Base::func1()
Derive d;
pb = &d;
pb->func1(); // Derive::func1()
return 0;
}
当基类指针
pb
指向基类对象 b 时,pb->func1();
就是在 b 的虚表中找到虚函数 Base::func1。当基类指针
pb
指向派生类对象 d 时,pb->func1();
就是在 d 的虚表中找到虚函数 Derive::func1。这样就让基类指针表现出了多种形态。
注意:不满足多态的函数调用是编译时确定好的,满足多态的函数调用是运行时去对象中找的:
Base b;
b.func1();
// 00195182 lea ecx,[b]
// 00195185 call Person::func1 (01914F6h)
// 汇编代码分析:
// 虽然 func1 是虚函数,但是 b 是对象,不满足多态的条件,所以这里是普通函数的调用,
// 编译时就确定好了函数的地址,直接 call。
Base* pb = &b;
pb->func1();
// 注意:不相关的汇编代码被省去了
// 001940DE mov eax,dword ptr [pb]
// 001940E1 mov edx,dword ptr [eax]
// 00B823EE mov eax,dword ptr [edx]
// 001940EA call eax
// 汇编代码分析:
// 1、pb 中存的是 b 对象的地址,将 pb 移动到 eax 中
// 2、[eax] 就是取 eax 值指向的内容,相当于把 b 对象中的虚表指针移动到 edx
// 3、[edx] 就是取 edx 值指向的内容,相当于把虚表中第一个虚函数的地址移动到 eax
// 4、call eax 中存的虚函数地址
// 由此可以看出满足多态的函数调用,不是在编译时确定的,而是运行起来后去对象中找的。
二、单继承和多继承关系中的虚函数表
2.1 - 单继承关系中的虚函数表
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
protected:
int _i = 1;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
virtual void func4() { cout << "Derive::func4()" << endl; }
protected:
int _j = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
在 d 的虚表中,我们看不到虚函数 func3 和 func4,这可能是监视窗口故意隐藏了这两个函数,也可能是一个小 bug,我们可以通过以下代码进行验证:
typedef void(*VFPTR)();
void PrintVftable(VFPTR vftable[])
{
for (size_t i = 0; vftable[i] != nullptr; ++i)
{
printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
vftable[i]();
}
cout << endl;
}
void test()
{
Base b;
VFPTR* p1 = (VFPTR*)*(int*)&b;
PrintVftable(p1);
Derive d;
VFPTR* p2 = (VFPTR*)*(int*)&d;
PrintVftable(p2);
}
2.2 - 多继承关系中的虚函数表
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
protected:
int _i1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1()" << endl; }
virtual void func2() { cout << "Base2::func2()" << endl; }
protected:
int _i2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
protected:
int _j;
};
typedef void(*VFPTR)();
void PrintVftable(VFPTR vftable[])
{
for (size_t i = 0; vftable[i] != nullptr; ++i)
{
printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
vftable[i]();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* p1 = (VFPTR*)*(int*)&d;
PrintVftable(p1);
// VFPTR* p2 = (VFPTR*)*(int*)((char*)&d + sizeof(Base1));
// PrintVftable(p2)
// 或者:
Base2* p2 = &d;
PrintVftable((VFPTR*)*(int*)p2);
return 0;
}
-
派生类中的虚函数 func3 放在第一个继承自基类部分的虚函数表中。
-
假设有以下场景:
Derive d; Base1* p1 = &d; p1->func1(); Base2* p2 = &d; p2->func1();
首先要确定的是:
所以在语句
p2->func1();
中,需要修正 this 指针:这也是为什么在 d 的两个虚表中,重写的虚函数 func1 的地址不一样。
三、纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在某些情况下,在基类中不能对虚函数给出有意义的实现,就可以把它声明为纯虚函数。纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给派生类去做。具体语法:virtual 返回值类型 函数名(参数列表) = 0;
。
含有纯虚函数的类被称为抽象类(或接口类),不能实例化对象,但可以创建指针和引用。
派生类必须重写抽象类中的纯虚函数,否则也属于抽象类。
#include <iostream>
using namespace std;
class Car
{
public:
virtual void Drive() = 0;
};
class AITO : public Car
{
public:
virtual void Drive() { cout << "Intelligent" << endl; }
};
class AVATR : public Car
{
public:
virtual void Drive() { cout << "Comfortable" << endl; }
};
void func1(Car* p) { p->Drive(); }
void func2(Car& c) { c.Drive(); }
int main()
{
AITO aito;
AVATR avatr;
func1(&aito); // Intelligent
func1(&avatr); // Comfortable
func2(aito); // Intelligent
func2(avatr); // Comfortable
return 0;
}