C++虚函数笔记(2)

本文转载自CSDN C++虚函数表解析

虚函数

虚函数就是用virtual来修饰的函数。虚函数是实现C++多态的基础。

虚函数表

每个类都会为自己类的虚函数创建一个表,来存放类内部的虚函数成员。

在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

实例

1
2
3
4
5
6
7
8
9
10

class Base {
public:
virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};

按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef void(*Fun)(void);

Base b;

Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;

cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;

// Invoke the first virtual function

pFun = (Fun)*((int*)*(int*)(&b));

pFun();
实际运行经果如下:

虚函数表地址:0012FED4

虚函数表 — 第一个函数地址:0044F148

Base::f

通过这个示例,我们可以看到,我们可以通过强行把&b转成int ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:

1
2
3
(Fun)*((int*)*(int*)(&b)+0);  // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
示例图如下所示:

pic

在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值如果是1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

一般继承

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

pic

对于实例:Derive d; 的虚函数表如下:
pic

当派生类覆盖了基类的函数f()时,实例的虚函数表则是这样:
pic

这样,我们就可以看到对于下面这样的程序,

1
2
3

Base *b = new Derive();
b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承

假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

pic

对于子类实例中的虚函数表,是下面这个样子:

pic

我们可以看到:

  • 每个父类都有自己的虚表。

  • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

当有函数覆盖时,即重载了f(),实例的虚函数表:

pic

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

C++虚函数笔记(1)

C++虚函数简单介绍

如果派生类在继承了基类后直接重写了基类的某个方法,当使用基类指针指向派生类实例时,调用的方法其实是调用的父类的方法。要实现多态,则需要使用虚函数。(动态多态性)

内存泄露

如果同样是上面的情况,需要释放两个指针指向的空间时,则会造成内存泄漏,因为在默认情况下调用的析构函数是基类的析构函数,而没有调用子类的析构函数。

1
2
3
4
5
6

Shape *shape1=new Circle(4.0);
Shape *shape2=new Rect(3.0,5.0);

delete shape1;
delete shape2;

如果在基类的析构函数声明虚函数,这样基类指针指向的是哪个对象,销毁的时候派生类的析构函数也会执行,再执行基类的析构函数。

C变量存储

  • 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

  • 堆,就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。

  • 自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。

  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)