C++内部如何利用虚函数实现多态机制

1. 引言

什么是虚函数?在某基类中声明为virtual 并在一个或多个派生类中被重新定义的成员函数叫虚函数。比如下面这个例子:

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
class A {
public:
virtual void func() { cout << "A func" << endl; }
virtual void funcA() { cout << "A funcA" << endl; }
void normalA() { cout << "normal A" << endl; };
public:
int a;
};

class B: public A {
public:
virtual void func() { cout << "B func" << endl; }
virtual void funcB() { cout << "B funcB" << endl; }
void normalB() { cout << "normal B" << endl; };
public:
int b;
};

int main () {
A* a = new A;
A* b = new B;
a->func(); // output will be "A func"
b->func(); // output will be "B func"
return 0;
}

上面是一个很常见的例子,基类A有一些函数,其中有些函数前面有virtual修饰,然后我们有一个派生类B,继承了A,然后重写了func这个函数。我们在main里调用了b->func(),虽然我一开始创建b的时候用的是A*,但程序还是跟我们需要的一样输出了跟B有关的信息”B func”,而不是”A func”,这个就是C++的多态机制。下面让我们来探讨一下C++的多态机制到底是怎么实现的。

2. 类是如何存放在内存中的(C++对象模型)

我们首先来看下面这个类定义:

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
A() {};
virtual ~A() {};
void normalA { cout << "normal A" << endl; }
virtual void func() { cout << "A func" << endl; }
static void printType() { cout << "class A" << endl; }
public:
int a;
static int b;
};

这个类定义包括数据成员(静态变量非静态变量)和函数成员(静态函数非静态函数virtual函数),那么这个类在内存中到底是什么样子呢?

2.1 简单对象模型


上图中类的每一个实例(或者说对象)维护一个包含所有成员的指针表,数据成员和函数成员不做明显的区分,每个成员占用相同的空间,每个slot保存的是成员的地址。

  • 优点:显然这个模型非常简单,降低了编译器设计的复杂度。
  • 缺点:空间和时间上的效率降低。访问一个成员是,先要用过维护的表的索引找到相应的函数或者数据成员,所以时间上性能有所损失,当成员很多的时候会非常明显。每一个object都维护这样一个表,显然空间消耗是一个对象中所有成员的个数x运行时class A的对象个数

2.2 表格驱动对象模型

这个模型将数据成员和函数成员分离开,引入了一个中间层,一个类对象只维护一个表,这个表有两个slot,分别指向数据成员表和函数成员表,这个模型比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。

  • 优点:采用两层索引机制,可以保证所有的对象具有相同的大小,如果我们在类中怎家一个非静态数据变量,而应用程序代码没有改变,这时只需要重新编译这个类就可以了。
  • 缺点:和2.1简单对象模型一样,空间和时间上的效率降低。

2.3 C++对象模型

真正的C++对象模型结合上面两者并做了优化,非静态数据成员直接放到对象内部,static数据成员,static和non-static 函数成员都独立出来,存储在对象之外。

对象内部除了非静态成员,还有一个虚函数表指针(vptr: virtual table pointer),该指针指向一个虚函数表,这个虚函数表存了所有的虚函数地址,以便函数调用。值得注意的是在虚函数表的最前面是一个type_Info指针,type_info用于支持runtime type identification (RTTI),里面的信息包括对象继承关系,对象本身的描述,多态中的dynamic_cast类型检查全靠这个type_info,vptr只有具有虚函数的对象会生成,vptr的设定和重置都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。
只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。

  • 优点:static和non-static函数独立出来后,调用的时候就不需要向前面两种模型一样每次通过索引查找,对于非静态成员也是直接访问,空间和存取时间都有不同程度上的优化;
  • 缺点:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。对于虚函数还是要通过一个vptr查找到,不可能直接找到

3. 动态多态的实现

3.1 单继承

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
class A {
public:
virtual void func() { cout << "A func" << endl; }
virtual void funcA() { cout << "A funcA" << endl; }
void normalA() { cout << "normal A" << endl; };
public:
int a;
};

class B: public A {
public:
virtual void func() { cout << "B func" << endl; }
virtual void funcB() { cout << "B funcB" << endl; }
void normalB() { cout << "normal B" << endl; };
public:
int b;
};

int main () {
A* a = new A;
A* b = new B;
a->func(); // output will be "A func"
b->func(); // output will be "B func"
return 0;
}


我们再来看一开始的这个例子,简化起见上图中只保留了vptr(比如实际上B对象还有一个内部变量int A::a),派生类B的虚函数表,第一个slot是关于B的type_info,然后由于重写了func,所以B::func替换了原本的A::func,没有被重写的A::funcA被保留。

3.2 多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
public:
virtual void func() { cout << "A func" << endl; }
virtual void funcA() { cout << "A funcA" << endl; }
};

class B {
public:
virtual void func() { cout << "B func" << endl; }
virtual void funcB() { cout << "B funcB" << endl; }
};

class C: public A, public B {
public:
virtual void func() { cout << "C func" << endl; }
virtual void funcC() { cout << "C funcC" << endl; }
};


当派生类C继承多个基类时,如上表,那么C将会有两个vptr:

  • 第一个虚函数表放的是C的type_info,重写过的C::func,继承自A但未被重写的A::funcA(),C自己的C::funcC
  • 第二个虚函数表是B的type_infoB::funcB::funcB,但由于B::func的函数名和A::func的一样都是func,在C中func是被重写过的,所以调用的时候实际会跳转到C::func
  • 如果C继承了三个类,那么会有三个vptr,右侧继承的第一个基类和C的所有虚函数会存放到第一个虚函数表,第二个基类的虚函数会存放到第二个虚函数表,第三个基类的虚函数会存放到第三个虚函数表。

4. Dynamic_cast

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 A {
public:
virtual void func() { cout << "A func" << endl; }
virtual void funcA() { cout << "A funcA" << endl; }
void normalA() { cout << "normal A" << endl; };
public:
int a;
};

class B: public A {
public:
virtual void func() { cout << "B func" << endl; }
virtual void funcB() { cout << "B funcB" << endl; }
void normalB() { cout << "normal B" << endl; };
public:
int b;
};

int main () {
A* a = new A;
A* b = new B;
b->func(); // output will be "B func"
// b->normalB(); // compilation error
// int tmp = b->b; // compilation error
B* bb = dynamic_cast<B*>(b);
bb->normalB();
int tmp = bb->b;

B* ba = dynamic_cast<B*>(a); // ba = NULL
return 0;
}

虚函数解决的只是虚函数自己的多态机制,像上面这个例子,在dynamic_cast之前我们直接调用b->normalB()或者b->b编译出错,说没有找到相应的函数或者变量,原因就是因为我们一开始用的是A*而非B*,如果要正常调用除了一开始直接指定B* b = new B外我们还可以用dynamic_cast

1
dynamic_cast <type-id> (expression)

该运算符把expression转换成type-id类型的对象。type-id必须是类的指针、类的引用或者void*,如果type-id是类指针类型,那么expression也必须是一个指针。dynamic_cast运算符可以在执行期决定真正的类型,dynamic_cast具有类型检查的功能,比static_cast更安全,B* ba = dynamic_cast<B*>(a);这里的ba转化后就是NULL,因为在dynamic_cast的时候,会检查a这个对象的vptr最前面所指向的type_info,他发现A* a不能转化成B*所以返回了NULL,而static_cast则没有检查,是强制类型转换。
还有一个B要有虚函数,否则会编译出错;static_cast则没有这个限制

5. 其他

5.1 用虚析构函数是一个好的习惯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A() { cout << "A constructor" << endl; }
~A() { cout << "A deconstructor" << endl; }
};

class B: public A {
public:
B() { cout << "B constructor" << endl; }
~B() { cout << "B deconstructor" << endl; }
};

int main()
{
A *b = new B;
delete b;
return 0;
}

上面的例子析构函数不是虚函数,程序的输出如下:

1
2
3
A constructor
B constructor
A deconstructor

这里会有一些问题,如果class B有很多内部变量,那么在析构的时候由于没有调用B的析构函数,会导致B中有些对象没有被释放掉,从而出现内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A() { cout << "A constructor" << endl; }
virtual ~A() { cout << "A deconstructor" << endl; }
};

class B: public A {
public:
B() { cout << "B constructor" << endl; }
virtual ~B() { cout << "B deconstructor" << endl; }
};

int main()
{
A *b = new B;
delete b;
return 0;
}

那么我们修改一下,在A和B的析构函数前面都加上virtual,此时我们可以看到如下信息,这样就不会有内存泄露的隐患了。

1
2
3
4
A constructor
B constructor
B deconstructor
A deconstructor

5.2 RTTI

5.3 virtual base class