C++中的虚函数的做用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,而后经过父类的指针调用实际子类的成员函数。这种技术可让父类的指针有“多种形态”。ios
编译器必须生成可以在程序运行时选择正确的虚方法的代码,这被称为动态联编数组
当类中存在虚函数时,编译器默认会给对象添加一个隐藏成员。该成员为一个指向虚函数表(virtual function table,vtbl)的指针。安全
虚函数表是一个保存了虚函数地址的数组。编译器会检查类中全部的虚函数,依次将每一个虚函数的地址,存入虚函数表。函数
虚函数表主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,因此,当咱们用父类的指针来操做一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图同样,指明了实际所应该调用的函数。
没有覆盖父类的虚函数是毫无心义的。spa
#include <iostream> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } }; class Derive : public Base { }; int main() { Base *b = new Derive(); b->f(); } //打印结果为: Base::f
虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。设计
#include <iostream> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; class Derive : public Base { virtual void f() { cout << "Derive::f" << endl; } virtual void g1() { cout << "Derive::g1" << endl; } virtual void h1() { cout << "Derive::h1" << endl; } }; int main() { Base *b = new Derive(); b->f(); } //打印结果为: Derive::f
派生类覆盖父类虚函数的函数被放到了虚表中原来父类虚函数的位置。没有被覆盖的函数依旧。指针
每一个父类都有本身的虚表。code
子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)对象
这样作就是为了解决不一样的父类类型的指针指向同一个子类实例,而可以调用到实际的函数。blog
个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,咱们就能够任一静态类型的父类来指向子类,并调用子类的f()了。
总结:
虚函数的做用是容许在派生类中从新定义与基类同名的函数,而且能够经过基类指针或引用来访问基类和派生类中的同名函数。
1, 当在基类中不能为虚函数给出一个有意义的实现时,能够将其声明为纯虚函数,其实现留待派生类完成。
2, 纯虚函数的做用是为派生类提供一个一致的接口。
纯虚函数不能实例化,但能够声明指针。
类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。抽象类不能被用于实例化对象,它只能做为接口使用。
#include <iostream> using namespace std; class A { public: int iValue; }; class B :public A { public: void bPrintf(){ cout << "This is class B" << endl; }; }; class C :public A { public: void cPrintf(){ cout << "This is class C" << endl; }; }; class D :public B, public C { public: void dPrintf(){ cout << "This is class D" << endl; }; }; void main() { D d; // cout << d.iValue << endl; //错误,不明确的访问 cout << d.A::iValue << endl; //正确 cout << d.B::iValue << endl; //正确 cout << d.C::iValue << endl; //正确 }
类B C都继承了类A的iValue成员,所以类B C都有一个成员变量iValue ,而类D又继承了B C,这样类D就有一个重名的成员 iValue(一个是从类B中继承过来的,一个是从类C中继承过来的).在主函数中调用d.iValue 由于类D有一个重名的成员iValue编译器不知道调用 从谁继承过来的iValue因此就产生的二义性的问题.正确的作法应该是加上做用域限定符 d.B::iValue 表示调用从B类继承过来的iValue。不过 类D的实例中就有多个iValue的实例,就会占用内存空间。因此C++中就引用了虚基类的概念,来解决这个问题。
class A { public: int iValue; }; class B:virtual public A { public: void bPrintf(){cout<<"This is class B"<<endl;}; }; class C:virtual public A { public: void cPrintf(){cout<<"This is class C"<<endl;}; }; class D:public B,public C { public: void dPrintf(){cout<<"This is class D"<<endl;}; }; void main() { D d; cout<<d.iValue<<endl; //正确 }
在继承的类的前面加上virtual关键字表示被继承的类是一个虚基类,它的被继承成员在派生类中只保留一个实例。例如iValue这个成员,从类 D这个角度上来看,它是从类B与类C继承过来的,而类B C又是从类A继承过来的,但它们只保留一个副本。所以在主函数中调用d.iValue时就不 会产生错误。
总结:
将析构函数定义为虚函数主要缘由是由于多态的存在。
#include <iostream> class Base { public: Base(){ std::cout << "Constructing Base!" << std::endl; }; ~Base() { std::cout << "Destroy Base!" << std::endl; }; }; class Derive : public Base { public: Derive(){ std::cout << "Constructing Derive!" << std::endl; }; ~Derive() { std::cout << "Destroy Derive!" << std::endl; }; }; int main() { Base *basePtr = new Derive(); delete basePtr; return 0; } //打印结果为: Constructing Base! Constructing Derive! Destroy Base! //只删除了基类的分配的空间,派生类的对象的空间没有删除,会形成内存泄漏。
析构函数应是虚函数,除非类不用作基类。
由虚函数表,咱们知道,若析构函数不声明为virtual,则调用的将是Base类的析构函数,而没有调用Derive类的析构函数,此时形成了内存泄露。
因此析构函数必须声明为虚函数,调用的将是子类Derive的析构函数,
咱们还须要知道的一点是,子类析构函数,必定会调用父类析构函数,释放父类对象,则内存安全释放。
析构函数的调用顺序为先调用派生类析构函数清理新增的成员,再调用子对象析构函数(基类析构函数)清理子对象,最后再调用基类析构函数清理基类成员。
#include <iostream> class Base { public: Base(){ std::cout << "Constructing Base!" << std::endl; }; virtual ~Base() { std::cout << "Destroy Base!" << std::endl; }; }; class Derive : public Base { public: Derive(){ std::cout << "Constructing Derive!" << std::endl; }; ~Derive() { std::cout << "Destroy Derive!" << std::endl; }; }; int main() { Base *basePtr = new Derive(); delete basePtr; return 0; } //打印结果: Constructing Base! Constructing Derive! Destroy Derive! Destroy Base!
1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这你们都知道,但是这个指向vtable的指针实际上是存储在对象的内存空间的。问题出来了,若是构造函数是虚的,就须要经过 vtable来调用,但是对象尚未实例化,也就是内存空间尚未,怎么找vtable呢?因此构造函数不能是虚函数。
2. 从使用角度,虚函数主要用于在信息不全的状况下,能使重载的函数获得对应的调用。构造函数自己就是要初始化实例,那使用虚函数也没有实际意义呀。因此构造函数没有必要是虚函数。虚函数的做用在于经过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在建立对象时自动调用的,不可能经过父类的指针或者引用去调用,所以也就规定构造函数不能是虚函数。
3. 构造函数不须要是虚函数,也不容许是虚函数,由于建立一个对象时咱们老是要明确指定对象的类型,尽管咱们可能经过实验室的基类的指针或引用去访问它但析构却不必定,咱们每每经过基类的指针来销毁对象。这时候若是析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4. 从实现上看,vbtl在构造函数调用后才创建,于是构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能肯定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的做用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。
5. 当一个构造函数被调用时,它作的首要的事情之一是初始化它的VPTR。所以,它只能知道它是“当前”类的,而彻底忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。因此它使用的VPTR必须是对于这个类的VTABLE。并且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但若是接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数肯定的。这就是为何构造函数调用是从基类到更加派生类顺序的另外一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它本身的VTABLE。若是函数调用使用虚机制,它将只产生经过它本身的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。