C++ 系列:虚函数

摘要:
C++纯虚拟函数1。定义纯虚函数是在基类中声明的虚函数,它没有在基类中定义,但需要任何派生类定义自己的实现方法。同时,包含纯虚拟函数的类称为抽象类,不能生成对象。声明纯虚拟函数的类是抽象类。定义纯虚拟函数的目的是使派生类仅成为继承函数的接口。C++中的虚拟函数是通过虚拟函数表实现的。只要有虚拟函数,C++类就会有这样的虚拟函数表,

Copyright © 1900-2016, NORYES, All Rights Reserved.

http://www.cnblogs.com/noryes/

欢迎转载,请保留此版权声明。

---------------------------------------------------------------------------------------

转载自:http://blog.csdn.net/hackbuteer1/article/details/7558868

首先:强调一个概念

定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
1、简介
假设我们有下面的类层次:

  1. class A  
  2. {  
  3. public:  
  4.     virtual void foo()  
  5.     {  
  6.         cout<<"A::foo() is called"<<endl;  
  7.     }  
  8. };  
  9. class B:public A  
  10. {  
  11. public:  
  12.     void foo()  
  13.     {  
  14.         cout<<"B::foo() is called"<<endl;  
  15.     }  
  16. };  
  17. int main(void)  
  18. {  
  19.     A *a = new B();  
  20.     a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!  
  21.     return 0;  
  22. }  

     这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
    虚函数只能借助于指针或者引用来达到多态的效果。

C++纯虚函数
一、定义
 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
 virtual void funtion1()=0
二、引入原因
  1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

抽象类的介绍
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义:  称带有纯虚函数的类为抽象类。
(2)抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意:
•   抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
•   抽象类是不能定义对象的。

总结:
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
定义纯虚函数就是为了让基类不可实例化化
因为实例化这样的抽象数据结构本身并没有意义。
或者给出实现也没有意义
实际上我个人认为纯虚函数的引入,是出于两个目的
1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
2、为了效率,不是程序执行的效率,而是为了编码的效率。

C++的虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖(override)的问题,保证其能真实的反应实际的函数。这样,在有虚函数的类的实例中这张表被分配在了这个实例的内存中,所以当我们用父类的指针操作一个子类的时候,这张虚函数表就显得尤为重要了,他就像一个地图一样,指明了实际所应该调用的函数。

说明:虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数  亦或 是 纯虚函数,亦或是 派生类中隐式声明的这些虚函数都会 生成这张虚函数表。

虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。

首先了解下这张虚函数表:虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。

1、无继承情况

[cpp] view plain copy
 
  1. #include <iostream>  
  2.   
  3. using namespace std;  
  4.   
  5. class Base  
  6. {  
  7. public:  
  8.     Base(){cout<<"Base construct"<<endl;}  
  9.     virtual void f() {cout<<"Base::f()"<<endl;}  
  10.     virtual void g() {cout<<"Base::g()"<<endl;}  
  11.     virtual void h() {cout<<"Base::h()"<<endl;}  
  12.     virtual ~Base(){}  
  13. };  
  14.   
  15. int main()  
  16. {  
  17.     typedef void (*Fun)();  //定义一个函数指针类型变量类型 Fun  
  18.     Base *b = new Base();  
  19.     //虚函数表存储在对象最开始的位置  
  20.     //将对象的首地址输出  
  21.     cout<<"首地址:"<<*(int*)(&b)<<endl;  
  22.   
  23.     Fun funf = (Fun)(*(int*)*(int*)b);  
  24.     Fun fung = (Fun)(*((int*)*(int*)b+1));//地址内的值 即为函数指针的地址,将函数指针的地址存储在了虚函数表中了  
  25.     Fun funh = (Fun)(*((int *)*(int *)b+2));  
  26.   
  27.     funf();  
  28.     fung();  
  29.     funh();  
  30.   
  31.     cout<<(Fun)(*((int*)*(int*)b+4))<<endl; //最后一个位置为0 表明虚函数表结束 +4是因为定义了一个 虚析构函数  
  32.   
  33.     delete b;  
  34.     return 0;  
  35. }  

注意:在上面这个图中,虚函数表中最后一个节点相当于字符串的结束符,其标志了虚函数表的结束,在Codeblocks下打印为0。 

C++ 系列:虚函数第1张  Base::~Base()在Base::h()后边

2、继承,无虚函数覆盖的情形

[cpp] view plain copy
 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Base {  
  5. public:  
  6.     virtual void f() { cout << "Base::f()" << endl; }  
  7.     virtual void g() { cout << "Base::g()" << endl; }  
  8.     virtual void h() { cout << "Base::h()" << endl; }  
  9. };  
  10.   
  11. class Derive: public Base {  
  12.     virtual void f1() { cout << "Derive::f1()" << endl; }  
  13.     virtual void g1() { cout << "Derive::g1()" << endl; }  
  14.     virtual void h1() { cout << "Derive::h1()" << endl; }  
  15. };  
  16.   
  17. int main()  
  18. {  
  19.     typedef void (*Fun)();  
  20.   
  21.     Base *b = new Derive;  
  22.     cout << *(int*)b << endl;  
  23.     Fun funf = (Fun)(*(int*)*(int*)b);  
  24.     Fun fung = (Fun)(*((int*)*(int*)b + 1));  
  25.     Fun funh = (Fun)(*((int*)*(int*)b + 2));  
  26.     Fun funf1 = (Fun)(*((int*)*(int*)b + 3));  
  27.     Fun fung1 = (Fun)(*((int*)*(int*)b + 4));  
  28.     Fun funh1 = (Fun)(*((int*)*(int*)b + 5));  
  29.   
  30.   
  31.     funf(); // Base::f()  
  32.     fung(); // Base::g()  
  33.     funh(); // Base::h()  
  34.     funf1(); // Derive::f1()  
  35.     fung1(); // Derive::g1()  
  36.     funh1(); // Derive::h1()  
  37.   
  38.     cout << (Fun)(*((int*)*(int*)b + 6));  
  39.     return 0;  
  40. }  


C++ 系列:虚函数第2张

从表上可以看出

1、虚函数按照声明的顺序放在表中。

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

3. 继承,虚函数覆盖的情形

[cpp] view plain copy
 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Base {  
  5. public:  
  6.     virtual void f() { cout << "Base::f()" << endl; }  
  7.     virtual void g() { cout << "Base::g()" << endl; }  
  8.     virtual void h() { cout << "Base::h()" << endl; }  
  9. };  
  10.   
  11. class Derive: public Base {  
  12.     virtual void f() { cout << "Derive::f()" << endl; }  
  13.     virtual void g1() { cout << "Derive::g1()" << endl; }  
  14.     virtual void h1() { cout << "Derive::h1()" << endl; }  
  15. };  
  16.   
  17. int main()  
  18. {  
  19.     typedef void (*Fun)();  
  20.   
  21.     Base *b = new Derive;  
  22.     cout << *(int*)b << endl;  
  23.     Fun funf = (Fun)(*(int*)*(int*)b);  
  24.     Fun fung = (Fun)(*((int*)*(int*)b + 1));  
  25.     Fun funh = (Fun)(*((int*)*(int*)b + 2));  
  26.     Fun fung1 = (Fun)(*((int*)*(int*)b + 3));  
  27.     Fun funh1 = (Fun)(*((int*)*(int*)b + 4));  
  28.   
  29.   
  30.     funf(); // Derive::f()  
  31.     fung(); // Base::g()  
  32.     funh(); // Base::h()  
  33.     fung1(); // Derive::g1()  
  34.     funh1(); // Derive::h1()  
  35.   
  36.     cout << (Fun)(*((int*)*(int*)b + 5));  
  37.     return 0;  
  38. }  


C++ 系列:虚函数第3张

从表上可以看出:

1、覆盖的 f() 函数被放到虚函数表中原来父类虚函数的位置。

2、没有被覆盖的函数依旧。

3、可通过获取成员函数指针来调用成员函数(即时是private类型的成员函数),这就出现一定的安全问题。

4、多继承情况

[cpp] view plain copy
 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Base1 {  
  5. public:  
  6.     virtual void f() { cout << "Base1::f()" << endl; }  
  7.     virtual void g() { cout << "Base1::g()" << endl; }  
  8.     virtual void h() { cout << "Base1::h()" << endl; }  
  9. };  
  10.   
  11. class Base2 {  
  12. public:  
  13.     virtual void f() { cout << "Base2::f()" << endl; }  
  14.     virtual void g() { cout << "Base2::g()" << endl; }  
  15.     virtual void h() { cout << "Base2::h()" << endl; }  
  16. };  
  17.   
  18.   
  19. class Base3 {  
  20. public:  
  21.     virtual void f() { cout << "Base3::f()" << endl; }  
  22.     virtual void g() { cout << "Base3::g()" << endl; }  
  23.     virtual void h() { cout << "Base3::h()" << endl; }  
  24. };  
  25.   
  26.   
  27. class Derive: public Base1,public Base2, public Base3 {  
  28.     virtual void f() { cout << "Derive::f()" << endl; }  
  29.     virtual void g1() { cout << "Derive::g1()" << endl; }  
  30. };  
  31.   
  32. int main()  
  33. {  
  34.     typedef void (*Fun)();  
  35.   
  36.     Derive d;  
  37.     Base1 *b1 = &d;  
  38.     Base2 *b2 = &d;  
  39.     Base3 *b3 = &d;  
  40.   
  41.   
  42.     b1->f(); //Derive::f()  
  43.     b2->f(); //Derive::f()  
  44.     b3->f(); //Derive::f()  
  45.     b1->g(); //Base1::g()  
  46.     b2->g(); //Base2::g()  
  47.     b3->g(); //Base3::g()  
  48.   
  49.   
  50.     Fun b1fun = (Fun)(*(int*)*(int*)b1);  
  51.     Fun b2fun = (Fun)(*(int*)*((int*)b1+1));  
  52.     Fun b3fun = (Fun)(*(int*)*((int*)b1+2));  
  53.   
  54.     b1fun(); // Derive::f()  
  55.     b2fun(); // Derive::f()  
  56.     b3fun(); // Derive::f()  
  57.   
  58.     return 0;  
  59. }  

C++ 系列:虚函数第4张

从表上可以看出:

1、每个父类都有自己的虚函数表。

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

3、对于多继承无虚函数覆盖的情况,布局与上图类似(Derive的位置对应Base)

http://blog.chinaunix.net/uid-20196318-id-28833.html

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

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

C++ 系列:虚函数第5张

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

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

我们可以看到下面几点:

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

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

我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。

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

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

C++ 系列:虚函数第6张

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

C++ 系列:虚函数第7张

我们从表中可以看到下面几点,

1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

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

Base *b = new Derive();

b->f();

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

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

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

C++ 系列:虚函数第8张

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

C++ 系列:虚函数第9张

我们可以看到:

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

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

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

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

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

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

C++ 系列:虚函数第10张

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

C++ 系列:虚函数第11张

我们可以看见,三个父类虚函数表中的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()

安全性

每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

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

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到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();

}

感谢博主:http://hi.baidu.com/twqxapqwftbmoxq/item/a8d46307acd214c975cd3cf7

补充:
 
今天在c++坛子里瞎逛,看到精华坛里在讨论“为什么虚函数效率低”的问题,
××楼主回答面试官说“跟cpu流水线执行效率有关”        
××某人回答“因为虚函数需要一次间接的寻址... 而一般的函数可以在编译时定位到函数的地址,虚函数(动态类型调用)是要根据某个指针定位到函数的地址. ” 
×ד虚函数有个虚函数表,而且会传一个index索引~!会间接寻址!”
×ד流水线执行的话,和"命中率"有关吧. 也就是说在流水线后端,已经译码成功的,和正在执行的代码的后继是一样的. 否则流水线会中断,也就是说在后端做的是无效的,需要重新译码.”    
搞笑的是以下人的回复:
×ד的确,计算机程序效率说到底和计算机指令流水线息息相关(还和缓存命中率有关)。但是,把虚函数效率低的原因解释到流水线这一层,是极其变态的,这个考官很可能是在卖弄自己的水平而已。”   
×ד楼主以后你要是遇到这种考官,你和他谈与非逻辑门,硅锗原子的组成和爱因斯坦相对论对虚函数的影响,绝对震惊四座!”
×ד说是因为流水线执行的原因,根本与问题不着边际。或者应该说影响流水线执行是效率低的无数原因中的一种才好。”    
×ד首先是由this指向查找虚函数表,然后找到相应的虚函数地址 
比非虚函数多查找一次 
如果是(多继承)基类指针指向派生类对象的话,有可能会涉及this指针的调整                     
比如先访问基类的成员数据再访问派生类的析构函数  就要进行一次this指针的调整 
具体可以参见 insied the c++ object model的多重继承下的virtual functions ”
×ד一些C++的书籍有明确的说明,针对类的虚函数的机制,如果有虚函数的话,编译器会为类增加一个虚函数表(VBL),当在动态执行程序时,会到该虚函数表中寻找函数。多增加了一个过程,效率肯定会低一些,但带来了运行时的多态。”    
×ד流水线 貌似说的是 CPU执行代码的提前取指令吧 
虚函数 效率低 是因为 执行过程中会跳转两次(首先找到对象的函数表,其次通过该函数表中存的虚函数表地址找到真正的执行地址),这样CPU运行的时候会跳转两次,而普通函数只跳一次。CPU每跳转一次,预取指令基本上就要作废很多,所以效率会很低。”
/////////////////////////////////////最后得分者
和流水线相关是说得通的,究其原因还是因为存在动态跳转,这会导致分支预测失败,流水线排空。 

设想一下,如果说不是虚函数,那么在编译时期,其相对地址是确定的,编译器可以直接生成jmp/invoke指令; 
如果是虚函数,多出来的一次查找vtable所带来的开销,倒是次要的,关键在于,这个函数地址是动态的,譬如 
取到的地址在eax里,则在call eax之后的那些已经被预取进入流水线的所有指令都将失效。流水线越长,一次分支预测失败的代价也就越大。 

pf->test(); 
011E146D  mov        eax,dword ptr [pf] 
011E1470  mov        edx,dword ptr [eax] 
011E1472  mov        esi,esp 
011E1474  mov        ecx,dword ptr [pf] 
011E1477  mov        eax,dword ptr [edx] 
011E1479  call        eax  <------------------------- 分支预测失效 
011E147B  cmp        esi,esp 
011E147D  call        @ILT+355(__RTC_CheckEsp) (11E1168h)    

此兄接着回答道“说到流水线,penalty基本上都是因为气泡(也就是分支指令造成预取失效),知道这个以后碰到了就不会再卡壳了。虽然引入流水线(流水线其实是 RISC最初使用的),极大提高了效率,流水线不是越长越好。像P4,几十级流水线,频率虽高,但是性能不好,很大原因就是因为流水线实在臭长。有兴趣可 以去看看CPU怎么做分支预测,乱序执行的。”
//////////////////////////////////
还是贴上原帖的地址吧 http://topic.csdn.net/u/20081031/12/06d0e218-8aab-4203-850c-9e6b76099c09.html
由此还引申出一个问题 虚函数在编译器里是怎么工作的
http://blog.csdn.net/metalkittie/article/details/3281916


C++虚函数表面试汇总

一般来说,对于开发者我们只需要知道虚函数的使用方法,以及虚函数表的存在即可。但面试时往往会遇到更细节的问题,比如让你实现一个虚函数机制,虽然不太实用,总归了解些底层知识也是件好事。但如果有人苦苦相逼一定要拿这个刷人,你就去骂他吧,你才是写编译器的,你们全家都是写编译器的。唉,我有些失态了...

1. 虚函数与虚函数表基本知识

这里有一篇介绍,只需看前两页,各种配图,很形象:http://dev.yesky.com/208/8061708.shtml

这篇文章则更精练,只需看第一段就好:http://blog.csdn.net/jiangnanyouzi/article/details/3720807

总的来说,每一个拥有virtual function的类实例化对象时,都会额外申请一块内存存储虚函数表存储所有虚函数地址,并在对象某个位置存储一个vptr指针指向该表起始地址。这个指针具体放在什么位置,虚函数表怎么组织,怎么索引各个虚函数,这些都是编译器在编译期间决定的,在不同编译环境下不见得相同。

2. 多态子类的调用顺序 -- 为什么不要在构造函数中调用虚函数

原因是,在子类的构造函数执行时,虚函数表还没有被子类覆盖,换句话说,此时调用的函数是当前类的函数,虚函数机制在构造函数中无法触发。其原因在于子类构造时各个初始化步骤的调用顺序:

全部推演过程见此:http://saturnman.blog.163.com/blog/static/557611201081421344244/

直接摘录构造顺序:

1.构造子类构造函数的参数

2.子类调用基类构造函数

3.基类设置vptr

4.基类初始化列表内容进行构造

5.基类函数体调用

6.子类设置vptr

7.子类初始化列表内容进行构造

8.子类构造函数体调用

(注意一点,初始化列表内的数据不按书写顺序,而是按类内部的定义顺序)

析构的顺序恰好相反,所以也不要在析构函数中调用虚函数,那样也是没有意义的。

3. 如何去验证虚函数表的存在

其实在第一个链接里已经有了示例程序。

如果你看不懂函数指针,请看这里:http://hi.baidu.com/homonia/blog/item/90b7a72c49c521ea8a1399e2.html

4. 为什么构造函数不能是虚函数

1、从设计理念上说,构造函数不需要是虚函数;

2、从当前vptr的实现机制上说,无法实现虚的构造函数。

 先看下面的一个例子:

/**
 *rief virtual function test case
 *author peakflys
 *date Sun Dec  1 14:52:47 CST 2013
 */
#include <iostream>
 
using namespace std;
 
class Base
{
public:
    virtual void print(const int a = 10) {cout<<"Base: "<<a<<endl;}
};
 
class Derive : public Base
{
public:
    virtual void print(const int a = 100) {cout<<"Derive: "<<a<<endl;}
};
 
int main()
{
    Base *pb = new Derive;
    pb->print();
    Base& rb = *pb;
    rb.print();
    Derive d;
    d.print();
    Base *pbb = &d; 
    pbb->print();
    Base& rbb = d;
    rbb.print();
    Base b;
    b.print();
    Derive *pd = (Derive*)&b;
    pd->print();
    Derive& rd = *(Derive*)&b;
    rd.print();
    delete pb; 
    return 0;
}

你认为运行后的结果是什么呢?
下面是在我机器上的运行结果(Linux dev 2.6.32,gcc (GCC) 4.8.1)

Derive: 10
Derive: 10
Derive: 100
Derive: 10
Derive: 10
Base: 10
Base: 100
Base: 100

上面例子主要考察的内容有四块:虚函数的执行、引用和指针的关系、函数调用过程、类型强转后的行为。如果你能答对所有的结果,下面的内容可以略过。
下面我们来一一回顾一下所涉及到的这四块内容。
1、虚函数的运行机理:
虚函数是C++实现多态性的必要手段,它在运行时刻才决定具体该调用哪个函数。对于虚函数的完整细节实现标准并未给出,但是大多数编译器厂商,包括GCC、VS的常见实现都是在含有虚函数的类对象起始地址增加一个虚表指针,虚表指针指向的数组空间称之为虚表,这个数组包含了类对象的所有虚函数地址。详细内容大家可以参看《Inside The C++ Object Model》的Function语义学(注:这本书里有部分结论和例子运行同现在主流编译器的实现有出入)。
2、引用的行为
在常见的编译器中,引用一般都是通过指针来实现的,它同指针的区别就是它比指针有更多的约束,使用上有更多的限制。
3、虚函数的调用过程:
虚函数的调用过程通常是以下三个步骤:
①、参数压栈
②、从虚表指针指向的虚表中找出函数的地址
③、调用函数。
这些操作都是在编译时期就确定的,所不同的是运行时刻对象不同,其对应的虚表中函数地址自然也就是运行时真实对象的函数,这也就是虚函数实现的本质。
而这个过程中,参数的入栈是对象无关的,而且是在编译时期就确定下来的。所以上面例子中所有指针和引用所调用函数的参数,都是指针和引用本身类型对应的函数默认参数,同运行时刻他们真实指向的对象内存无关。
4、类型强转后的行为
通常的类型强转是告诉编译器必须按照指定结构的内存布局来解析对应内存,正如上例中”Derive *pd = (Derive*)&b; “ ,编译器就会把b对应的内存来当做Derive的内存布局来解析,但是内存里的内容不变,所以虚函数运行正常。
注:这种行为很危险,如果使用的内存布局并不适合真实内存,很可能造成访问越界等问题,所以要格外小心强转操作的使用!对于例子中的downcasting行为,建议使用C++提供的dynamic_cast来转换。
为了大家更好的理解上面的内容,特附上使用指针和引用分别调用虚函数过程的gcc汇编代码和注释:

    Base *pb = new Derive;
  400b49:   bf 08 00 00 00          mov    $0x8,%edi
  400b4e:   e8 6d fe ff ff          callq  4009c0 <_Znwm@plt>
  400b53:   48 89 c3                mov    %rax,%rbx
  400b56:   48 89 df                mov    %rbx,%rdi
  400b59:   e8 f4 01 00 00          callq  400d52 <_ZN6DeriveC1Ev>   //以上均为Derive对象的构造
   400b5e:   48 89 5d e8             mov    %rbx,-0x18(%rbp)             //pb指针的赋值
    pb->print();
  400b62:   48 8b 45 e8             mov    -0x18(%rbp),%rax               //pb指针指向的内存的首地址,即Derive对象的起始地址,亦即虚表指针的地址
  400b66:   48 8b 00                mov    (%rax),%rax                        //取虚表地址
  400b69:   48 8b 00                mov    (%rax),%rax                        //取虚表中的第一项内容(因Derive和Base只有一个虚函数),即print函数地址
  400b6c:   48 8b 55 e8             mov    -0x18(%rbp),%rdx               //this指针传入rdx
  400b70:   be 0a 00 00 00          mov    $0xa,%esi                         //参数10入栈(可见在编译时期就已经确定了)
  400b75:   48 89 d7                mov    %rdx,%rdi                            //this指针借rdx传给rdi
  400b78:   ff d0                   callq  *%rax                                     //调用虚函数(通过真实对象的虚表来确定的真正被调函数)
    Base& rb = *pb;
  400b7a:   48 8b 45 e8             mov    -0x18(%rbp),%rax
  400b7e:   48 89 45 e0             mov    %rax,-0x20(%rbp)
    rb.print();
  400b82:   48 8b 45 e0             mov    -0x20(%rbp),%rax
  400b86:   48 8b 00                mov    (%rax),%rax
  400b89:   48 8b 00                mov    (%rax),%rax
  400b8c:   48 8b 55 e0             mov    -0x20(%rbp),%rdx
  400b90:   be 0a 00 00 00          mov    $0xa,%esi
  400b95:   48 89 d7                mov    %rdx,%rdi
  400b98:   ff d0                   callq  *%rax                                       //以上为通过引用调用虚函数的过程,可见同指针调用的实现完全相同,注释略

免责声明:文章转载自《C++ 系列:虚函数》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇XML导入Access/MySql数据库 XML2OleDb Fred编辑datagridview单元格下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

delphi函数调用约定

指令 参数存放位置 参数传递顺序 参数内存管理 使用地方 Register CPU寄存器 从左到右 被调用者 默认,published属性存取方法必须使用 Pascal 栈 从左到右 被调用者 向后兼容 Cdecl 栈 从右到左 调用者 调用c/c++共享库 Stdcall 栈 从右到左 被调用者 API调用 Safecall 栈...

Android Service总结02 service介绍

Android Service总结02 service介绍 版本 版本说明 发布时间 发布人 V1.0 介绍了Service的种类,常用API,生命周期等内容。 2013-03-16 Skywang         概要   若读者之前没接触过service,对下面内容有个大致了解即可。待使用过service之后再来阅读本章内容,会理解...

Oracle数据库的函数,存储过程,程序包,游标,触发器

Oracle自定义函数 函数的主要特性是它必须返回一个值。创建函数时通过 RETURN 子句指定函数返回值的数据类型。函数的一些限制:● 函数只能带有 IN 参数,不能带有 IN OUT 或 OUT 参数。● 形式参数必须只使用数据库类型,不能使用 PL/SQL 类型。● 函数的返回类型必须是数据库类型 Create function 函数名称 retur...

再探NSString

再探NSString NSString应该是oc开发中最常用的一个数据类型了,这次对该类型再进行一次全方位的探索与总结。 NSString本质上属于OC类对象,继承于NSObject,遵守NSCopying, NSMutableCopying, NSSecureCoding协议。 NSMutableString与之类似,唯一不同的是它继承于NSStrin...

Linux12-内存管理

Linux内核第12章 内核不能像用户空间那样奢侈地使用内存,内核与用户空间不同,它不具备这种能力,它不支持简单便捷的内存分配方式。比如,内核一般不能睡眠,此外处理内存分配错误对内核来说也很困难。正是因为这些限制和内存分配机制不能太复杂,所以在内核中获取内存要比在用户空间复杂得多。 12.1 页 内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单...

Win32编程

    Win32编程 此资料为ITjob软件开发教程网提供,特此分享,互相学习! C/C++/VC/MFC技术交流群:95453496 一、Win32编程基本概念 1、消息驱动 在介绍Windows消息驱动概念之前,我们首先来回顾面向过程的程序结构:main()程序有明显的开始、中间过程和结束点,程序是围绕这个过程编写好相关的子过程,再把这些子过程串联...