本文为 C++ 学习笔记,参考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代码大全》第 2 版。ios
多态(Polymorphism)是面向对象语言的一种特征,可能使用类似的方式(基类中的接口)处理不一样类型的对象。在编码时,咱们将不一样类型(具备继承层次关系的基类和派生类)的对象视为基类对象进行统一处理,没必要关注各派生类的细节,在运行时,将会经过相应机制执行各对象所属的类中的方法。多态是一种很是强大的机制,咱们考虑这种状况,基类早已写好并定义了良好的接口,基类的使用者编写代码时,将能经过基类的接口来调用派生类中的方法,也就是说,后写的代码能被先写的代码调用,这使程序具备很强的复用性和扩展性。程序员
看以下例程:编程
#include <iostream> using namespace std; class Fish { public: /* virtual */ void Swim() { cout << "Fish swims!" << endl; } }; class Tuna : public Fish { public: void Swim() { cout << "Tuna swims!" << endl; } }; class Carp : public Fish { public: void Swim() { cout << "Carp swims!" << endl; } }; void FishSwim(Fish &fish) { fish.Swim(); } int main() { // 引用形式 Fish myFish; Tuna myTuna; Carp myCarp; FishSwim(myFish); FishSwim(myTuna); FishSwim(myCarp); // 引用形式 Fish &rFish1 = myFish; Fish &rFish2 = myTuna; Fish &rFish3 = myCarp; rFish1.Swim(); rFish2.Swim(); rFish3.Swim(); // 指针形式 Fish *pFish1 = new Fish(); Fish *pFish2 = new Tuna(); Fish *pFish3 = new Carp(); pFish1->Swim(); pFish2->Swim(); pFish3->Swim(); return 0; }
直接编译运行代码,获得以下结果:数组
Fish swims! Fish swims! Fish swims! Fish swims! Fish swims! Fish swims! Fish swims! Fish swims! Fish swims!
将第 7 行的 virtual 关键字取消注释,再次编译运行代码,获得以下结果:ide
Fish swims! Tuna swims! Carp swims! Fish swims! Tuna swims! Carp swims! Fish swims! Tuna swims! Carp swims!
分析上述例程:函数
使用基类指针或引用指向基类或派生类对象,运行时调用对象所属的类(具备继承层次关系的基类或派生类)中的方法,这就是多态。在编写代码时,可将派生类对象视为基类对象进行统一处理,据此咱们能够先实现一个通用接口,如第 29 行 FishSwim() 函数所示,运行时具体调用哪一个方法由传入的参数决定。学习
编程实践:对于将被派生类覆盖的基类方法,务必将其声明为虚函数,以使其支持多态。this
虚析构函数与普通虚函数机制并没有不一样。编码
若是不将析构函数声明为虚函数,那么若是一个函数的形参是基类指针,实参是指向堆内存的派生类指针时,函数返回时做为实参的派生类指针将被看成基类指针进行析构,这会致使资源释放不彻底和内存泄漏;要避免这一问题,可将基类的析构函数声明为虚函数,那么函数返回时,做为实参的派生类指针就会被看成派生类指针进行析构。spa
换句话说,对于使用 new 在堆内存中实例化的派生类对象,若是将其赋给基类指针,并经过基类指针调用 delete,若是基类析构函数不是虚函数,delete 将按基类析构的方式来析构此指针,若是基类析构函数是虚函数,delete 将按派生类析构的方式来析构此指针(调用派生类析构函数和基类析构函数)。
编程实践:务必要将基类的析构函数声明为虚函数,以免派生类实例未被妥善销毁的状况发生。
为方便说明,将第一节代码加以修改,以下:
#include <iostream> using namespace std; class Fish { public: virtual void Swim() { cout << "Fish swims!" << endl; } }; class Tuna : public Fish { public: void Swim() { cout << "Tuna swims!" << endl; } }; class Carp:public Fish { public: void Swim() { cout << "Carp swims!" << endl; } }; int main() { Fish *pFish = NULL; pFish = new Fish(); pFish->Swim(); pFish = new Tuna(); pFish->Swim(); pFish = new Carp(); pFish->Swim(); return 0; }
编译运行的输出结果为:
Fish swims! Tuna swims! Carp swims!
例程中使用统一类型(基类)的指针 pFish 指向不一样类型(基类或派生类)的对象,指针的赋值是在运行阶段执行的,在编译阶段,编译器把 pFish 认做 Fish 类型的指针,而并不知道 pFish 指向的是哪一种类型的对象,没法肯定将执行哪一个类中的 Swim() 方法。调用哪一个类中的 Swim() 方法显然是在运行阶段决定的,这是使用实现多态的逻辑完成的,而这种逻辑由编译器在编译阶段提供。
以下 Base 类声明了 N 个虚函数:
class Base { public: virtual void Func1() { // Func1 implementation } virtual void Func2() { // Func2 implementation } // .. so on and so forth virtual void FuncN() { // FuncN implementation } };
以下 Derived 类继承自 Base 类,并覆盖了除 Base::Func2() 外的其余全部虚函数。
class Derived: public Base { public: virtual void Func1() { // Func2 overrides Base::Func2() } // no implementation for Func2() // .. so on and so forth virtual void FuncN() { // FuncN overrides Base::FuncN() } };
编译器见到这种继承层次结构后,知道 Base 定义了一些虚函数,并在 Derived 中覆盖了它们。在这种状况下,编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别建立一个虚函数表(Virtual Function Table, VFT)。换句话说,Base 和 Derived 类都将有本身的虚函数表。实例化这些类的对象时,会为每一个对象建立一个隐藏的指针(咱们称之为 VFT*),它指向相应的 VFT。可将 VFT 视为一个包含函数指针的静态数组,其中每一个指针都指向相应的虚函数,以下图所示:
每一个虚函数表都由函数指针组成,其中每一个指针都指向相应虚函数的实现。在类 Derived 的虚函数表中,除一个函数指针外,其余全部函数指针都指向 Derived 本地的虚函数实现。Derived 没有覆盖 Base::Func2(),所以相应的函数指针指向 Base 类的 Func2() 实现。
下述代码调用未覆盖的虚函数,编译器将查找 Derived 类的 VFT,最终调用的是 Base::Func2() 的实现:
Derived objDerived; objDerived.Func2();
调用被覆盖的虚函数时,也是相似的机制:
void DoSomething(Base& objBase) { objBase.Func1(); // invoke Derived::Func1 } int main() { Derived objDerived; DoSomething(objDerived); };
在这种状况下,虽然将 objDerived 传递给了 objBase,进而被解读为一个 Base 实例,但该实例的 VFT 指针仍指向 Derived 类的虚函数表,所以经过该 VTF 执行的是 Derived::Func1()。
C++ 就是经过虚函数表实现多态的。
#include <iostream> using namespace std; class Class1 { private: int a, b; public: void DoSomething() {} }; class Class2 { private: int a, b; public: virtual void DoSomething() {} }; int main() { cout << "sizeof(Class1) = " << sizeof(Class1) << endl; cout << "sizeof(Class2) = " << sizeof(Class2) << endl; return 0; }
在 64 位系统下编译并运行,结果为:
sizeof(Class1) = 8 sizeof(Class2) = 16
Class2 中将函数声明为虚函数,所以类的成员多了一个 VFT 指针,64 位系统中,指针变量占用 8 字节空间,所以 Class2 比 Class1 多占用了 8 个字节。
#include <iostream> using namespace std; class Base { private: int x = 1; int y = 2; const static int z = 3; /* 注释1 public: virtual void test() {}; */ }; class Derived : public Base { private: int u = 11; int v = 22; const static int w = 33; /* 注释2 public: virtual void test() {}; */ }; int main() { Base base; Derived derived; cout << "sizeof(Base) = " << sizeof(Base) << endl; cout << "sizeof(Derived) = " << sizeof(Derived) << endl; return 0; }
上述代码运行结果为:
sizeof(Base) = 8 sizeof(Derived) = 16
使用 gdb 查看变量值:
(gdb) p base $1 = {x = 1, y = 2, static z = 3} (gdb) p derived $2 = {<Base> = {x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}
取消“注释1”处的注释,运行结果为:
sizeof(Base) = 16 sizeof(Derived) = 24
使用 gdb 查看变量值:
(gdb) p base $1 = {_vptr.Base = 0x400b10 <vtable for Base+16>, x = 1, y = 2, static z = 3} (gdb) p derived $2 = {<Base> = {_vptr.Base = 0x400af0 <vtable for Derived+16>, x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}
取消“注释1”和“注释2”处的注释,运行结果为:
sizeof(Base) = 16 sizeof(Derived) = 24
使用 gdb 查看变量值:
(gdb) p base $1 = {_vptr.Base = 0x400b10 <vtable for Base+16>, x = 1, y = 2, static z = 3} (gdb) p derived $2 = {<Base> = {_vptr.Base = 0x400af0 <vtable for Derived+16>, x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}
根据上述实验结果,给出结论:
在 C++ 中,包含纯虚函数的类是抽象基类。抽象基类用于定义接口,在派生类中实现接口,这样能够实现接口与实现的分离。抽象基类不能被实例化。抽象基类提供了一种很是好的机制,可在基类声明全部派生类都必须实现的函数接口,将这些派生类中必须实现的接口声明为纯虚函数便可。
纯虚函数写法以下:
class AbstractBase { public: virtual void DoSomething() = 0; // pure virtual method };
其派生类中必须实现此函数。
分析下列代码:
#include <iostream> #include <stdio.h> using namespace std; class B { public: virtual void func1() = 0; // 纯虚函数不能在基类中实现,必定要在派生类中实现 virtual void func2() = 0; // 纯虚函数不能在基类中实现,必定要在派生类中实现 virtual void func3() { cout << "B::func3" << endl; } // 此虚函数被派生类中函数覆盖 virtual void func4() { cout << "B::func4" << endl; } // 此虚函数在派生类中无覆盖 void func5() { cout << "B::func5" << endl; } // 此函数被派生类中函数覆盖 void func6() { cout << "B::func6" << endl; } // 此函数在派生类中无覆盖 private: int x = 1; int y = 2; static int z; }; class D : public B { public: virtual void func1() override { cout << "D::func1" << endl; } virtual void func2() override { cout << "D::func2" << endl; } virtual void func3() override { cout << "D::func3" << endl; } void func5() { cout << "D::func5" << endl; } // 不能带 override private: int u = 11; int v = 22; static int w; }; int main() { // B b; // 编译错误,抽象基类不能被实例化 D d; cout << "sizeof(d) = " << sizeof(d) << endl; d.func1(); // 访问派生类中的覆盖函数(覆盖纯虚函数) d.func2(); // 访问派生类中的覆盖函数(覆盖纯虚函数) d.func3(); // 访问派生类中的覆盖函数(覆盖虚函数) d.func5(); // 访问派生类中的覆盖函数(覆盖普通函数) d.B::func3(); // 访问基类中的虚函数 d.B::func4(); // 访问基类中的虚函数 d.B::func5(); // 访问基类中的普通函数 return 0; }
上述代码运行结果:
sizeof(d) = 24 D::func1 D::func2 D::func3 D::func5 B::func3 B::func4 B::func5
结论以下:
一个类继承多个父类,而这多个父类又继承一个更高层次的父类时,会引起菱形问题。例如,鸭嘴兽具有哺乳动物、鸟类和爬行动物的特征,这意味着 Platypus 类须要继承 Mammal、 Bird 和 Reptile 三个类。然而,这些类都从同一个 Animal 类派生而来,以下图所示:
例程以下:
#include <iostream> using namespace std; class Animal { public: Animal() { cout << "Animal constructor" << endl; } int age; }; class Mammal : public /* virtual */ Animal { }; class Bird : public /* virtual */ Animal { }; class Reptile : public /* virtual */ Animal { }; class Platypus : public Mammal, public Bird, public Reptile { public: Platypus() { cout << "Platypus constructor" << endl; } }; int main() { Platypus duckBilledP; // uncomment next line to see compile failure // age is ambiguous as there are three instances of base Animal // duckBilledP.age = 25; duckBilledP.Mammal::age = 25; duckBilledP.Bird::age = 25; duckBilledP.Reptile::age = 25; return 0; }
编译并运行,输出结果以下:
Animal constructor Animal constructor Animal constructor Platypus constructor
可见,Platypus 有三个 Animal 实例。若是取消第 35 行的注释,编译没法经过,由于没法肯定是要设置哪一个 Animal 实例中的 age 成员。
若是取消第 十一、1五、19 行对关键字 virtual 的注释,再次编译运行,可看到以下输出结果:
Animal constructor Platypus constructor
此时,Platypus 只有一个 Animal 实例。可见使用虚继承能够解决多继承时的菱形问题,确保
在继承层次结构中,继承多个从同一个类派生而来的基类时,若是这些基类没有采用虚继承,将致使二义性。这种二义性被称为菱形问题(Diamond Problem)。
C++关键字 virtual 被用于实现两个不一样的概念,其含义随上下文而异,以下:
从 C++11 起,程序员可以使用限定符 override 来核实被覆盖的函数在基类中是否被声明为虚函数。形式以下:
class Fish { public: virtual void Swim() { cout << "Fish swims!" << endl; } }; class Tuna:public Fish { public: void Swim() const override // Error: no virtual fn with this sig in Fish { cout << "Tuna swims!" << endl; } void Swim() override // Right: has virtual fn with this sig in Fish { cout << "Tuna swims!" << endl; } };
换而言之, override 提供了一种强大的途径,让程序员可以明确地表达对基类的虚函数进行覆盖的意图,进而让编译器作以下检查:
• 基类函数是不是虚函数?
• 派生类中被声明为 override 的函数是不是基类中对应虚函数的覆盖?确保没有有手误写错。
编程实践:在派生类中声明要覆盖基类函数的函数时,务必使用关键字 override
被声明为 final 的类禁止继承,不能用做基类。而被声明为 final 的虚函数,不能在派生类中进行覆盖。
所以,要在 Tuna 类中禁止进一步定制虚函数 Swim(),可像下面这样作:
class Tuna:public Fish { public: // override Fish::Swim and make this final void Swim() override final { cout << "Tuna swims!" << endl; } };
Tuna 类能够被继承,但 Swim() 函数不能派生类中的实现覆盖。
答案是不能够。不可能实现虚复制构造函数,由于在基类方法声明中使用关键字 virtual 时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能建立固定类型的对象,不具有多态性,所以 C++不容许使用虚复制构造函数。
虽然如此,但存在一种不错的解决方案,就是定义本身的克隆函数来实现上述目的。这部份内容有些复杂,待用到时再做补充。