关于继承,多态和虚函数

注明:此文转载并参考陈皓csdn博客http://blog.csdn.net/haoel/article/details/1948051#reply以及独酌逸醉的博文http://www.cnblogs.com/chinazhangjie/archive/2012/07/11/2586535.html,并且引用了网上其他一些资料,只供学习交流使用

引子:

什么是指针变量

指针变量,本质上是一个变量,只是它是存放地址的变量,指针的类型代表的是它所指向的变量的类型。因此就有了指向整型、字符型、浮点型等其它类型的指针,但实际上所有类型的指针变量存放的都是int型(对于16位编译系统,比如TC,int是2字节,对于32位编译系统,比如VC,GCC,int是4字节)的地址地址运算符&只能应用于内存中的对象,即变量和数组元素,它不能作用于表达式、常量和register类型的变量。因此从本质上不同类型的指针变量并没有区别(因为指针变量的类型为int型,因此指针变量只能存放地址。注意和指针指向对象的类型区分开),指针变量所存储的地址为指针所指向的对象的首地址

不同类型的指针变量之间的区别

我们都知道不同类型的指针变量指向不同类型的对象,这些指针变量结合指针运算符(*)就等价于指向的对象的值,但我们又知道所有的指针变量的类型都是一样的(都是int型)。到底声明不同类型的指针变量的背后是什么?其实声明不同类型的指针变量既是规定了该变量结合指针运算符时读取内存中的字节数,同样在指针移动和指针的运算时(加、减)在内存中移动的最小字节数。

查了下书,指针是这样定义的:“指向某种类型对象的复合数据类型”。“复合数据类型”这句说得很好,因为一般说道都会想到“就是地址”,说开无非是存着所指向对象的地址呗?但是如果单单说是一个存有地址的类型这显然不够称得上“复合类型”。

  不妨再加上一条:1.存着所指对象地址。2.存着所指对象类型和类型大小。虽然指针远没这么简单(并且一个指针变量4个字节存的仅仅是32位的地址,本篇为了给你一种感觉暂且说同时存了“类型和类型大小”,下篇会给出另外的解释),但是我们看看这两条都能做些什么。

  第一条应该没什么好解释的了,第二条有必要说说,因为看似很显然,但是可以明白好多事情。

  试想我们定义一个某类型指针p,后可以p++,p--,*p这些都与“指针记录了所指对象类型大小”有关,不然p++和p--  一次跳了多远?还有*p一次取了多长,都是由记录的类型大小决定的:如int *p=&i;那么++ 一次至少会跳int那么远即4个字节(连续存储下),*p一次也会从p所指向地址为起点取出int那么长(4字节)空间的二进制信息,并将这些二进制翻译成“指针内部存着的其所指向的类型”的类型。记住这种“感觉”:初始化指针就是:(1)记下所指对象的地址(2)同时记下所指类型和类型大小。(3)对一个指针解引用(*p)就是从指针所指地址为起点,读出所指类型大小那么大的空间的二进制,然后将二进制翻译成所指类型。(4)而++,——操作就是向前或向后跳所指类型那么大小的空间

下面强化一下。

[cpp]     view plaincopy
  1. int t = 65<<8;  
  2. char *p = (char*)&t+1;  
  3. cout<<(int)*p<<endl; 

  65左移了8位然后被给一个被赋值给整型t,然后定义了char*指针p,而后的强制转换就有些意思了,按照前面的感觉也就不难理解了:取了t的地址然后由于遇见(char*),p存储的类型原本应该是t的类型int,然而由于(char*)的出现p中存储的类型将变成char类型,大小也为char那么大即一个字节,而后+1,这时这一跳将因为目前对象是char类型,而只跳一个字节(8位),也就是说这时p将指向之前左移8位的65这个数所在内存那么下面的cout将如期望的一样输出65这个数。

  我们有了感觉之后似乎对指针间的强制类型转换也很清楚了:(1)转换后的指针中记录的地址依旧是转换前的地址(2)转换后的指针记下的所指类型和类型大小,为强制转换的类型和强制转换类型的大小。(3)*操作会依旧会以指针所指地址为起点,却取出转换后的指针所记录的类型大小那么大的空间的二进制,翻译成指针转换后的所指类型。(4)转换后的++,和--跳的距离以转换后的指针中存放的类型大小为依据

注:对于上述程序中 (char*)&t+1不是很明白,根据自己理解将变量t的地址(int型)强制转换成指向字符型的指针变量,可是我始终无法理解对一个地址值强制转换为一个指向char的指针,按照前面所述的话强制转换后实际该指针变量存储的还是int型,只是所指向的内容的类型是char,且后面的1也代表char字节大小,还是不太确定,留待解决????

  那么我们更有感觉了:只要指针间在“能转换的情况下”我们就可以转来转去,只要保证不对指针进行移位就可以保证指针所指向地址一直不变转换仅仅转的是里面存的类型和类型大小,只要我们最后能转回原来的类型还可以如同最初时一样按原类型操作它。

[cpp]     view plaincopy

  1. typedefstruct  
  2.    int a; 
  3. }test; 
  4.  
  5. test t; 
  6. t.a=100; 
  7.  
  8.  
  9. long* t1=(long*)&t; 
  10. char* t2=(char*)t1; 
  11. test * t3=(test*)t2; 
  12. cout<<t3->a<<endl;//output:100 

 转来转去又转回初始类型,如我们所愿的输出100。

  最后确认一下感觉。

[cpp]     view plaincopy

 
  1. int i=*(int *)"ABC"
  2. cout<<i<<endl;//output:4407873 

4407873是什么看不清楚吗?转成二进制10000110100001001000001,还看不清?补齐四个字节,再分割一下,00000000'01000011'01000010'01000001。不知道是什么了吗?对照下表:

二进制数        ASCII(也是%d输出的整型值)       字符

01000001                   65                               A

01000010                   66                               B

01000011                   67                               C

00000000                    0                               /0

又很有感觉了吧?对于上面的程序中原本ABC/0(字符串隐含/0下一篇会解释)是一坨二进制,生成后返回地址char*类型然后遇到 (int *)被转成了int*,就等于告诉程序:以后对待存ABC和/0这坨内存别再看成是char*的了,而是我int*的了,要读的话别读char*那样的一个字节了,要给我读满4个字节(int是4个字节),也就是后再遇到*解引用,取出4个字节的东西赋值给i,而i是int型,00000000'01000011'01000010'01000001这串二进制自然翻译成int型的即4407873。

  最后,忘了这一切吧,记住一句话“永远别玩指针!玩不好容易被指针玩”,以上的一切代码都不是好习惯,只是为了给你一种感觉,记住那句话,再记住感觉(下一篇还要用到这种感觉)就足够了。

 

 

1.单继承

对于单继承

2.C++虚函数及虚函数表解析

虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
纯虚函数(Pure Virtual Function):基类中没有实现体的虚函数称为纯虚函数(有纯虚函数的基类称为虚基类)。
C++  “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。通过动态赋值,实现调用不同的子类的成员函数(动态绑定)。正是因为这种机制,把析构函数声明为“虚函数”可以防止在内存泄露。

虚函数表(Virtual Table,V-Table):使用 V-Table 实现 C++ 的多态。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
编译器应该保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

(1)无继承时的虚函数表

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class base_class
 5 {
 6 public:
 7     virtual void v_func1()
 8     {
 9         cout << "This is base_class's v_func1()" << endl;
10     }
11     virtual void v_func2()
12     {
13         cout << "This is base_class's v_func2()" << endl;
14     }
15     virtual void v_func3()
16     {
17         cout << "This is base_class's v_func3()" << endl;
18     }
19 };
20 
21 int main()
22 {
23     // 查看 base_class 的虚函数表
24     base_class bc;
25     cout << "base_class first addr of vtable:        " << (int*)&bc << endl; // 虚函数表地址存在对象的前四个字节
26 
27 //    cout << "base_class content of vtable:        " << *(int*)&bc << endl;
28 //    cout << "        " << (int*)*(int*)&bc << endl;
29 
30     cout << "base_class addr of the first function: " << (int*)*(int*)&bc+0 << endl; // 指针运算看不懂?没关系,一会解释给你听
31     cout << "base_class addr of the second function:" << (int*)*(int*)&bc+1 << endl;
32     cout << "base_class addr of the third function: " << (int*)*(int*)&bc+2 << endl;
33     cout << "base_class flag of end:" << *((int*)*(int*)&bc+3) << endl;
34     
35     // 通过函数指针调用函数,验证正确性
36     typedef void(*func_pointer)(void);
37     func_pointer fp = NULL;
38     fp = (func_pointer)*((int*)*(int*)&bc+0); // v_func1()
39     fp();
40     fp = (func_pointer)*((int*)*(int*)&bc+1); // v_func2()
41     fp();
42     fp = (func_pointer)*((int*)*(int*)&bc+2); // v_func3()
43     fp();
44     return 0;
45 }
vtable

运行结果:

理解:bc表示base_class类的一个实例即对象,该对象的地址&bc其实就是虚函数表的首地址,只是虚函数是一个函数指针数组,函数指针的大小是4 bytes,使用 int* 可以使得他们每次的偏移量保持一致(sizeof(int*) = 4,32-bit机器);故采用(int*)&bc将其强制转换为指向int类型的指针,(int*)&bc也就表示了虚函数的首地址,也就是说只是将该地址声明为了一个指向整型数据的指针变量,而该指针变量(&bc)本身的值并未改变于是同理后面对(int*)&bc进行解引用*(int*)&bc得到首地址内的值,再进行强制类型转换(int*)*(int*)&bc就得到了虚函数表中第一个虚函数的地址了

程序中*((int*)*(int*)&bc+3表示获得虚函数表的结束标志,即虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。

typedef void(*func_pointer)(void):定义一个函数指针,参数和返回值都是 void;*((int*)*(int*)&bc+0):找到第一个函数,注意这里需要解引用。

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

一般继承(无虚函数覆盖)

 

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

                                                                   

请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

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

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base
 5 {
 6 public:
 7     virtual void f()
 8     {
 9         cout << "This is Base's f()" << endl;
10     }
11     virtual void g()
12     {
13         cout << "This is Base's g()" << endl;
14     }
15     virtual void h()
16     {
17         cout << "This is Base's h()" << endl;
18     }
19 };
20 class Derive : public Base
21 {
22 public:
23     virtual void f1()
24     {
25         cout << "This is Derive's f1()" << endl;
26     }
27     virtual void g1()
28     {
29         cout << "This is Derive's g1()" << endl;
30     }
31     virtual void h1()
32     {
33         cout << "This is Derive's h1()" << endl;
34     }
35 };
36 
37 int main()
38 {
39     // 查看 dev_class 的虚函数表
40     Derive d; 
41     cout << "Derive first addr of vtable:    " << (int*)&d << endl;
42     cout << "Derive addr of first function:  " << (int*)*(int*)&d+0 << endl;
43     cout << "Derive addr of second function: " << (int*)*(int*)&d+1 << endl;
44     cout << "Derive addr of third function:  " << (int*)*(int*)&d+2 << endl;
45     cout << "Derive addr of fourth function: " << (int*)*(int*)&d+3 << endl;
46     cout << "Derive addr of fifth function:  " << (int*)*(int*)&d+4 << endl;
47     cout << "Derive addr of sixth function:  " << (int*)*(int*)&d+5 << endl;
48     cout << "Derive flag of end:             " << *((int*)*(int*)&d+6) << endl;
49     // 通过函数指针调用函数,验证正确性
50     typedef void(*func_pointer)(void);
51     func_pointer fp = NULL;
52     for (int i=0; i<6; i++) {
53         fp = (func_pointer)*((int*)*(int*)&d+i);
54         fp();
55     }
56     return 0;
57 }
vtable1

运行结果:

 单继承:有虚函数覆盖

 1 include <iostream>
 2 using namespace std;
 3 
 4 class base_class
 5 {
 6 public:
 7     virtual void v_func1()
 8     {
 9         cout << "This is base_class's v_func1()" << endl;
10     }
11     virtual void v_func2()
12     {
13         cout << "This is base_class's v_func2()" << endl;
14     }
15     virtual void v_func3()
16     {
17         cout << "This is base_class's v_func3()" << endl;
18     }
19 };
20 class dev_class : public base_class
21 {
22 public:
23     virtual void v_func1()
24     {
25         cout << "This is dev_class's v_func1()" << endl;
26     }
27     virtual void v_func2()
28     {
29         cout << "This is dev_class's v_func2()" << endl;
30     }
31     virtual void v_func4()
32     {
33         cout << "This is dev_class's v_func4()" << endl;
34     }
35     virtual void v_func5()
36     {
37         cout << "This is dev_class's v_func5()" << endl;
38     }
39 };
40 
41 int main()
42 {
43     // 查看 dev_class 的虚函数表
44     dev_class dc;
45     cout << "dev_class 的虚函数表首地址为:" << (int*)&dc << endl;
46     cout << "dev_class 的 第一个函数首地址:" << (int*)*(int*)&dc+0 << endl;
47     cout << "dev_class 的 第二个函数首地址:" << (int*)*(int*)&dc+1 << endl;
48     cout << "dev_class 的 第三个函数首地址:" << (int*)*(int*)&dc+2 << endl;
49     cout << "dev_class 的 第四个函数首地址:" << (int*)*(int*)&dc+3 << endl;
50     cout << "dev_class 的 第五个函数首地址:" << (int*)*(int*)&dc+4 << endl;
51     cout << "dev_class 的虚函数表结束标志: " << *((int*)*(int*)&dc+5) << endl;
52     // 通过函数指针调用函数,验证正确性
53     typedef void(*func_pointer)(void);
54     func_pointer fp = NULL;
55     for (int i=0; i<5; i++) {
56         fp = (func_pointer)*((int*)*(int*)&dc+i);
57         fp();
58     }
59     return 0;
60 }
vtable2

输出结果:

dev_class 的虚函数表首地址为:0x22ff0c
dev_class 的 第一个函数首地址:0x472d50
dev_class 的 第二个函数首地址:0x472d54
dev_class 的 第三个函数首地址:0x472d58
dev_class 的 第四个函数首地址:0x472d5c
dev_class 的 第五个函数首地址:0x472d60
dev_class 的虚函数表结束标志: 0
This is dev_class's v_func1()
This is dev_class's v_func2()
This is base_class's v_func3()
This is dev_class's v_func4()
This is dev_class's v_func5()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

可以看出当派生类中 dev_class 中重写了父类 base_class 的前两个虚函数(v_func1,v_func2)之后,使用派生类的虚函数指针代替了父类的虚函数。未重写的父类虚函数位置没有发生变化。

注:关于虚结点的值:这个结束标志(虚函数表)的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。”。那么,我在 Windows 7 + Code::blocks 10.05 下尝试,这个值是如果是 -4,表示还有下一个虚函数表,如果是0,表示是最后一个虚函数表。

一个问题:

link:http://stackoverflow.com/questions/11426970/why-can-a-derived-class-virtual-function-call-a-base-class-virtual-fuction-how

    http://bbs.csdn.net/topics/390130196

不知道看到这里,你心里有没有一个小问题?至少我是有的。看下面的代码:

复制代码
virtual void v_func1()
{
    base_class::v_func1();
    cout << "This is dev_class's v_func1()" << endl;
}
复制代码

既然派生类的虚函数表中用 dev_class::v_func1 指针代替了 base_class::v_func1,假如我显示的调用 base_class::v_func1,会不会有错呢?答案是没错的,可以正确的调用!不是覆盖了吗?dev_class 已经不知道 base_class::v_func1 的指针了,怎么调用的呢?

 多重继承(无虚函数覆盖)

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class base_class1
 5 {
 6 public:
 7     virtual void bc1_func1()
 8     {
 9         cout << "This is bc1_func1's v_func1()" << endl;
10     }
11 };
12 
13 class base_class2
14 {
15 public:
16     virtual void bc2_func1()
17     {
18         cout << "This is bc2_func1's v_func1()" << endl;
19     }
20 };
21 
22 class dev_class : public base_class1, public base_class2
23 {
24 public:
25     virtual void dc_func1()
26     {
27         cout << "This is dc_func1's dc_func1()" << endl;
28     }
29 };
30 
31 int main()
32 {
33     dev_class dc;
34     cout << "dc 的虚函数表 bc1_vt 地址:" << (int*)&dc << endl;
35     cout << "dc 的虚函数表 bc1_vt 第一个虚函数地址:" << (int*)*(int*)&dc+0 << endl;
36     cout << "dc 的虚函数表 bc1_vt 第二个虚函数地址:" << (int*)*(int*)&dc+1 << endl;
37     cout << "dc 的虚函数表 bc1_vt 结束标志:" << *((int*)*(int*)&dc+2) << endl;
38     cout << "dc 的虚函数表 bc2_vt 地址:" << (int*)&dc+1 << endl;
39     cout << "dc 的虚函数表 bc2_vt 第一个虚函数首地址::" << (int*)*((int*)&dc+1)+0 << endl;
40     cout << "dc 的虚函数表 bc2_vt 结束标志:" << *((int*)*((int*)&dc+1)+1) << endl;
41     // 通过函数指针调用函数,验证正确性
42     typedef void(*func_pointer)(void);
43     func_pointer fp = NULL;
44     // bc1_vt
45     fp = (func_pointer)*((int*)*(int*)&dc+0);
46     fp();
47     fp = (func_pointer)*((int*)*(int*)&dc+1);
48     fp();
49     // bc2_vt
50     fp = (func_pointer)*(((int*)*((int*)&dc+1)+0));
51     fp();
52     return 0;
53 }
vtable3

输出结果:

复制代码
dc 的虚函数表 bc1_vt 地址:0x22ff08
dc 的虚函数表 bc1_vt 第一个虚函数地址:0x472d38
dc 的虚函数表 bc1_vt 第二个虚函数地址:0x472d3c
dc 的虚函数表 bc1_vt 结束标志:-4
dc 的虚函数表 bc2_vt 地址:0x22ff0c
dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472d48
dc 的虚函数表 bc2_vt 结束标志:0
This is bc1_func1's v_func1()
This is dc_func1's dc_func1()
This is bc2_func1's v_func1()
复制代码

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

可以看出:多重继承的情况,会为每一个基类建一个虚函数表。派生类的虚函数放到第一个虚函数表的后面。

PS:下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

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

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

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

 多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。

下图中,我们在子类中覆盖了父类的f()函数。

下面是对于子类实例中的虚函数表的图:

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

Derive d;

Base1 *b1 = &d;

Base2 *b2 = &d;

Base3 *b3 = &d;

b1->f(); //Derive::f()

b2->f(); //Derive::f()

b3->f(); //Derive::f()

b1->g(); //Base1::g()

b2->g(); //Base2::g()

b3->g(); //Base3::g()

PS:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class base_class1
 5 {
 6 public:
 7     virtual void bc1_func1()
 8     {
 9         cout << "This is base_class1's bc1_func1()" << endl;
10     }
11     virtual void bc1_func2()
12     {
13         cout << "This is base_class1's bc1_func2()" << endl;
14     }
15 };
16 
17 class base_class2
18 {
19 public:
20     virtual void bc2_func1()
21     {
22         cout << "This is base_class2's bc2_func1()" << endl;
23     }
24     virtual void bc2_func2()
25     {
26         cout << "This is base_class2's bc2_func2()" << endl;
27     }
28 };
29 
30 class dev_class : public base_class1, public base_class2
31 {
32 public:
33     virtual void bc1_func1()
34     {
35         cout << "This is dev_class's bc1_func1()" << endl;
36     }
37     virtual void bc2_func1()
38     {
39         cout << "This is dev_class's bc2_func1()" << endl;
40     }
41     virtual void dc_func1()
42     {
43         cout << "This is dev_class's dc_func1()" << endl;
44     }
45 };
46 
47 int main()
48 {
49     dev_class dc;
50     cout << "dc 的虚函数表 bc1_vt 地址:" << (int*)&dc << endl;
51     cout << "dc 的虚函数表 bc1_vt 第一个虚函数地址:" << (int*)*(int*)&dc+0 << endl;
52     cout << "dc 的虚函数表 bc1_vt 第二个虚函数地址:" << (int*)*(int*)&dc+1 << endl;
53     cout << "dc 的虚函数表 bc1_vt 第三个虚函数地址:" << (int*)*(int*)&dc+2 << endl;
54     cout << "dc 的虚函数表 bc1_vt 第四个虚函数地址:" << (int*)*(int*)&dc+3 << endl;
55     cout << "dc 的虚函数表 bc1_vt 结束标志:" << *((int*)*(int*)&dc+4) << endl;
56     cout << "dc 的虚函数表 bc2_vt 地址:" << (int*)&dc+1 << endl;
57     cout << "dc 的虚函数表 bc2_vt 第一个虚函数首地址::" << (int*)*((int*)&dc+1)+0 << endl;
58     cout << "dc 的虚函数表 bc2_vt 第二个虚函数首地址::" << (int*)*((int*)&dc+1)+1 << endl;
59     cout << "dc 的虚函数表 bc2_vt 结束标志:" << *((int*)*((int*)&dc+1)+2) << endl;
60     // 通过函数指针调用函数,验证正确性
61     typedef void(*func_pointer)(void);
62     func_pointer fp = NULL;
63     // bc1_vt
64     fp = (func_pointer)*((int*)*(int*)&dc+0);
65     fp();
66     fp = (func_pointer)*((int*)*(int*)&dc+1);
67     fp();
68     fp = (func_pointer)*((int*)*(int*)&dc+2);
69     fp();
70     fp = (func_pointer)*((int*)*(int*)&dc+3);
71     fp();
72     // bc2_vt
73     fp = (func_pointer)*(((int*)*((int*)&dc+1)+0));
74     fp();
75     fp = (func_pointer)*(((int*)*((int*)&dc+1)+1));
76     fp();
77     return 0;
78 }
vtable4

输出结果:

dc 的虚函数表 bc1_vt 地址:0x22ff08
dc 的虚函数表 bc1_vt 第一个虚函数地址:0x472e28
dc 的虚函数表 bc1_vt 第二个虚函数地址:0x472e2c
dc 的虚函数表 bc1_vt 第三个虚函数地址:0x472e30
dc 的虚函数表 bc1_vt 第四个虚函数地址:0x472e34
dc 的虚函数表 bc1_vt 结束标志:-4
dc 的虚函数表 bc2_vt 地址:0x22ff0c
dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472e40
dc 的虚函数表 bc2_vt 第一个虚函数首地址::0x472e44
dc 的虚函数表 bc2_vt 结束标志:0
This is dev_class's bc1_func1()
This is base_class1's bc1_func2()
This is dev_class's bc2_func1()
This is dev_class's dc_func1()
This is dev_class's bc2_func1()
This is base_class2's bc2_func2()

通过上面的例子的尝试和输出结果,我们可以得出下面的布局图示:

是不是感觉很乱?其实一点都不乱!就是两个单继承而已。把多余的部分(派生类的虚函数)增加到第一个虚函数表的最后,CB(Code::Blocks)是这样实现的。我试了一下,vs2010不是这样实现的,读者可以自己尝试一下。本文只针对 CB 来探讨。

有人觉得多重继承不好理解。我想如果你明白了它的虚函数表是怎么样的,也就没什么不好理解了吧。 也许还有人会说,不同的编译器实现方式是不一样的,我去研究某一种编译器的实现有什么意义呢?我个人理解是这样的:1.实现方式是不一样的,但是它们的实现结果是一样的(多态)。2.无论你了解虚函数表或者不了解虚函数表,我相信你都很少会用到它。但是当你了解了它的实现机制之后,你再去看多态,再去写虚函数的时候[作为你一个coder],相信你的感觉是不一样的。你会感觉很透彻,不会有丝毫的犹豫。3.学习编译器这种处理问题的方式(思想),这才是最重要的。[好像扯远了,^-^]。 如果你了解了虚函数表之后,可以通过虚函数表直接访问类的方法,这种访问是不受成员的访问权限限制的(private,protected)。这样做是很危险的,但是确实是可以这样做的。这也是C++为什么很危险的语言的一个原因……

看完之后,你不是产生了许多其他的问题呢?至少我有了几个问题[我这人问题特别多。^-^]比如: 1.访问权限是怎么实现的?编译器怎么知道哪些函数是public,哪些是protected? 2.虚函数调用是通过虚函数表实现的,那么非虚成员函数存放在哪里?是怎么实现的呢? 3.类的成员存放在什么位置?怎么继承的呢?[这是对象布局问题,=.=] 你知道的越多,你感觉你知道的越少。推荐大家一本书吧,《深度探索C++对象模型》(英文名字是《Inside to C++ Object Model》),看完你会明白很多。

安全性

一、通过父类型的指针访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();

b1->f1(); //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

如:

class Base {

private:

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

};

class Derive : public Base{

};

typedef void(*Fun)(void);

void main() {

Derive d;

Fun pFun = (Fun)*((int*)*(int*)(&d)+0);

pFun();

}

 

Note:此外关于虚函数表的内容还有一些博文可以参考:

http://www.cnblogs.com/Binhua-Liu/archive/2010/06/16/1759019.html

http://www.cnblogs.com/wirelesser/archive/2008/03/09/1097463.html

http://www.cnblogs.com/clor001/archive/2013/09/11/3313589.html

 

 

 

 

 

 

 

 

 

posted @ 2013-08-15 17:10  CoolRandy  阅读(242)  评论(0编辑  收藏  举报