C++之 “虚函数” 详解

来源:互联网 发布:淘宝新版质保金好退吗 编辑:程序博客网 时间:2024/05/24 04:00

C++之 “虚函数” 详解

原文见:www.louhang.xin

虚函数在C++中有着十分重要的作用,通过虚函数可以实现多态(polymorphism)机制

在看《C++ primer plus》时,发现作者将虚函数放在类继承的一章之中,和动态/静态联编一起进行了讲解。我也就顺着复习了继承,便再剖析一下虚函数。

虚函数--类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。

在之前的博客《多态及其对象模型》中我剖析虚函数在其中的作用。

底层原理:

当存在虚函数时,编译器会给每个对象添加一个隐藏成员,此隐藏成员保存的是指向虚函数地址数组的指针。而这个数组就叫虚函数表(virtual function table)。

虚函数表中存放的正是为类对象进行声明的虚函数的地址。

需要注意的是,在继承之中,如果基类存在虚表,那么基类对象将存在一个指向其虚表的指针;而派生类对象中将包含的是一个指向独立虚表的指针。这个独立虚表中不仅包含从父类继承的虚函数地址,还包括派生类定义的新的虚函数的地址。

当调用虚函数时,程序将查看存储在对象中的虚表的地址,进而去虚表中查找相应的虚函数。

代码如下:

#include<iostream>using namespace std;//单继承模型class Father{public:virtual void fun1(){cout << "Father::fun1()" << endl;}virtual void fun2(){cout << "Father::fun2()" << endl;}protected:int _a = 10;};class Son : public Father{public:virtual void fun2(){cout << "Son::fun2()" << endl;}virtual void fun3(){cout << "Son::fun3()" << endl;}protected:int _b = 20;};void Fun(Father& f){f.fun2();}void test(){Father a;Son b;Fun(a);Fun(b);}int main(){test();system("pause");return 0;}

在监视窗口查看:

image.png

我们可以看到在基类内部存在着一个地址,而这个地址指向了一张表,既虚表,虚表内部存储的正是虚函数。但是我们会发现派生类虚函数fun3却不在此表内部。实际上,fun3也在此表内部,只不过编译器做了优化,没有在监视窗口显示出来而已。

我们可以在内存中观察,也可以通过书写函数将虚表内的地址打印出来。

内存中观察:

image.png


通过函数来将虚表及其内存放的地址打印出来,如下:

#include<iostream>using namespace std;class Father{public:virtual void fun1(){cout << "Father::fun1()" << endl;}virtual void fun2(){cout << "Father::fun2()" << endl;}private:int _a = 10;};class Son : public Father{public:virtual void fun2(){cout << "Son::fun2()" << endl;}virtual void fun3(){cout << "Son::fun3()" << endl;}protected:int _b = 20;};typedef void(*FUNC) ();void PrintVTable(int* VTable){cout << " 虚表地址>" << VTable << endl;for (int i = 0; VTable[i] != 0; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);FUNC f = (FUNC)VTable[i];f();}cout << endl;}void test(){Father a;Son b;int* VTable1 = (int*)(*(int*)&a);int* VTable2 = (int*)(*(int*)&b);PrintVTable(VTable1);PrintVTable(VTable2);}int main(){test();system("pause");return 0;}

TIM截图20171203105545.png

image.png

可以看到在创建的两个对象中都存在着虚表。这也不可避免的出现了一些问题:

  1. 每个对象占据内存都变大了,增加内存来存放虚表的地址。

  2. 针对每个类,编译器都要创建一个虚函数表。

  3. 每次的函数调用,都要额外执行操作,要去虚表中查找虚函数的地址。

虽然非虚函数比虚函数的效率稍高一点,但是起不具备动态联编,不能构成多态。

虚函数应用:

  • 构造函数

构造函数不能为虚构函数。虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引 起混淆。

  • 析构函数

析构函数定义为虚函数,除非类不用做基类。

class A{public:    A()     {     _ptra = new char[10];    }        ~A()     {         delete[] _ptra;    }       private:    char* _ptra;};class B: public A{public:    B(){ _ptrb = new char[20];}    ~B() { delete[] _ptrb;}private:    char * _ptrb;};void test(){    A * a = new B;    delete a;}

上面的程序存在内存泄漏,因为其是静态联编,在释放对象a时仅仅调用了A的·析构函数调用了,B的析构函数并未调用,这就造成了一个很危险的漏洞。

但如果将A类的析构函数定义为虚析构函数,那么执行的将是动态联编,则会将内存全部成功的释放掉。

  • 纯虚函数

纯虚函数只进行声明,而不定义。如下:

class Test{public:    virtual void fun()=0;   // =0 标志一个虚函数为纯虚函数};

包含有纯虚函数的类是抽象类,而抽象类不能进行实例化。只能被其他派生类继承,而纯虚函数就是一个公共的接口,所有继承了抽象类的派生类内部都包含纯虚函数。

纯虚函数不定义,其定义交给派生类来完成,继承了抽象类的派生类必须对纯虚函数进行定义

  • 友元函数

友元函数不能定义为虚函数。因为友元不是类的成员,只有类的成员才能定义为虚函数。

总结:

        1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外) 

        2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。 

        3. 只有类的成员函数才能定义为虚函数。 

        4. 静态成员函数不能定义为虚函数。 

        5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。 

        6. 构造函数不能为虚函数,最好也不要将operator=定义为虚函数,因为容易使用时容易引起混淆。

        7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。 

        8. 最好把基类的析构函数声明为虚函数。


参考资料:

    《C++ primer plus》Stephen Prata,张海龙,袁国忠译

    《深度探索C++对象模型》 Stanley B.Lippman,侯捷译




原创粉丝点击