最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++多態(tài)詳解之從靜態(tài)多態(tài)到動態(tài)多態(tài)

 更新時間:2025年12月08日 10:07:07   作者:許長安  
C++多態(tài)技術(shù)是面向?qū)ο缶幊痰闹匾匦?這篇文章主要介紹了C++多態(tài)詳解之從靜態(tài)多態(tài)到動態(tài)多態(tài)的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下

一、什么是“多態(tài)”

從字面上理解,多態(tài)就是“多種形態(tài)”。在程序設(shè)計里,它指的是:

使用統(tǒng)一的接口,卻可以對不同類型的對象做出不同的具體行為。

更具體一點(diǎn):

  • 靜態(tài)多態(tài)(編譯期多態(tài))
    編譯器在編譯階段就能決定到底調(diào)用哪一個函數(shù)、用哪個版本的代碼。
    代表形式:函數(shù)重載、運(yùn)算符重載、模板(泛型)。
  • 動態(tài)多態(tài)(運(yùn)行期多態(tài))
    編譯時先只“知道有這個虛函數(shù)”,真正要調(diào)用哪個實(shí)現(xiàn)要到運(yùn)行時,根據(jù)對象的實(shí)際類型來決定。
    代表形式:virtual 虛函數(shù) + 繼承 + 基類指針/引用。

二、靜態(tài)多態(tài):編譯期就決定一切

1. 靜態(tài)多態(tài)的特點(diǎn)

“靜態(tài)”的含義是:綁定發(fā)生在編譯期。

  • 編譯器在編譯階段,就根據(jù)實(shí)參類型、模板參數(shù)等,把要調(diào)用的函數(shù)、生成的代碼都選好、生成好。
  • 運(yùn)行時不會再為了“選函數(shù)”去查表,因此不需要虛表(vtable),也沒額外的間接調(diào)用開銷。
  • 代價是:泛型代碼會在編譯期生成多個實(shí)例,代碼體積可能增長;另外有些行為必須在編譯期就能確定。

常見的靜態(tài)多態(tài)形式有三個:函數(shù)重載、運(yùn)算符重載、模板。

2. 函數(shù)重載

同名函數(shù),根據(jù)參數(shù)列表的不同進(jìn)行區(qū)分:

void print(int x) {
    std::cout << "int: " << x << std::endl;
}

void print(double x) {
    std::cout << "double: " << x << std::endl;
}

void print(const std::string& s) {
    std::cout << "string: " << s << std::endl;
}

int main() {
    print(10);           // 調(diào)用 print(int)
    print(3.14);         // 調(diào)用 print(double)
    print("hello");      // 字面量轉(zhuǎn)成 std::string,調(diào)用 print(const std::string&)
}

在這里,“多態(tài)”的表現(xiàn)是:同一個名字 print,可以處理不同的類型。
編譯器會在編譯期進(jìn)行“重載決議”,選出最合適的一個版本。這就是靜態(tài)多態(tài)。

補(bǔ)充一個和繼承相關(guān)的點(diǎn):
如果派生類中重新定義了與基類同名但參數(shù)不同的函數(shù),會發(fā)生“名字隱藏”。要想保留基類的其他重載,可以用 using Base::func; 把基類同名重載導(dǎo)入作用域。

3. 運(yùn)算符重載

運(yùn)算符重載本質(zhì)上也是一種函數(shù)重載,區(qū)別只是語法形式更自然。編譯器在編譯期決定調(diào)用哪個重載,所以它也是靜態(tài)多態(tài)。

struct Point {
    int x, y;

    Point(int x, int y) : x(x), y(y) {}

    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
};

int main() {
    Point a(1, 2), b(3, 4);
    Point c = a + b;  // 實(shí)際是調(diào)用 a.operator+(b)
    std::cout << c.x << ", " << c.y << std::endl;  // 4, 6
}

“同一個運(yùn)算符 +” 對于不同類型(例如 int + int、Point + Point)會產(chǎn)生不同的行為,同樣屬于靜態(tài)多態(tài)。

4. 模板與泛型編程

模板是 C++ 中實(shí)現(xiàn)靜態(tài)多態(tài)最強(qiáng)大的工具。函數(shù)模板和類模板都屬于參數(shù)化多態(tài),在編譯期根據(jù)類型參數(shù)生成具體代碼。

template <typename T>
T add(T a, T b) {
    return a + b;  // 只要求 T 支持 operator+
}

int main() {
    std::cout << add(1, 2) << std::endl;         // 實(shí)例化出 add<int>
    std::cout << add(1.5, 2.5) << std::endl;     // 實(shí)例化出 add<double>
    std::cout << add(std::string("a"), "b") << std::endl; // 實(shí)例化出 add<std::string>
}

這里的 add 在源代碼里只寫了一份,但編譯器會根據(jù)實(shí)際調(diào)用自動生成多個版本。

本質(zhì)上,它也是一種“接口相同(add),但根據(jù)類型不同產(chǎn)生不同行為”的多態(tài),只是全部發(fā)生在編譯期。

模板和函數(shù)重載還可以配合使用(例如 std::sort 接受不同類型的迭代器、不同的比較器),本質(zhì)上依然是靜態(tài)多態(tài)的一種組合形式。

三、動態(tài)多態(tài):運(yùn)行期由對象說了算

靜態(tài)多態(tài)的“主角”是“類型”和“模板參數(shù)”,它解決的是“類型不一樣怎么共享代碼”。
動態(tài)多態(tài)的“主角”是“對象的實(shí)際類型”,解決的是“一群有共同接口的對象,具體用哪個實(shí)現(xiàn)要到運(yùn)行期才知道”。

1. 動態(tài)多態(tài)的三個要素

C++ 中要用到動態(tài)多態(tài),基本需要三個條件:

  1. 繼承:有一個基類和若干派生類;
  2. 虛函數(shù):基類中把要多態(tài)調(diào)用的函數(shù)聲明為 virtual
  3. 通過基類指針或引用來操作派生類對象

經(jīng)典例子:

class Shape {
public:
    virtual void draw() {  // 虛函數(shù)
        std::cout << "Shape::draw" << std::endl;
    }

    virtual ~Shape() = default; // 虛析構(gòu),后面會講
};

class Circle : public Shape {
public:
    void draw() override {  // override 明確表明“重寫基類虛函數(shù)”
        std::cout << "Circle::draw" << std::endl;
    }
};

class Rect : public Shape {
public:
    void draw() override {
        std::cout << "Rect::draw" << std::endl;
    }
};

void render(Shape& s) {
    s.draw(); // 這里發(fā)生動態(tài)綁定
}

int main() {
    Circle c;
    Rect r;

    render(c); // 調(diào)用 Circle::draw
    render(r); // 調(diào)用 Rect::draw
}

這里 render 只認(rèn)識 Shape& 這個“統(tǒng)一接口”,但傳入不同的實(shí)際對象(CircleRect)時,會在運(yùn)行期調(diào)用不同版本的 draw。這就是運(yùn)行期多態(tài)。

注意:
如果是值傳遞,比如 void render(Shape s),那么會發(fā)生對象切片(object slicing),派生類部分被“切掉”,只剩下基類部分,動態(tài)多態(tài)就失效了。因此,多態(tài)場景下要習(xí)慣性使用指針或引用。

2. 虛函數(shù)表(vtable)與 vptr 的實(shí)現(xiàn)原理

典型實(shí)現(xiàn)(大多數(shù)主流編譯器采用類似思路)是這樣的:

  1. 每個含有虛函數(shù)的類,編譯器都會為它生成一張虛函數(shù)表(vtable),里面是一串“函數(shù)指針”;

  2. 每個對象里會隱藏一個指針(通常叫 vptr),指向它所屬類的那張?zhí)摵瘮?shù)表;

  3. 當(dāng)你寫 p->func1() 時,如果 func1 是虛函數(shù),編譯器會把它翻譯成類似:

    // 偽代碼
    p->vptr[func1_index](p);
    

    即:從對象中取出 vptr,根據(jù)函數(shù)在虛表中的位置,找到對應(yīng)的函數(shù)指針,然后調(diào)用。

情景:Base類寫了兩個虛函數(shù)func1和func2,在子類Derived類中重寫了func1沒有重寫func2

class Base {
public:
    virtual void func1();
    virtual void func2();
};

class Derived : public Base {
public:
    void func1() override;
    // 沒有重寫 func2()
};

那么典型的虛表布局可以想象為:

  • Base 的虛表大致為:

    index函數(shù)
    0Base::func1
    1Base::func2
  • Derived 的虛表大致為:

    index函數(shù)
    0Derived::func1
    1Base::func2

也就是說:
派生類重寫了哪個虛函數(shù),對應(yīng)虛表?xiàng)l目就改成指向派生類實(shí)現(xiàn);沒重寫的虛函數(shù),虛表里仍然指向基類實(shí)現(xiàn)。

構(gòu)造與析構(gòu)期間的 vptr

  • 構(gòu)造基類對象時,先設(shè)置 vptr 指向 基類 的虛表;
  • 構(gòu)造派生類對象時,在基類構(gòu)造結(jié)束后,再把 vptr 改成指向 派生類 虛表;
  • 析構(gòu)時順序相反。

這帶來的一個重要結(jié)論是:

在構(gòu)造函數(shù)或析構(gòu)函數(shù)內(nèi)部調(diào)用虛函數(shù)時,不會表現(xiàn)出“派生類版本”,而是調(diào)用當(dāng)前構(gòu)造/析構(gòu)階段對應(yīng)類的版本。這是為了避免訪問尚未構(gòu)造/已經(jīng)銷毀的派生類成員。

3. 抽象類與純虛函數(shù)

有時我們只關(guān)心接口,不希望有人直接創(chuàng)建這個類的實(shí)例,就可以使用純虛函數(shù)定義一個抽象類

class Shape {
public:
    virtual void draw() = 0;   // 純虛函數(shù)
    virtual ~Shape() = default;
};

特點(diǎn):

  • 含有(或繼承自基類的)至少一個純虛函數(shù)的類,就是抽象類;
  • 抽象類不能直接實(shí)例化:Shape s; // 編譯錯誤;
  • 派生類必須把這些純虛函數(shù)全部重寫,否則它自己也是抽象類。

抽象類非常適合用來作為“接口基類”,例如游戲引擎中常見的 GameObject 基類,定義一組必須實(shí)現(xiàn)的接口如 update(), render() 等。

4. 虛析構(gòu)函數(shù)與資源釋放

動態(tài)多態(tài)中,一個非常重要但容易忽略的點(diǎn)是:基類析構(gòu)函數(shù)要聲明為 virtual。

典型情景:

class Base {
public:
    virtual ~Base() { // 必須是虛析構(gòu)
        std::cout << "Base dtor\n";
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived dtor\n";
    }
};

int main() {
    Base* p = new Derived();
    delete p;
}

如果 ~Base() 不是虛函數(shù),那么 delete p; 只會調(diào)用 Base 的析構(gòu)函數(shù),而不會調(diào)用 Derived 的析構(gòu)函數(shù),導(dǎo)致派生類中資源泄漏。這在實(shí)際工程里非常危險。

只要你打算通過 Base*Base& 以多態(tài)方式管理對象生命周期,就應(yīng)該把基類析構(gòu)函數(shù)聲明為 virtual。

5. 動態(tài)多態(tài)的一些細(xì)節(jié)注意

5.1 默認(rèn)參數(shù)與虛函數(shù)

默認(rèn)參數(shù)是靜態(tài)綁定的:它們在編譯期根據(jù)靜態(tài)類型來決定。

class Base {
public:
    virtual void func(int x = 1) {
        std::cout << "Base: " << x << std::endl;
    }
};

class Derived : public Base {
public:
    void func(int x = 2) override {
        std::cout << "Derived: " << x << std::endl;
    }
};

int main() {
    Derived d;
    Base* p = &d;

    p->func(); // 輸出什么?
}

這里:

  • 調(diào)用的函數(shù)體是 Derived::func(虛函數(shù),運(yùn)行期綁定);
  • 但默認(rèn)參數(shù)值是以 p 的靜態(tài)類型 Base* 為準(zhǔn),所以默認(rèn)值是 1

最終輸出:Derived: 1。

所以建議:不要依賴虛函數(shù)的默認(rèn)參數(shù)來區(qū)分行為,或者干脆在基類中避免給虛函數(shù)提供默認(rèn)參數(shù)。

5.2 對象切片(object slicing)

Derived d;
Base b = d; // 發(fā)生對象切片

此時 b 只是一個獨(dú)立的 Base 對象,派生類部分被“切掉了”,多態(tài)自然不存在了。

因此,多態(tài)設(shè)計中一般采用 Base*Base& ,而不是按值傳遞/按值存儲。

四、靜態(tài)多態(tài) vs 動態(tài)多態(tài):對比與選擇

簡單對比一下兩者的特點(diǎn):

特性靜態(tài)多態(tài)(重載/模板)動態(tài)多態(tài)(虛函數(shù))
綁定時機(jī)編譯期運(yùn)行期
性能開銷無虛表開銷,通常更快通過虛表間接調(diào)用,有一點(diǎn)調(diào)用開銷
代碼體積模板實(shí)例化可能生成很多代碼一般較穩(wěn)定
靈活性編譯期就要知道所有類型可以運(yùn)行期決定具體類型
典型使用場景STL 算法、通用工具庫、數(shù)值計算等插件系統(tǒng)、UI 系統(tǒng)、游戲?qū)ο笙到y(tǒng)等
需要的語言特性函數(shù)重載、運(yùn)算符重載、模板繼承、虛函數(shù)、基類指針/引用

兩者不是“誰更高級”的關(guān)系,而是各有適用場景

  • 如果你寫的是通用算法、容器、工具庫,適合用模板等靜態(tài)多態(tài)手段;
  • 如果你有一組“類型不同但接口統(tǒng)一”的對象要在運(yùn)行期間統(tǒng)一管理,例如圖形界面控件、游戲里的各種實(shí)體、不同格式的文件解碼器,通常用動態(tài)多態(tài)更自然。

五、結(jié)合實(shí)際開發(fā)的幾個例子

1. 使用靜態(tài)多態(tài)寫通用算法

比如寫一個簡單版本的 for_each

template <typename It, typename Func>
void my_for_each(It first, It last, Func f) {
    for (; first != last; ++first) {
        f(*first);
    }
}

int main() {
    std::vector<int> v{1, 2, 3};

    my_for_each(v.begin(), v.end(), [](int x) {
        std::cout << x << " ";
    });
}
  • Func 可以是函數(shù)指針、函數(shù)對象、lambda;
  • It 可以是各種迭代器;
  • 編譯器會根據(jù)實(shí)際類型生成具體代碼,運(yùn)行時基本沒有額外開銷。

這就是典型的靜態(tài)多態(tài)用法,也是 STL 的設(shè)計思想。

2. 使用動態(tài)多態(tài)做“對象系統(tǒng)”(例如游戲里的實(shí)體)

假設(shè)一個游戲里有不同的實(shí)體:玩家、怪物、NPC,都需要 update()

class Entity {
public:
    virtual void update(float dt) = 0;    // 純虛函數(shù)
    virtual ~Entity() = default;
};

class Player : public Entity {
public:
    void update(float dt) override {
        // 處理玩家輸入、移動等
    }
};

class Monster : public Entity {
public:
    void update(float dt) override {
        // AI 行為
    }
};

void updateAll(std::vector<std::unique_ptr<Entity>>& entities, float dt) {
    for (auto& e : entities) {
        e->update(dt); // 動態(tài)多態(tài),運(yùn)行期調(diào)用對應(yīng)實(shí)體的 update
    }
}

在這里:

  • 游戲主循環(huán)只需要維持一個 std::vector<std::unique_ptr<Entity>>;
  • 不關(guān)心具體是 Player 還是 Monster,全部通過多態(tài)調(diào)用 update;
  • 這樣系統(tǒng)擴(kuò)展新實(shí)體時只要增加派生類和工廠邏輯就行,主循環(huán)不用改。

典型地,這種需要“運(yùn)行時混合多種類型”的場景,非常適合用動態(tài)多態(tài)。

六、小結(jié)

  1. 多態(tài)的本質(zhì):用統(tǒng)一接口,處理多種類型/對象,讓代碼更通用、更易擴(kuò)展。
  2. 靜態(tài)多態(tài)
    • 發(fā)生在編譯期;
    • 典型形式有函數(shù)重載、運(yùn)算符重載、模板;
    • 性能好,但靈活性在“運(yùn)行時決定類型”方面不足。
  3. 動態(tài)多態(tài)
    • 發(fā)生在運(yùn)行期;
    • 依靠繼承、虛函數(shù)和基類指針/引用;
    • 借助虛函數(shù)表實(shí)現(xiàn),根據(jù)對象實(shí)際類型決定行為;
    • 注意虛析構(gòu)函數(shù)、構(gòu)造/析構(gòu)中調(diào)用虛函數(shù)、對象切片等細(xì)節(jié)。
  4. 基于虛表的實(shí)現(xiàn)細(xì)節(jié)
    • 每個有虛函數(shù)的類有一張?zhí)摫恚?/li>
    • 每個對象有一個 vptr 指向虛表;
    • 派生類重寫虛函數(shù)時,相應(yīng)虛表項(xiàng)會替換為派生類實(shí)現(xiàn),沒重寫的仍指向基類版本——這也回答了你之前關(guān)于“只重寫其中一個虛函數(shù)時虛表長什么樣”的問題。

到此這篇關(guān)于C++多態(tài)詳解之從靜態(tài)多態(tài)到動態(tài)多態(tài)的文章就介紹到這了,更多相關(guān)C++從靜態(tài)多態(tài)到動態(tài)多態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++實(shí)現(xiàn)字符格式相互轉(zhuǎn)換的示例代碼

    C++實(shí)現(xiàn)字符格式相互轉(zhuǎn)換的示例代碼

    這篇文章主要為大家詳細(xì)介紹了C++中實(shí)現(xiàn)字符格式相互轉(zhuǎn)換的方法,主要有UTF8與string互轉(zhuǎn)、wstring與string互轉(zhuǎn),感興趣的小伙伴可以了解一下
    2022-11-11
  • C++異常機(jī)制:棧展開、異常傳播與異常安全

    C++異常機(jī)制:棧展開、異常傳播與異常安全

    本文主要介紹了C語言和C++處理錯誤的方式,C語言提供了終止程序、返回錯誤碼、errno等方式,而C++引入了異常處理機(jī)制,通過throw拋出異常,通過try-catch捕獲和處理異常,異常處理可以提高程序的健壯性
    2026-05-05
  • C語言實(shí)現(xiàn)字符串字符反向排列的方法詳解

    C語言實(shí)現(xiàn)字符串字符反向排列的方法詳解

    這篇文章主要為大家分享了幾種通過C語言實(shí)現(xiàn)字符串字符反向排列(不是逆序打?。┑姆椒?,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2022-05-05
  • c++如何實(shí)現(xiàn)歸并兩個有序鏈表

    c++如何實(shí)現(xiàn)歸并兩個有序鏈表

    這篇文章主要介紹了c++如何實(shí)現(xiàn)歸并兩個有序鏈表,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • C++實(shí)踐數(shù)組作數(shù)據(jù)成員的參考

    C++實(shí)踐數(shù)組作數(shù)據(jù)成員的參考

    今天小編就為大家分享一篇關(guān)于C++實(shí)踐數(shù)組作數(shù)據(jù)成員的參考,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-02-02
  • ?C++模板template原理解析

    ?C++模板template原理解析

    這篇文章主要介紹了C++模板template原理,函數(shù)模板代表了一個函數(shù)家族,該函數(shù)模板與類型無關(guān),在使用時被參數(shù)化,根據(jù)實(shí)參類型產(chǎn)生函數(shù)的特定類型版本
    2022-07-07
  • 最小生成樹算法C語言代碼實(shí)例

    最小生成樹算法C語言代碼實(shí)例

    這篇文章主要介紹了最小生成樹算法C語言代碼實(shí)例,有需要的朋友可以參考一下
    2013-12-12
  • VScode中C++頭文件問題的終極解決方法詳析

    VScode中C++頭文件問題的終極解決方法詳析

    最近使用VSCode編譯C/C++時發(fā)現(xiàn)了問題,下面這篇文章主要給大家介紹了關(guān)于VScode中C++頭文件問題的終極解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • 一篇文章帶你了解C語言操作符

    一篇文章帶你了解C語言操作符

    這篇文章主要以圖文結(jié)合的方式為大家詳細(xì)介紹了C語言操作符基礎(chǔ)知識,感興趣的小伙伴們可以參考一下,希望能給你帶來幫助
    2021-09-09
  • C語言重難點(diǎn)之內(nèi)存對齊和位段

    C語言重難點(diǎn)之內(nèi)存對齊和位段

    這篇文章主要介紹了C語言重難點(diǎn)之內(nèi)存對齊和位段,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05

最新評論

哈巴河县| 平塘县| 东兰县| 绥滨县| 深圳市| 马尔康县| 台南市| 新巴尔虎右旗| 偏关县| 平罗县| 潜山县| 屏边| 峨眉山市| 柯坪县| 丹棱县| 山西省| 平武县| 新巴尔虎右旗| 三明市| 台南市| 明光市| 项城市| 西藏| 宜兰市| 威宁| 兴和县| 曲沃县| 上杭县| 泌阳县| 开化县| 宣恩县| 改则县| 临颍县| 巴彦县| 秦安县| 四子王旗| 舟山市| 屏东市| 剑阁县| 阿拉善左旗| 弥渡县|