摘自《C++ Primer Plus》第6版13.3ios
示例:c++
#include<string> class Brass { private: std::string fullName; long acctNum; double balance; public: Brass(const std::string & s= "NullBody", long an=-1, double bal =0.0); void Despoit(double amt); virtual void Withdraw(double amt); double Balance() const; virtual void ViewAcct() const; virtual ~Brass(){} }; class BrassPlus: public Brass { private: double maxLoan; double rate; double owesBank; public: BrassPlus(const std::string & s= "NullBody", long an=-1, double bal =0.0, double ml=500, double r=0.1125); BrassPlus(const Brass & ba, double ml=500,double r=0.1125); virtual void ViewAcct() const; virtual void Withdraw(double amt); void ResetMax(double m) {maxLoan=m;} };
类Brass中加了Virtual关键字的函数就是虚函数 。程序员
若是方法是经过引用类型或指针而不是对象调用的,它将肯定使用哪种方法。若是没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;若是使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。编程
对于一个函数ViewAcct()来讲,若是ViewAcct()不是虚的,则程序的行为以下:dom
// behavior with non-virtual ViewAcct() // method chosen according to reference type Brass dom("Dominic Banker", 11224, 4183.45); BrassPlus dot("Dorothy Banker", 12118, 2592.00); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use Brass::ViewAcct()
引用变量的类型为Brass,因此选择了Brass::ViewAccount()。
使用Brass指针代替引用时,行为将与此相似。
若是ViewAcct()是虚的,则行为以下:编程语言
// behavior with virtual ViewAcct() // method chosen according to object Brass dom("Dominic Banker", 11224, 4183.45); BrassPlus dot("Dorothy Banker", 12118, 2592.00); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()
这里两个引用的类型都是Brass,但b2_ref引用的是一个BrassPlus对象,因此使用的是BrassPlus::ViewAcct()。使用Brass指针代替引用时,行为将相似。函数
基类声明了一个虚析构函数。这样作是为了确保释放派生对象时,按正确的顺序调用析构函数。spa
#include <iostream> using namespace std; class GrandFather { public: GrandFather() {} virtual void fun() { cout << "GrandFather call function!" << endl; } virtual ~GrandFather() { cout << "GrandFather destruction!" << endl; } }; class Father : public GrandFather { public: Father() {} void fun() { cout << "Father call function!" << endl; } ~Father() { cout << "Father destruction!" << endl; } }; class Son : public Father { public: Son() {} void fun() { cout << "Son call function!" << endl; } ~Son() { cout << "Son destruction!" << endl; } }; void print(GrandFather* p) { p->fun(); } int main(int argc, char* argv[]) { Father * pfather = new Son; delete pfather; return 0; }
运行结果:设计
Son destruction! Father destruction! GrandFather destruction!
若是基类 GrandFather 的析构函数不为虚,则执行结果为:指针
Father destruction! GrandFather destruction!
为什么须要虚析构函数
在程序清单13.10中,使用delete释放由new分配的对象的代码说明了为什么基类应包含一个虚析构函数,虽然有时好像并不须要析构函数。若是析构函数不是虚的,则将只调用对应与指针类型的析构函数。对于程序清单13.10,这意味着只有Brass的析构函数被调用,即便指针指向的是一个BrassPlus对象。若是析构函数是虚的,将调用相应对象类型的析构函数。所以,若是指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,而后自动调用基类的析构函数。所以,使用虚析构函数能够确保正确的析构函数序列被调用!
C++语言为咱们提供了一种语法结构,经过它能够指明,一个虚拟函数只是提供了一个可被子类型改写的接口。可是,它自己并不能经过虚拟机制被调用。这就是纯虚函数(pure virtual function)。 纯虚函数的声明以下所示:
class BaseEllipse //abstract base class { private: double x; //x-coordinate of the ellipse's center double y; //y-coordinate of the ellipse's center ....... public: BaseEllipse(double x0 = 0,double y0 = 0): x(x0), y(y0) {} virtual ~BaseEllipse() {} void Move(int nx, int ny){x = nx; y = ny;} virtual double Area() const =0; //a pure virual function ..... }
当类声明中包含纯虚函数时,则不能建立该类的对象。这里的理念是,包含纯虚函数的类只用做基类。原型中的=0使虚函数成为纯虚函数。这里的方法Area()没有定义,但c++甚至容许纯虚函数有定义。例如,也许全部的基类方法都与Move()同样,能够在基类中进行定义,但你仍须要将这个类声明为抽象的。在这种状况下,能够将原型声明为虚的:
virtual void Move(int nx, int ny) = 0;
这将使基类成为抽象的,但你仍能够在实现文件中提供方法的定义:
void BaseEllipse::Move(int nx, int ny) {x = nx; y = ny;}
总之在原型中使用=0指出类是一个抽象基类,在类中能够不定义该函数。
总结:抽象类只能做为基类来使用,其纯虚函数的实现由派生类给出。若是派生类没有从新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然仍是一个抽象类。若是派生类中给出了基类纯虚函数的实现,则该派生类就再也不是抽象类了,它是一个能够创建对象的具体类了。
虚继承主要解决多重继承带来的问题。
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
//间接基类A class A{ protected: int m_a; }; //直接基类B class B: virtual public A{ //虚继承 protected: int m_b; }; //直接基类C class C: virtual public A{ //虚继承 protected: int m_c; }; //派生类D class D: public B, public C{ public: void seta(int a){ m_a = a; } //正确 void setb(int b){ m_b = b; } //正确 void setc(int c){ m_c = c; } //正确 void setd(int d){ m_d = d; } //正确 private: int m_d; }; int main(){ D d; return 0; }
这段代码使用虚继承从新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的目的是让某个类作出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
如今让咱们从新梳理一下本例的继承关系,以下图所示:
图1:使用虚继承解决菱形继承中的命名冲突问题
观察这个新的继承体系,咱们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操做。在上图中,当定义 D 类时才出现了对虚派生的需求,可是若是 B 类和 C 类不是从 A 类虚派生获得的,那么 D 类仍是会保留 A 类的两份成员。
换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类自己。
在实际开发中,位于中间层次的基类将其继承声明为虚继承通常不会带来什么问题。一般状况下,使用虚继承的类层次是由一我的或者一个项目组一次性设计完成的。对于一个独立开发的类来讲,不多须要基类中的某一个类是虚基类,何况新类的开发者也没法改变已经存在的类体系。
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,不然将致使 iostream 类中保留两份 base_ios 类的成员。
图2:虚继承在C++标准库中的实际应用
由于在虚继承的最终派生类中只保留了一份虚基类的成员,因此该成员能够被直接访问,不会产生二义性。此外,若是虚基类的成员只被一条派生路径覆盖,那么仍然能够直接访问这个被覆盖的成员。可是若是该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪一个类。
以图1中的菱形继承为例,假设 B 定义了一个名为 x 的成员变量,当咱们在 D 中直接访问 x 时,会有三种可能性:
能够看到,使用多继承常常会出现二义性问题,必须十分当心。上面的例子是简单的,若是继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工做都会变得更加困难,所以我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的状况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是因为这个缘由,C++ 以后的不少面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
virtual可用来定义类函数和应用到虚继承。
友元函数, 构造函数不能用virtual关键字修饰;
普通成员函数和析构函数能够用virtual关键字修饰;