运行期多态
实例
运行期多态是关于面向对象的。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Animal { public: virtual void speak() { std::cout << "Animal\n"; } };
class Dog : public Animal { public: void speak() { std::cout << "Dog\n"; } };
class Cat : public Animal { public: void speak() { std::cout << "Cat\n"; } };
void speak(Animal* p) { p->speak(); }
int main() { Cat p1; Dog p2; speak(&p1); speak(&p2); }
|
上面代码运行结果为:
函数 speak
中基类指针 Animal *p
没有调用自己的 speak
函数, 而是调用实参对象的 speak
函数。
普通函数并不会存放于类中,但是虚函数会以虚函数表指针的形式存放与类中。
即使派生类指针转换为了基类指针,指针指向的内容并没有改变,其虚函数表还是指向原来的位置,因此在调用虚函数时,通过虚函数表来查找该调用哪个函数。
多态实现的原理
虚函数表
对类使用 sizeof
是不显示类中普通函数的大小的,但是虚函数是可以被显示的,不管有多少个虚函数,它们的大小都是 8 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class B { int a{}; int b{}; public: virtual void vf1() { std::cout << "virtual function 1\n"; } virtual void vf2() { std::cout << "virtual function 2\n"; } virtual void vf3() { std::cout << "virtual function 3\n"; } };
|
sizeof(B)
是等于 16 (32 位操作系统是 12)。
使用 MSVC
工具链查看对象模型:
1 2
| cl /d1 reportSingleClassLayoutB main.cpp
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| class B size(16): +--- 0 | {vfptr} 8 | a 12 | b +---
B::$vftable@: | &B_meta | 0 0 | &B::vf1 1 | &B::vf2 2 | &B::vf3
|
结果很清晰,下面表示的是虚函数表的结构,虚函数表以指针形式存放在类中, 虚函数表中存放的也是指针。
尝试使用指针调用虚函数。
1 2 3 4 5 6 7 8
| using i64 = int64_t; using vf = void(*)(); B b{}; i64 *vfptr = (i64*)&b; i64* vftable = (i64*)*vfptr; ((vf)vftable[0])(); ((vf)vftable[1])(); ((vf)vftable[2])();
|
输出为
1 2 3
| virtual function 1 virtual function 2 virtual function 3
|
是否会为每一个对象都创建一个虚表 ?
1 2 3 4 5 6
| using i64 = int64_t; using vf = void(*)(); B b{}, b1{}; i64 *vfptr = (i64*)&b; std::cout << "b vfptr = " << *vfptr << "\n"; std::cout << "b1 vfptr = " << *((i64*)&b1) << "\n";
|
输出相同,b
和 b1
都指向了同一张虚表。
1 2
| b vfptr = 140697380241528 b1 vfptr = 140697380241528
|
单继承时的虚函数表
查看实例代码中的虚函数表的内存地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| using i64 = int64_t; using vf = void(*)(); Animal p1; Cat p2; Dog p3; i64 *p1_vfptr = (i64*)&p1; i64 *p2_vfptr = (i64*)&p2; i64 *p3_vfptr = (i64*)&p3; std::cout << "Animal vtable address = " << *p1_vfptr << "\n"; std::cout << "Cat vtable address = " << *p2_vfptr << "\n"; std::cout << "Dog vtable address = " << *p3_vfptr << "\n"; vf p1_speak_ptr = (vf)((i64*)*p1_vfptr)[0]; vf p2_speak_ptr = (vf)((i64*)*p2_vfptr)[0]; vf p3_speak_ptr = (vf)((i64*)*p3_vfptr)[0]; std::cout << "Animal speak address = " << p1_speak_ptr << "\n"; std::cout << "Cat speak address = " << p2_speak_ptr << "\n"; std::cout << "Dog speak address = " << p3_speak_ptr << "\n";
|
Animal
、Cat
和 Dog
的虚函数表位置各不相同,函数的地址也各不相同。
1 2 3 4 5 6
| Animal vtable address = 140702687674488 Cat vtable address = 140702687673376 Dog vtable address = 140702687674176 Animal speak address = 00007FF7E5B71555 Cat speak address = 00007FF7E5B71564 Dog speak address = 00007FF7E5B71569
|
内存结构为:
但是这里使用了重写,重写会对内存结构产生影响吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class Animal { public: virtual void speak() { std::cout << "Animal\n"; } };
class Dog : public Animal { public: void speak() { std::cout << "Dog\n"; } };
class Cat : public Animal { public:
};
void speak(Animal* p) { p->speak(); }
int main() {
using i64 = int64_t; using vf = void(*)(); Animal p1; Cat p2; Dog p3; i64 *p1_vfptr = (i64*)&p1; i64 *p2_vfptr = (i64*)&p2; i64 *p3_vfptr = (i64*)&p3; std::cout << "Animal vtable address = " << *p1_vfptr << "\n"; std::cout << "Cat vtable address = " << *p2_vfptr << "\n"; std::cout << "Dog vtable address = " << *p3_vfptr << "\n"; vf p1_speak_ptr = (vf)((i64*)*p1_vfptr)[0]; vf p2_speak_ptr = (vf)((i64*)*p2_vfptr)[0]; vf p3_speak_ptr = (vf)((i64*)*p3_vfptr)[0]; std::cout << "Animal speak address = " << p1_speak_ptr << "\n"; std::cout << "Cat speak address = " << p2_speak_ptr << "\n"; std::cout << "Dog speak address = " << p3_speak_ptr << "\n"; }
|
1 2 3 4 5 6
| Animal vtable address = 140700141011064 Cat vtable address = 140700141009952 Dog vtable address = 140700141010752 Animal speak address = 00007FF74DEC1555 Cat speak address = 00007FF74DEC1555 Dog speak address = 00007FF74DEC1569
|
Cat
与 Animal
都指向了同一个函数,但是这三个类都有自己的虚函数表。
Cat
的对象模型为:
1 2 3 4 5 6 7 8 9 10 11
| class Cat size(8): +--- 0 | +--- (base class Animal) 0 | | {vfptr} | +--- +---
Cat::$vftable@: | &Cat_meta | 0 0 | &Animal::speak
|
此时的内存模型为:
创建多个对象时的内存结构会发生改变吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| using i64 = int64_t; using vf = void(*)(); Animal p1; Cat p2; Dog p3; Animal p11; Cat p22; Dog p33; i64 *p1_vfptr = (i64*)&p1; i64 *p2_vfptr = (i64*)&p2; i64 *p3_vfptr = (i64*)&p3; std::cout << "Animal p1 vtable address = " << *p1_vfptr << "\n"; std::cout << "Cat p2 vtable address = " << *p2_vfptr << "\n"; std::cout << "Dog p3 vtable address = " << *p3_vfptr << "\n"; vf p1_speak_ptr = (vf)((i64*)*p1_vfptr)[0]; vf p2_speak_ptr = (vf)((i64*)*p2_vfptr)[0]; vf p3_speak_ptr = (vf)((i64*)*p3_vfptr)[0]; std::cout << "Animal p1 speak address = " << p1_speak_ptr << "\n"; std::cout << "Cat p2 speak address = " << p2_speak_ptr << "\n"; std::cout << "Dog p3 speak address = " << p3_speak_ptr << "\n"; i64* p11_vfptr = (i64*)&p11; i64* p22_vfptr = (i64*)&p22; i64* p33_vfptr = (i64*)&p33; std::cout << "Animal p11 vtable address = " << *p11_vfptr << "\n"; std::cout << "Cat p22 vtable address = " << *p22_vfptr << "\n"; std::cout << "Dog p33 vtable address = " << *p33_vfptr << "\n"; vf p11_speak_ptr = (vf)((i64*)*p11_vfptr)[0]; vf p22_speak_ptr = (vf)((i64*)*p22_vfptr)[0]; vf p33_speak_ptr = (vf)((i64*)*p33_vfptr)[0]; std::cout << "Animal p11 speak address = " << p11_speak_ptr << "\n"; std::cout << "Cat p22 speak address = " << p22_speak_ptr << "\n"; std::cout << "Dog p33 speak address = " << p33_speak_ptr << "\n";
|
1 2 3 4 5 6 7 8 9 10 11 12
| Animal p1 vtable address = 140702695145024 Cat p2 vtable address = 140702695145096 Dog p3 vtable address = 140702695145064 Animal p1 speak address = 00007FF7E629123A Cat p2 speak address = 00007FF7E629123A Dog p3 speak address = 00007FF7E62910B4 Animal p11 vtable address = 140702695145024 Cat p22 vtable address = 140702695145096 Dog p33 vtable address = 140702695145064 Animal p11 speak address = 00007FF7E629123A Cat p22 speak address = 00007FF7E629123A Dog p33 speak address = 00007FF7E62910B4
|
同一个类还是共用一张虚函数表,此时的内存结构为:
虚函数表的总结
- 同一个类只会创建一张虚函数表。
- 存在继承关系时,派生类没有重写虚函数,虚函数表中的指针指向基类的函数。重写后,会指向一个新的函数。
编译期多态
CRTP 全称是curious recurring template pattern, 可以用来在基类暴露接口且派生类实现对应接口时实现“编译期多态”。
之前的代码可以被更改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| template <typename T> class Animal { public: void speak() { static_cast<T*>(this)->speak(); } };
class Dog : public Animal<Dog> { public: void speak() { std::cout << "Dog\n"; } };
class Cat : public Animal<Cat> { public: void speak() { std::cout << "Cat\n"; } };
template <typename T> void speak(Animal<T> *p) { p->speak(); }
int main() { Cat p1{}; Dog p2{}; speak(&p1); speak(&p2); }
|
派生类必须要实现这个函数才行,看起来 CRTP 并不是很有用。
当需要返回一个 指向 this
的 shared_ptr
时, 才应该使用 CRTP 。
1 2 3 4 5 6 7 8 9 10
| struct A { std::shared_ptr<A> get() { return std::shared_ptr<A>(this); } };
int main() { std::shared_ptr<A> a = std::make_shared<A>(); auto b = a->get(); }
|
运行结果为:
1
| double free or corruption (out)
|
多次释放了 A
, 通过使用 use_count()
查看引用计数个数发现,计数并没有增加,仍为 1,但是有两个指针指向了同一个对象。
将类改为 :
1 2 3 4 5
| struct A : std::enable_shared_from_this<A> { std::shared_ptr<A> get() { return shared_from_this(); } };
|
这时就可以正常运行了。
参考
CRTP cppreference
std::enable_shared_from_this cppreference