1 、概念
多态:同一指令,针对不同对象,产生不同行为。
多态的类型
- 静态多态(静态联编):编译时多态。形式:函数重载、运算符重载、模板。
- 动态多态(动态联编):运行时多态。形式:虚函数。
多态与虚函数不是等价的,动态多态的体现必须要有虚函数,调用虚函数并不一定体现多态。
2、虚函数
2.1、虚函数的定义
虚函数:在成员函数前加 virtual 关键字。
派生类重写基类的虚函数
- 函数同名
- 返回值类型相同
- 参数列表相同:参数类型、参数个数、参数顺序
可选关键字
override
:父类使用虚函数,子类对虚函数重写。添加override
后,若重写的不是父类虚函数则报错。final
:不希望某个类被继承或某个虚函数被重写。添加final
后,若被继承或重写,编译器报错。
* 不能设置为虚函数的函数
- 普通函数:非成员函数
- 静态成员函数:编译时绑定。虚函数的调用需要对象,需要 this 指针,而静态函数没有 this 指针,无法访问虚函数指针 vfptr。
- 内联成员函数:没有必要,内联函数本身是为了减少函数调用的代价,而虚函数需要创建虚函数表,失去内联的意义。
- 非成员函数的友元函数
- 构造函数:
- 从继承观点来看,构造函数不能被继承,而虚函数可以被派生类重写。
- 从存储角度,如果构造函数是虚函数,则需用通过虚表来调用,但是对象还没有实例化,没有内存空间,无法通过虚函数指针找到虚表。
- 从语义角度,构造函数就是为了初始化数据成员,然而虚函数是为了在完全不了解细节情况下也能正确处理对象,虚函数要对不同类型的对象产生不同的动作。如果构造函数是虚函数,那么对象都没有产生,无法完成想要的操作。
2.2、虚函数的实现机制
2.2.1、实现原理
- 虚函数指针 vfptr:指向虚表
- 虚函数表(虚表):虚函数的入口地址。注意:多基继承时,只有第一个虚表存放虚函数入口地址,其他虚表存放跳转指令,指向第一个虚表。
当基类定义虚函数的时候,就会在基类对象的存储布局的前面多一个虚函数指针,指向自己的虚函数表,存放虚函数的入口地址。当派生类继承该基类的时候,把基类的虚函数吸收过来,派生类虚函数指针指向自己的虚函数表,若派生类重写该虚函数,则派生类虚函数表中对应的虚函数的入口地址被覆盖 override。
2.2.2、多态被激活的条件
- 基类定义虚函数
- 派生类重定义虚函数
- 创建派生类对象
- 基类的指针(引用)指向(绑定)到派生类对象
- 使用基类指针(引用)调用虚函数
2.2.3、测试虚表
测试虚表的存在,一级指针指向派生类对象(虚表的首地址),二级指针指向虚表中的虚函数,打印虚表。
#include <iostream> using std::cout; using std::endl; class Base { public: Base(long base) : _base(base) { cout << "Base(long)" << endl; } virtual void func1() { cout << "Base::func1()" << endl; } virtual void func2() { cout << "Base::func2()" << endl; } virtual void func3() { cout << "Base::func3()" << endl; } private: long _base; }; class Derived : public Base { public: Derived(long base, long derived) : Base(base) , _derived(derived) { cout << "Derived(long,long)" << endl; } virtual void func1() { cout << "Derived::func1() _derived:" << _derived << endl; } virtual void func2() { cout << "Derived::func2()" << endl; } private: long _derived; }; // 通过二级指针验证虚函数表的存在, void test() { Derived d(10, 100); cout << "----- 打印虚函数表中的虚函数地址 -----" << endl; // 一级指针,指向派生类对象的首地址,即虚函数表的地址 long *pvtable = (long*)&d; for(int idx = 0; idx < 3; ++idx) { // 打印虚函数的地址 cout << pvtable[idx] << endl; } cout << "----- 调用虚函数表中的虚函数,不传 this 指针 -----" << endl; // 二级指针,指向派生类对象地址的地址,即虚函数的地址 long **pVtable = (long **)&d; typedef void(* Function)(); for(int idx = 0; idx < 3; ++idx) { // 回调虚函数,没有传this指针 Function f = (Function)pVtable[0][idx]; f(); } cout << "----- 调用虚函数表中的虚函数,传入 this 指针-----" << endl; typedef void (*Function2)(Derived*); for(int idx = 0; idx < 3; ++idx) { // 回调虚函数,传入this指针 Function2 f2 = (Function2)pVtable[0][idx]; f2(&d); } } int main(void) { test(); return 0; }
2.3、虚函数的访问
- 指针访问:基类的指针指向派生类对象,动态联编,体现多态
- 引用访问:基类的引用绑定派生类对象,动态联编,体现多态
- 成员函数访问: this 指针调用虚函数,表现动态多态
- 对象访问:对象调用虚函数,静态联编,不体现多态
- 构造函数或析构函数:静态联编,只会调用自己的虚函数
测试在构造函数或析构函数中,调用虚函数
#include <iostream> using std::cout; using std::endl; class Grandpa { public: Grandpa() { cout << "Grandpa()" << endl; } virtual void func1() { cout << "Grandpa::func1()" << endl; } virtual void func2() { cout << "Grandpa::func2()" << endl; } ~Grandpa() { cout << "~Grandpa()" << endl; } }; class Father: public Grandpa { public: Father() { cout << "Father()" << endl; func1(); // 构造函数调用虚函数 } virtual void func1() { cout << "Father::func1()" << endl; } virtual void func2() { cout << "Father::func2()" << endl; } ~Father() { cout << "~Father()" << endl; func2(); // 析构函数调用虚函数 } }; class Son: public Father { public: Son() { cout << "Son()" << endl; } virtual void func1() { cout << "Son::func1()" << endl; } virtual void func2() { cout << "Son::func2()" << endl; } ~Son() { cout << "~Son()" << endl; } }; int main() { Son son; // 栈对象,自动销毁 return 0; }
测试结果:构造函数或析构函数中调用虚函数,表现的是静态联编,只会调用自己的虚函数
/* 构造过程:grandfather -> father -> son 析构过程:~son-> ~father -> ~grandfather */ Grandpa() Father() Father::func1() // son还未创建,此时只能调用father的func1 Son() ~Son() ~Father() Father::func2() // son已经销毁,此时只能调用father的func2 ~Grandpa()
2.4 、纯虚函数
纯虚函数:只有声明,没有实现,作为函数接口存在。
virtual 返回类型 函数名(参数列表) = 0;
2.4.1、抽象类
抽象类作为函数接口存在,不能创建对象,但可以创建抽象类的指针和引用
抽象类的形式
- 纯虚函数:声明了纯虚函数的类,就是抽象类。若派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。
- protected 修饰构造函数的类。可以派生新类,但不能创建对象。
// 基类定义为抽象类,不能创建基类对象 class Base { protected: Base(long base): _base(base) {} protected: long _base; }; class Derived : public Base { public: Derived(long base, long derived) : Base(base) // 可以调用基类的构造函数,创建派生类 , _derived(derived) {} private: long _derived; }
2.4.2、开闭原则
面向对象的设计原则:开闭原则
特点:对扩展开放,对修改关闭
测试
#include <math.h> #include <iostream> using std::cout; using std::endl; // 抽象类:纯虚函数作为函数接口 class Figure { public: virtual void display() const = 0; virtual double area() const = 0; }; // 通过引用访问虚函数,表现动态多态 void func(const Figure &fig) { fig.display(); cout << "'s area is : " << fig.area() << endl; } class Rectangle: public Figure { public: Rectangle(double length = 0, double width = 0) : _length(length) , _width(width) { cout << "Rectangle(double = 0, double = 0)" << endl;} void display() const override { cout << "Rectangle "; } double area() const override { return _length * _width; } ~Rectangle() { cout << "~Rectangle()" << endl; } private: double _length; double _width; }; class Circle: public Figure { public: Circle(double radius = 0) : _radius(radius) { cout << "Circle(double = 0)" << endl; } void display() const override { cout << "Circle "; } double area() const override { return _radius * _radius * 3.14159; } ~Circle() { cout << "~Circle()" << endl; } private: double _radius; }; class Triangle : public Figure { public: Triangle(double a = 0, double b = 0, double c = 0) : _a(a) , _b(b) , _c(c) { cout << "Triangle(double = 0, double = 0, double = 0)" << endl; } void display() const override { cout << "Triangle " ; } double area() const override { double tmp = (_a + _b + _c)/2; return sqrt(tmp * (tmp - _a) * (tmp - _b) * (tmp - _c)); } ~Triangle() { cout << "~Triangle()" << endl; } private: double _a; double _b; double _c; }; int main(int argc, char **argv) { Rectangle rectangle(10, 12); Circle circle(10); Triangle triangle(3, 4, 5); cout << endl; func(rectangle); func(circle); func(triangle); return 0; }
2.5、虚析构函数
多态的问题:如果一个基类的指针指向派生类的对象,当 delete 该指针释放派生类对象,系统只会执行基类的析构函数,不会执行派生类的析构函数,发生内存泄漏。
为了防止内存泄漏,只要基类中定义了虚函数,必须将基类的析构函数设置为虚函数,派生类的析构函数自动成为为虚函数。
基类和派生类的函数名虽然看起来不同,不符合重写规则,但实际上每个类只有一个析构函数,编译器将析构函数名统一解释为 destructor,实现重写。
例:父类是虚析构函数,子类重写了父类的析构函数,当 delete base 指针时 pbase->~destructor
即重写后的子类析构函数,子类析构后,调用父类的析构函数。
#include <string.h> #include <iostream> using std::cout; using std::endl; class Base { public: Base(const char *pbase) : _pbase(new char[strlen(pbase) + 1]()) { cout << "Base(const char *)" << endl; strcpy(_pbase, pbase); } // 将基类的析构函数声明为虚函数:~destructor virtual ~Base() { cout << "~Base()" << endl; if(_pbase) { delete [] _pbase; _pbase = nullptr; } } private: char *_pbase; }; class Derived: public Base { public: Derived(const char *pbase, const char *pderived) : Base(pbase) , _pderived(new char[strlen(pderived) + 1]()) { cout << "Derived(const char *, const char *)" << endl; strcpy(_pderived, pderived); } // 重写发生在派生类的析构函数: ~destructor // 基类的析构函数虚化 -> 派生类的析构函数虚化 -> 名字相同发生重写 ~Derived() { cout << "~Derived()" << endl; if(_pderived) { delete [] _pderived; _pderived = nullptr; } } private: char *_pderived; }; int main() { Base *pbase = new Derived("hello", "world"); // 执行析构函数 pbase->~destructor() // 先执行派生类对象的析构函数,再执行基类(Base*)的析构函数 delete pbase; return 0; }
2.6、重载 覆盖 隐藏
- 重载:同一个作用域,函数名相同,参数列表不同。
- 覆盖 | 重定义 | 重写:基类与派生类中的虚函数,函数名相同,参数列表相同。
- 隐藏:基类与派生类,函数名相同,派生类屏蔽了基类的同名数据成员。使用基类的作用域才能访问到其同名函数。