博客 / 詳情

返回

每日一個C++知識點|面向對象之多態

C++面向對象的三大特性是封裝,繼承,多態。上兩篇文章分別討論了封裝和繼承,今天主要是講解C++的另一個面向對象的特性~~多態

多態的概念

什麼是多態呢?
多態的核心是"同一個接口,不同的實現"
簡單來説,就是調用同一個函數名,程序會根據上下文和調用對象的實際類型來自動執行對應的函數邏輯,後面我們將會用代碼來舉例説明

多態的分類

C++多態分為靜態多態動態多態

靜態多態

靜態多態是編譯時多態,編譯器在編譯階段就確定要調用的函數版本,主要通過函數重載、模板實現,用代碼舉例如下:

#include <iostream>
using namespace std;

// 重載1:計算兩個整數的和
int add(int a,int b) {
    return a + b;
}

// 重載2:計算三個浮點數的和
double add(double a,double b,double c) {
    return a + b + c;
}

int main() {
    cout << add(1,2) << endl;// 調用int版本,輸出3
    cout << add(1.1,2.2,3.3) << endl; // 調用double版本,輸出6.6
    return 0;
}

這裏編譯器根據參數的個數、類型,在編譯時就確定了要調用的add()函數

動態多態

動態多態是運行時多態,程序在運行階段才確定要調用的函數版本

動態多態的實現需要以下三個條件

  1. 存在繼承關係
  2. 父類聲明虛函數,子類重寫該虛函數
  3. 使用父類的指針指向子類對象

由於還沒有説到虛函數,暫時先不用代碼舉例,等説完虛函數再一併舉例

虛函數

上面説到虛函數是動態多態實現的必要條件之一,那麼什麼是虛函數呢?

普通虛函數

虛函數是在父類中用virtual關鍵字修飾的成員函數,調用時要根據對象的實際類型來動態綁定,而不是編譯時固定綁定

// 父類:圖形
class Shape {
public:
    // 虛函數:繪製圖形
    virtual void draw() {
        cout << "繪製基礎圖形" << endl;
    }
    double getArea(){
        return 1;    
    }
};

上述代碼中,用virtual關鍵字修飾的draw()函數就是虛函數,調用時要根據對象的實際類型來決定其內容,假如它的子類如下所示:

// 子類:圓形
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // 重寫父類的虛函數(override關鍵字可選,但建議加)
    void draw() override {
        cout << "繪製半徑為" << radius << "的圓形" << endl;
     }   
    double getArea(){
        return M_PI * radius * radius;
    } 
};

上述代碼中子類Circle的成員函數draw()有關鍵字override來修飾,是對父類虛函數draw()的重寫,假如調用情況如下所示:

int main() {
    // 父類指針指向子類對象(多態的關鍵)
    Shape* shape = new Circle(5.0);

    // 調用的是子類Circle的draw和getArea(動態綁定)
    shape->draw();       // 輸出:繪製半徑為5的圓形
    cout << "面積:" << shape->getArea() << endl; // 輸出:面積:78.5398

    delete shape;
    return 0;
}

由於實際的對象由Shape* shape = new Circle(5.0);決定,所以draw()函數是執行Circle類的內容而不是父類Shape的內容

這就是虛函數的內容,也是動態多態的內容(滿足動態多態的三個條件)

虛函數的實現原理

我們已經知道了虛函數的用法,那麼虛函數是怎麼實現動態多態的呢?下面我們簡單瞭解其原理

編譯器為了實現虛函數的動態綁定做了兩件事:分別是創建虛函數表添加虛表指針

虛函數表是每個包含虛函數的類(包括父類和子類)都會有一個獨立的虛函數表,表中存儲了該類所有虛函數的地址。上述代碼中Shape類和Circle類都有各自的虛函數表,表中存儲虛函數draw()的地址

虛表指針是每個對象會包含一個隱藏的虛表指針,指向所屬類的虛函數表。上述代碼Shape* shape = new Circle(5.0);中的對象shape存在一個虛函數指針,指向Circle類裏的虛函數表裏的draw()的地址

當程序運行時,通過對象的虛表指針找到對應的虛函數表,再調用表中的函數地址,從而實現 “根據對象實際類型調用函數”

純虛函數

上述虛函數的例子是有實際意義的,有時候父類的虛函數沒有實現意義,這時可以定義純虛函數,格式是在函數後加 = 0

class Shape {
public:
    // 純虛函數:沒有函數體,強制子類必須重寫
    virtual double getArea() = 0;
    // 普通虛函數可以有默認實現
    virtual void draw() {
        std::cout << "繪製基礎圖形" << std::endl;
    }
    virtual ~Shape() {}
};

包含純虛函數的類稱為抽象類,不能實例化對象,只能作為基類被繼承,同時子類必須重寫所有純虛函數

虛析構函數

當類中有虛函數時,必須將析構函數聲明為虛函數,否則會導致內存泄漏,代碼如下:

#include <iostream>
#include <cmath>
using namespace std;

class Shape {
public:
    // 1. 業務虛函數(多態的核心接口)
    virtual void draw() = 0; // 純虛函數,強制子類實現
    virtual double getArea() = 0;

    // 2. 虛析構函數(配合多態刪除場景,必須加)
    virtual ~Shape() {
        cout << "Shape析構" << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // 重寫業務虛函數
    void draw() override {
        cout << "繪製圓形,半徑:" << radius << endl;
    }

    double getArea() override {
        return M_PI * radius * radius;
    }

    ~Circle() override {
        cout << "Circle析構" << endl;
    }
};

int main() {
    // 多態場景:父類指針指向子類對象
    Shape* shape = new Circle(5.0);
    shape->draw(); // 調用子類的draw(業務虛函數的作用)
    delete shape;  // 調用子類的析構(虛析構函數的作用)
    return 0;
}

上述代碼中由於Shape類和Circle類都有虛函數,所以必須要有虛析構函數,避免內存泄漏

如果想更深入瞭解虛析構函數可以看我往期關於虛析構函數的文章,那裏有更具體的描述~

總結

本文通過動態的概念、分類、虛函數、純虛函數、虛析構函數這幾個方面來描述了C++面向對象之多態的內容,並通過具體代碼示例來深入分析多態的實現原理和演示多態的實現過程

本文暫時寫到這裏,如果文章對你有用的話歡迎點贊收藏~

感興趣的話也可以關注我賬號,我會持續輸出C++相關的內容~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.