抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Preface

多态Tree Easy Pieces:

  • 强制类型转换
  • 公共前缀
  • this指针偏移

这里讨论的cpp的多态是指父类虚函数的执行是由指针具体指向的对象而定。下面将围绕如下例子说明:

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
class Shape {
public:
virtual void area(){};
};

class Round: public Shape {
public:
void area() {
cout << "area Round" << endl;
}
};
class Cube: public Shape {
public:
int h;
void area() {
cout << "area Cube. h=" << h << endl;
}
};

int main (){
Shape *s;
Round r;
Cube c;
s = &r;
s->area();
s = &c;
s->area();
return 0;
}

输出:

1
2
area Round
area Cube

即父类的指针的虚函数s->area(),由具体指针具体指向的对象(Round r, Cube c)而定。

多态的关键就是编译器为父类和所有派生类都偷偷地, 在恰当的位置, 创建了一个虚函数表指针。而且虚函数表指针是类内存模型地第一位,所以实际类的内存模型如下:

1
2
3
4
5
class {
vtable;
field1;
field2;
}

然后虚函数表中保存所属类的函数指针,如Round类,那它的虚函数表就是:

1
2
3
vtable {
void(*area)(void* this); // 指向Round类的area函数
}

上述的关键点是:

  1. 类的内存模型:虚函数表在内存中的位置
  2. 虚函数表维护一系列函数指针,具体指向根据其所属类而定
  3. 类成员函数自动传入this指针

破解多态

强制类型转换

通过类的内存模型的描述,我们可以知道虚函数表的位于类的第一个成员。也许不是第一个,但一定是属于子类父类共有的前缀。理解子类父类共有的前缀这点很重要,因为一旦”前缀”相同就可以进行强制类型转换!

强制类型转换一个通俗的理解是:子类继承自,包含父类的内容,子类转换成父类就将属于父类的那段空间截取出来就行了。

1
2
3
4
5
6
7
8
9
10
 0 ┌──────────┐
│ │
│ father │ 0 ┌──────────┐
│ │ cast to father │ │
10 ├──────────┤ ───────────────► │ father │
│ │ [0,10] │ │
│ son │ 10 └──────────┘
│ │ \ \
20 └──────────┘ \ son \
20 \----------\

公共前缀:vtable

理解了强制类型转换的思想,那么如果我们想要在发生类型转换后能够访问到我们的”vtable”,那么这个”vtable”就应该放在,内存模型的公共前缀部分。

所以编译器会自动添加虚函数表成员后,所以强制类型转换的结果就是:

1
2
3
4
5
6
7
8
9
10
11
12
13

┌──────────┐ this*
0 │ vtable │
├──────────┤ ┌──────────┐ this*
│ │ 0 │ vtable │
│ father │ ├──────────┤
│ │ cast to father* │ │
10 ├──────────┤ ───────────────► │ father │
│ │ [0,10] │ │
│ son │ 10 └──────────┘
│ │ \ \
20 └──────────┘ \ son \
20 \----------\

这样是父类指针Shape *s是由哪个子类强制类型转换得来的,那他的vtable就会被该子类的vtable替换掉。从而实现”动态虚函数表”的效果。

this指针偏移

好的,我已经理解了多态如如何动态调用函数了,那这个动态调用的函数又是如何动态拿到正确的成员的?比如Shape *s = &c后,调用Cube类的area()函数并访问Cube类的h成员。

其实虚函数表是个指针,也就是说的其大小的确定的,而类成员函数由会自动传入this指针,所以将this指针加上vtable*指针占用的空间的偏移量后就转换为了普通访问类/结构体的问题了。

评论