赞
踩
虚函数在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; }
在监视窗口查看:
我们可以看到在基类内部存在着一个地址,而这个地址指向了一张表,既虚表,虚表内部存储的正是虚函数。但是我们会发现派生类虚函数fun3却不在此表内部。实际上,fun3也在此表内部,只不过编译器做了优化,没有在监视窗口显示出来而已。
我们可以在内存中观察,也可以通过书写函数将虚表内的地址打印出来。
内存中观察:
通过函数来将虚表及其内存放的地址打印出来,如下:
#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; }
可以看到在创建的两个对象中都存在着虚表。这也不可避免的出现了一些问题:
每个对象占据内存都变大了,增加内存来存放虚表的地址。
针对每个类,编译器都要创建一个虚函数表。
每次的函数调用,都要额外执行操作,要去虚表中查找虚函数的地址。
虽然非虚函数比虚函数的效率稍高一点,但是起不具备动态联编,不能构成多态。
构造函数
构造函数不能为虚构函数。虽然可以将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,侯捷译
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。