C++多态: 运行期多态与编译期多态

运行期多态

实例

运行期多态是关于面向对象的。

当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

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);
}

上面代码运行结果为:

1
2
Cat
Dog

函数 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 reportSingleClassLayout类名 文件名
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])(); // B::vf1
((vf)vftable[1])(); // B::vf2
((vf)vftable[2])(); // B::vf3

输出为

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";

输出相同,bb1 都指向了同一张虚表。

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";

AnimalCatDog 的虚函数表位置各不相同,函数的地址也各不相同。

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() {
std::cout << "Cat\n";
}*/
};

void speak(Animal* p) {
p->speak();
}

int main() {
/*Cat p1;
Dog p2;
speak(&p1);
speak(&p2);*/
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

CatAnimal 都指向了同一个函数,但是这三个类都有自己的虚函数表

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
// Cat 的 speak 没有重写。
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

同一个类还是共用一张虚函数表,此时的内存结构为:

虚函数表的总结

  1. 同一个类只会创建一张虚函数表。
  2. 存在继承关系时,派生类没有重写虚函数,虚函数表中的指针指向基类的函数。重写后,会指向一个新的函数。

编译期多态

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 并不是很有用。

当需要返回一个 指向 thisshared_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