目录
问:sizeof(b) 是多少?
- #include
- 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
- 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
- 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,所以虚函数的重写也叫作覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法上的叫法,覆盖是原理层的叫法。
- #include
- 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 中存的虚函数地址
- // 由此可以看出满足多态的函数调用,不是在编译时确定的,而是运行起来后去对象中找的。
- #include
- 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);
- }

- #include
- 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
- 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;
- }