C++:多态

简介: C++:多态

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、重载 覆盖 隐藏

  • 重载:同一个作用域,函数名相同,参数列表不同。
  • 覆盖 | 重定义 | 重写:基类与派生类中的虚函数,函数名相同,参数列表相同。
  • 隐藏:基类与派生类,函数名相同,派生类屏蔽了基类的同名数据成员。使用基类的作用域才能访问到其同名函数。
相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
38 2
C++入门12——详解多态1
|
6月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
150 1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
79 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
53 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
74 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
54 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱