精品一区二区三区在线成人,欧美精产国品一二三区,Ji大巴进入女人66h,亚洲春色在线视频

科大訊飛C++二面:解釋C++中多態(tài)的實現原理,以及虛函數表的作用?

開發(fā) 前端
在日常生活中,我們常常會遇到這樣一種現象:同樣的行為,在不同的對象上卻有著不同的表現。就好比 “開車” 這個行為,當是賽車手駕駛賽車時,那速度與激情令人熱血沸騰;而當是新手司機駕駛家用車時,可能就多了幾分謹慎與小心。

當你在代碼里寫下Base* ptr = new Derived(); ptr->func();時,有沒有突然停下想過:編譯器是怎么知道該去執(zhí)行Derived類的func(),而不是Base類的?明明指針類型是Base,卻能精準找到派生類的實現 —— 這就是 C++ 多態(tài)最迷人的 “魔術”。這個魔術背后,既不是編譯器的 “智能猜測”,也不是簡單的條件判斷。

它依賴一套精心設計的底層機制,讓程序在運行時能 “看清” 對象的真實類型,找到該調用的函數。而支撐這一切的核心,就是我們常說的虛函數表和動態(tài)綁定。接下來,我們就一層層揭開這層面紗:從編譯器如何為帶虛函數的類 “偷偷” 準備數據結構,到對象里隱藏的指針如何導航到正確的函數實現,看看多態(tài)是如何讓 C++ 代碼既保持抽象性,又能靈活應對不同場景的。

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

1.1 C++多態(tài)概述

在日常生活中,我們常常會遇到這樣一種現象:同樣的行為,在不同的對象上卻有著不同的表現。就好比 “開車” 這個行為,當是賽車手駕駛賽車時,那速度與激情令人熱血沸騰;而當是新手司機駕駛家用車時,可能就多了幾分謹慎與小心。在 C++ 編程的世界里,也有一個與之類似的概念,那就是多態(tài)。

從定義上來說,多態(tài)是指同一個行為具有多個不同表現形式或形態(tài)的能力 。在 C++ 中,多態(tài)主要是通過虛函數來實現的。簡單來說,當一個基類的指針或引用指向不同的派生類對象時,調用同一個虛函數,會呈現出不同的行為,這便是多態(tài)的魅力所在。比如動物類有一個 “叫” 的函數,狗類和貓類繼承自動物類,并重寫了 “叫” 的函數,當用動物類的指針分別指向狗類和貓類的對象時,調用 “叫” 函數,就會分別聽到狗叫和貓叫。

在 C++ 中,多態(tài)又可以細分為靜態(tài)多態(tài)和動態(tài)多態(tài)。靜態(tài)多態(tài)主要是通過函數重載和模板來實現,它是在編譯期就確定了調用的函數版本;而動態(tài)多態(tài)則是基于虛函數,在運行時才根據對象的實際類型來決定調用哪個函數,這也是我們后續(xù)重點探討的內容。

一般來說,多態(tài)分為兩種,靜態(tài)多態(tài)和動態(tài)多態(tài)。靜態(tài)多態(tài)也稱編譯時多態(tài),主要包括模板和重載。而動態(tài)多態(tài)則是通過類的繼承和虛函數來實現,當基類和子類擁有同名同參同返回的方法,且該方法聲明為虛方法,當基類對象,指針,引用指向的是派生類的對象的時候,基類對象,指針,引用在調用基類的虛函數,實際上調用的是派生類函數。這就是動態(tài)多態(tài)。

(1)靜態(tài)多態(tài)的實現

靜態(tài)多態(tài)靠編譯器來實現,簡單來說就是編譯器對原來的函數名進行修飾,在c語言中,函數無法重載,是因為,c編譯器在修飾函數時,只是簡單的在函數名前加上下劃線"_" ,不過從gcc編譯器編譯之后發(fā)現函數名并不會發(fā)生變化。而c++編譯器不同,它根據函數參數的類型,個數來對函數名進行修飾,這就使得函數可以重載,同理,模板也是可以實現的,針對不同類型的實參來產生對應的特化的函數,通過增加修飾,使得不同的類型參數的函數得以區(qū)分。以下段程序為例:

#include <iostream>
using namespace std;

template <typename T1, typename T2>
int fun(T1 t1, T2 t2){}

int foofun(){}
int foofun(int){}
int foofun(int , float){}
int foofun(int , float ,double){}

int main(int argc, char *argv[])
{
    fun(1, 2);
    fun(1, 1.1);
    foofun();
    foofun(1);
    foofun(1, 1.1);
    foofun(1, 1.1, 1.11);
    return 0;
}

(2)動態(tài)多態(tài)的實現

聲明一個類時,如果類中有虛方法,則自動在類中增加一個虛函數指針,該指針指向的是一個虛函數表,虛函數表中存著每個虛函數真正對應的函數地址。動態(tài)多態(tài)采用一種延遲綁定技術,普通的函數調用,在編譯期間就已經確定了調用的函數的地址,所以無論怎樣調用,總是那個函數,但是擁有虛函數的類,在調用虛函數時,首先去查虛函數表,然后在確定調用的是哪一個函數,所以,調用的函數是在運行時才會確定的。

在聲明基類對象時,如果基類擁有虛函數,就會自動生成一個虛函數指針,這個虛函數指針指向基類對應的虛函數表。在聲明派生類對象時,虛函數指針指向的是派生類對應的虛函數表。在對象被創(chuàng)建之后(以指針為例),無論是基類指針還是派生類指針指向這個對象,虛函數表是不會改變的,虛表指針的指向也是不會變的。

以下段程序為例:

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void fun()
    {
        cout << "this is base fun" << endl;
    }
};

class Derived : public Base
{
public:
    void fun()
    {
        cout << "this is Derived fun" << endl;
    }
};
int main(int argc, char *argv[])
{
    Base b1;
    Derived d1;
    Base *pb = &d1;
    Derived *pd = (Derived *)&b1;
    b1.fun();
    pd->fun();
    d1.fun();
    pb->fun();
    return 0;
}

1.2多態(tài)如何解決代碼復用難題

在軟件開發(fā)中,代碼復用是提高開發(fā)效率、降低維護成本的關鍵。然而,在沒有多態(tài)的情況下,實現代碼復用往往面臨諸多挑戰(zhàn)。比如,我們要開發(fā)一個圖形繪制系統,其中包含圓形、矩形和三角形等多種圖形。如果不使用多態(tài),那么為了繪制這些不同的圖形,我們可能需要編寫大量重復的代碼。

class Circle {
public:
    void drawCircle() {
        // 繪制圓形的具體代碼
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle {
public:
    void drawRectangle() {
        // 繪制矩形的具體代碼
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle {
public:
    void drawTriangle() {
        // 繪制三角形的具體代碼
        std::cout << "Drawing a triangle" << std::endl;
    }
};

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;

    circle.drawCircle();
    rectangle.drawRectangle();
    triangle.drawTriangle();

    return 0;
}

在這段代碼中,每個圖形類都有自己獨立的繪制函數,當我們需要繪制不同的圖形時,需要分別調用不同的函數。如果后續(xù)要添加新的圖形,比如梯形,就需要再次編寫新的繪制函數,并且在使用時也需要額外添加調用邏輯,代碼的擴展性和復用性都很差 。

而當我們引入多態(tài)后,情況就大不相同了。我們可以定義一個基類,比如Shape,在其中聲明一個虛函數draw,然后讓各個圖形類繼承自Shape類,并重寫draw函數。

class Shape {
public:
    virtual void draw() = 0; // 純虛函數,使Shape成為抽象類
};

class Circle : public Shape {
public:
    void draw() override {
        // 繪制圓形的具體代碼
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        // 繪制矩形的具體代碼
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle : public Shape {
public:
    void draw() override {
        // 繪制三角形的具體代碼
        std::cout << "Drawing a triangle" << std::endl;
    }
};

void drawShapes(Shape* shapes[], int count) {
    for (int i = 0; i < count; ++i) {
        shapes[i]->draw();
    }
}

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;

    Shape* shapes[] = {&circle, &rectangle, &triangle};
    int count = sizeof(shapes) / sizeof(shapes[0]);

    drawShapes(shapes, count);

    return 0;
}

在這個改進后的代碼中,drawShapes函數可以接受一個Shape類型的指針數組,無論數組中的元素是指向Circle、Rectangle還是Triangle對象,都可以通過調用draw函數來實現正確的繪制。這樣,當我們需要添加新的圖形時,只需要創(chuàng)建一個新的派生類并重寫draw函數,而drawShapes函數的代碼無需修改,大大提高了代碼的復用性和可擴展性。

1.3多態(tài)讓代碼擴展更輕松

在軟件開發(fā)的過程中,我們常常面臨需求不斷變化和功能持續(xù)擴展的挑戰(zhàn)。一個好的程序設計應該能夠輕松應對這些變化,而多態(tài)在其中扮演著至關重要的角色,它讓代碼的擴展變得更加輕松。

以游戲開發(fā)為例,假設我們正在開發(fā)一款角色扮演游戲,游戲中有不同類型的角色,如戰(zhàn)士、法師和刺客 。每個角色都有自己獨特的攻擊方式和移動方式。

如果不使用多態(tài),我們可能會為每個角色編寫獨立的類,每個類中包含各自的攻擊和移動方法。當需要添加新的角色類型,比如牧師時,我們就需要在多個地方修改代碼。不僅要創(chuàng)建新的牧師類并編寫其獨特的技能方法,還可能需要在處理角色行為的邏輯中添加大量的條件判斷語句來處理牧師的行為。例如:

class Warrior {
public:
    void attackWarrior() {
        std::cout << "Warrior attacks with a sword" << std::endl;
    }
    void moveWarrior() {
        std::cout << "Warrior moves quickly" << std::endl;
    }
};

class Mage {
public:
    void attackMage() {
        std::cout << "Mage casts a spell" << std::endl;
    }
    void moveMage() {
        std::cout << "Mage moves slowly" << std::endl;
    }
};

class Assassin {
public:
    void attackAssassin() {
        std::cout << "Assassin attacks with a dagger" << std::endl;
    }
    void moveAssassin() {
        std::cout << "Assassin moves stealthily" << std::endl;
    }
};

void handleCharacterAction() {
    // 假設這里有一個變量表示角色類型
    int characterType = 1; // 1代表戰(zhàn)士,2代表法師,3代表刺客
    Warrior warrior;
    Mage mage;
    Assassin assassin;

    if (characterType == 1) {
        warrior.attackWarrior();
        warrior.moveWarrior();
    }
    else if (characterType == 2) {
        mage.attackMage();
        mage.moveMage();
    }
    else if (characterType == 3) {
        assassin.attackAssassin();
        assassin.moveAssassin();
    }
}

可以看到,這種方式的代碼不僅冗長,而且維護起來非常困難。每添加一種新的角色類型,都需要在handleCharacterAction函數中添加大量的if - else判斷,這使得代碼的可讀性和可維護性都很差。

而利用多態(tài)的特性,我們可以定義一個基類Character,在其中聲明虛函數attack和move,然后讓戰(zhàn)士、法師和刺客等角色類繼承自Character類,并根據自身特點重寫這些虛函數。這樣,當我們需要添加新的角色類型時,只需要創(chuàng)建一個新的派生類,重寫相應的虛函數,而不需要修改現有的核心代碼。例如:

class Character {
public:
    virtual void attack() = 0;
    virtual void move() = 0;
};

class Warrior : public Character {
public:
    void attack() override {
        std::cout << "Warrior attacks with a sword" << std::endl;
    }
    void move() override {
        std::cout << "Warrior moves quickly" << std::endl;
    }
};

class Mage : public Character {
public:
    void attack() override {
        std::cout << "Mage casts a spell" << std::endl;
    }
    void move() override {
        std::cout << "Mage moves slowly" << std::endl;
    }
};

class Assassin : public Character {
public:
    void attack() override {
        std::cout << "Assassin attacks with a dagger" << std::endl;
    }
    void move() override {
        std::cout << "Assassin moves stealthily" << std::endl;
    }
};

void handleCharacterAction(Character* character) {
    character->attack();
    character->move();
}

int main() {
    Warrior warrior;
    Mage mage;
    Assassin assassin;

    handleCharacterAction(&warrior);
    handleCharacterAction(&mage);
    handleCharacterAction(&assassin);

    return 0;
}

在這個改進后的代碼中,handleCharacterAction函數只需要接受一個Character類型的指針,無論傳入的是戰(zhàn)士、法師還是刺客的對象指針,都能正確地調用相應的攻擊和移動方法。當我們要添加牧師角色時,只需要創(chuàng)建一個Priest類繼承自Character類,并重寫attack和move方法,然后就可以直接在main函數中使用handleCharacterAction函數來處理牧師角色的行為,而無需修改handleCharacterAction函數的代碼。

1.4多態(tài)的實現原理

C++實現多態(tài)的主要方式有:

(1)重載(Overloading):通過函數名相同但參數不同的多個函數實現不同行為。在編譯時通過參數類型決定調用哪個函數。

void add(int a, int b) { ... } 
void add(double a, double b) { ... }

(2)重寫(Overriding):通過繼承讓派生類重新實現基類的虛函數。在運行時通過指針/引用的實際類型調用對應的函數。

class Base {
public:
    virtual void func() { ... }
};

class Derived extends Base {
public:
    virtual void func() { ... } 
}; 

Base* b = new Derived();
b->func(); // Calls Derived::func()

(3)編譯時多態(tài):通過模板和泛型實現針對不同類型具有不同實現的函數。在編譯時通過傳入類型決定具體實現。

template <typename T>
void func(T t) { ... }

func(1);   // Calls func<int> 
func(3.2); // Calls func<double>

(4)條件編譯:通過#ifdef/#elif等預處理命令針對不同條件編譯不同的代碼實現產生不同行為的程序。編譯時通過定義的宏決定具體實現

#ifdef _WIN32 
    void func() { ... }   // Windows version
#elif __linux__
    void func() { ... }   // Linux version   
#endif

綜上,C++通過重載、重寫、模板、條件編譯等手段實現多態(tài)。其中,重寫基于繼承和虛函數實現真正的運行時多態(tài),增強了程序的靈活性和可擴展性。

一個接口,多種方法:

  • 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
  • 存在虛函數的類都有一個一維的虛函數表叫做虛表。當類中聲明虛函數時,編譯器會在類中生成一個虛函數表。
  • 類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
  • 虛函數表是一個存儲類成員函數指針的數據結構。
  • 虛函數表是由編譯器自動生成與維護的。
  • virtual成員函數會被編譯器放入虛函數表中。
  • 當存在虛函數時,每個對象中都有一個指向虛函數的指針(C++編譯器給父類對象,子類對象提前布局vptr指針),當進行test(parent *base)函數的時候,C++編譯器不需要區(qū)分子類或者父類對象,只需要在base指針中,找到vptr指針即可)。
  • vptr一般作為類對象的第一個成員。

二、什么是虛函數表?

2.1虛函數表是什么

虛函數表,英文名為 Virtual Function Table,通常簡稱為 vtable ,它是一個編譯器在編譯階段為包含虛函數的類生成的存儲虛函數地址的數組,是 C++ 實現多態(tài)的關鍵機制。可以將虛函數表形象地看作是一個 “函數地址目錄”,在這個特殊的 “目錄” 里,每一項都記錄著對應虛函數在內存中的入口地址。當程序運行過程中需要調用某個虛函數時,就可以借助這個 “目錄” 快速定位到函數的具體位置,從而順利執(zhí)行函數代碼 。

例如,有一個游戲開發(fā)場景,定義一個基類Character(角色),其中包含一個虛函數Attack(攻擊):

class Character {
public:
    virtual void Attack() {
        std::cout << "Character attacks in a general way." << std::endl;
    }
};

然后,派生出子類Warrior(戰(zhàn)士)和Mage(法師),它們分別重寫Attack函數,實現各自獨特的攻擊方式:

class Warrior : public Character {
public:
    void Attack() override {
        std::cout << "Warrior attacks with a sword!" << std::endl;
    }
};

class Mage : public Character {
public:
    void Attack() override {
        std::cout << "Mage casts a fireball!" << std::endl;
    }
};

在這個例子中,編譯器會為Character類、Warrior類和Mage類分別生成各自的虛函數表。Character類的虛函數表中,Attack函數的地址指向基類中Attack函數的實現代碼;Warrior類的虛函數表,由于重寫了Attack函數,所以表中Attack函數的地址指向Warrior類中重寫后的Attack函數實現代碼,Mage類同理。這樣,在程序運行時,就能根據對象的實際類型,通過虛函數表準確地找到并調用相應的攻擊函數。

2.2為什么需要虛函數表

在 C++ 中,虛函數表對于實現運行時多態(tài)起著至關重要的作用。當使用基類指針或引用指向不同的派生類對象時,程序需要在運行時根據對象的實際類型來確定調用哪個版本的虛函數,而虛函數表就是實現這一動態(tài)綁定過程的核心。

假設沒有虛函數表,當用基類指針指向子類對象并調用一個被重寫的函數時,編譯器只能根據指針的靜態(tài)類型(即基類類型)來確定調用基類中的函數版本,無法實現根據對象實際類型來調用對應函數的多態(tài)效果 。例如在前面的游戲角色例子中,如果沒有虛函數表,當Character* ptr = new Warrior(); 后,調用ptr->Attack() ,就會一直調用Character類的Attack函數,而不是Warrior類中重寫后的更符合實際需求的攻擊函數,這顯然無法滿足游戲中不同角色具有不同攻擊方式的多樣化需求。

而有了虛函數表,當通過基類指針或引用調用虛函數時,程序首先會根據對象內存中存儲的虛指針(vptr,每個包含虛函數的類的對象都會有一個指向其對應類虛函數表的虛指針,且通常位于對象內存布局的前端 )找到對應的虛函數表,然后在虛函數表中根據函數的索引找到實際要調用的虛函數地址,最終調用該函數。

這樣,無論基類指針指向哪個派生類對象,都能準確地調用到派生類中重寫后的虛函數版本,實現了運行時多態(tài) 。虛函數表就像是一個智能的 “導航儀”,在復雜的繼承體系中,為程序指引著正確調用函數的方向,讓 C++ 的多態(tài)性得以完美呈現,大大提高了代碼的靈活性、可擴展性和可維護性 。

2.3在內存中的布局

(1)對象內存布局中的虛函數表指針

在 C++ 中,當一個類包含虛函數時,該類的對象內存布局會有一個特殊的成員 —— 虛函數表指針(vptr) 。這個指針就像一把指向虛函數表的 “鑰匙”,是實現多態(tài)的關鍵紐帶。

在絕大多數編譯器實現中,虛函數表指針通常位于對象內存的起始處 。以 32 位編譯器為例,指針占用 4 個字節(jié)的內存空間;在 64 位編譯器下,指針則占用 8 個字節(jié) 。假設我們有如下簡單的類定義:

class Animal {
public:
    virtual void Speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
    int m_age;
};

當創(chuàng)建一個Animal類的對象時,如Animal dog; ,在內存中,dog對象的前 4 個字節(jié)(32 位編譯器)或前 8 個字節(jié)(64 位編譯器)就是虛函數表指針 。我們可以通過以下代碼來驗證這一點:

#include <iostream>

class Animal {
public:
    virtual void Speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
    int m_age;
};

int main() {
    Animal dog;
    dog.m_age = 5;

    // 獲取對象的地址并轉換為整數指針,用于讀取內存中的數據
    int* ptr = reinterpret_cast<int*>(&dog);
    // 讀取對象內存起始處的4個字節(jié),即為虛函數表指針的值
    int vptr_value = *ptr; 

    std::cout << "The value of vptr in the dog object: " << std::hex << vptr_value << std::endl;

    return 0;
}

在這段代碼中,reinterpret_cast<int*>(&dog)將dog對象的地址轉換為整數指針,這樣就可以通過指針操作讀取對象內存中的數據 。*ptr讀取的就是對象內存起始處的 4 個字節(jié),也就是虛函數表指針的值 。通過輸出這個值,我們能直觀地看到虛函數表指針在對象內存中的位置和它所指向的虛函數表地址。

(2)虛函數表自身在內存中的位置

虛函數表在內存中的位置也是一個關鍵知識點 。通常情況下,虛函數表位于只讀數據段(.rodata),也就是 C++ 內存模型中的常量區(qū) 。這是因為虛函數表中的內容在程序運行期間是不會改變的,將其放置在只讀數據段可以保證數據的安全性和穩(wěn)定性,防止程序意外修改虛函數表內容導致運行時錯誤 。

為了驗證這一結論,我們來看下面的代碼示例:

#include <iostream>

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

int main() {
    Base obj;
    // 獲取對象的虛函數表指針
    int* vptr = reinterpret_cast<int*>(&obj);
    // 通過虛函數表指針獲取虛函數表的地址
    int vtable_address = *vptr;

    std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;

    return 0;
}

編譯并運行這段代碼后,我們得到虛函數表的地址 。接下來,使用工具(如 Linux 下的objdump -s命令來解析 ELF 格式的可執(zhí)行文件中的分段信息)來查看該地址屬于哪個內存段 。假設運行程序后得到虛函數表地址為0x400b40 ,在終端中執(zhí)行objdump -s your_executable_file (your_executable_file為生成的可執(zhí)行文件名),然后在輸出結果中查找0x400b40所在的內存段 。通常會發(fā)現,該地址位于.rodata段中,這就驗證了虛函數表位于只讀數據段的結論 。

2.4虛函數表的動態(tài)變化

(1)單繼承無覆蓋

在單繼承且子類沒有覆蓋父類虛函數的情況下,子類的虛函數表結構相對較為直觀 。我們來看下面的代碼示例:

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

class Derived : public Base {
public:
    virtual void Func3() {
        std::cout << "Derived::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Derived::Func4" << std::endl;
    }
};

在這個例子中,Base類包含兩個虛函數Func1和Func2,Derived類繼承自Base類,并且新增了兩個虛函數Func3和Func4 。此時,Derived類的虛函數表中,首先會按照聲明順序依次排列父類Base的虛函數Func1和Func2的地址,然后再接著排列子類Derived新增的虛函數Func3和Func4的地址 。

我們可以通過一些技巧來驗證這一結構 。在 32 位系統下,假設Derived類對象的內存起始地址為0x1000 ,由于虛函數表指針(vptr)通常位于對象內存起始處,占用 4 個字節(jié),所以通過*(int*)0x1000可以獲取到虛函數表的地址,假設為0x2000 。

虛函數表是一個存儲虛函數指針的數組,每個指針占用 4 個字節(jié) 。那么*(int*)0x2000就是Func1的函數地址,*(int*)(0x2000 + 4)就是Func2的函數地址,*(int*)(0x2000 + 8)是Func3的函數地址,*(int*)(0x2000 + 12)是Func4的函數地址 。通過這種方式,我們可以清晰地看到在單繼承無覆蓋情況下,子類虛函數表中父類虛函數和子類新增虛函數的排列順序 。

(2)單繼承有覆蓋

當子類覆蓋父類虛函數時,虛函數表會發(fā)生重要的變化 。還是以上面的代碼為基礎,假設Derived類覆蓋了Base類的Func1函數:

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

class Derived : public Base {
public:
    virtual void Func1() {
        std::cout << "Derived::Func1" << std::endl;
    }
    virtual void Func3() {
        std::cout << "Derived::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Derived::Func4" << std::endl;
    }
};

在這種情況下,Derived類的虛函數表中,原本指向Base::Func1的函數地址會被替換為Derived::Func1的函數地址 。而Func2的地址保持不變,因為它沒有被覆蓋 。新增的虛函數Func3和Func4依然按照順序排在后面 。

同樣以32位系統下的內存地址為例,假設Derived類對象內存起始地址為0x1000 ,虛函數表地址為0x2000 。此時*(int*)0x2000指向的就是Derived::Func1的函數地址,*(int*)(0x2000 + 4)仍然是Base::Func2的函數地址,*(int*)(0x2000+8)是Derived::Func3的函數地址,*(int*)(0x2000 + 12)是Derived::Func4的函數地址 。這種覆蓋機制確保了在通過基類指針或引用調用虛函數時,能夠準確地調用到子類中重寫后的函數版本,實現了多態(tài)性 。

(3)多繼承情況

多繼承時,虛函數表的結構變得更加復雜 。假設有如下代碼:

class Base1 {
public:
    virtual void Func1() {
        std::cout << "Base1::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base1::Func2" << std::endl;
    }
};

class Base2 {
public:
    virtual void Func3() {
        std::cout << "Base2::Func3" << std::endl;
    }
    virtual void Func4() {
        std::cout << "Base2::Func4" << std::endl;
    }
};

class Derived : public Base1, public Base2 {
public:
    virtual void Func1() {
        std::cout << "Derived::Func1" << std::endl;
    }
    virtual void Func5() {
        std::cout << "Derived::Func5" << std::endl;
    }
};

在多繼承中,Derived類會擁有兩個虛函數表,分別對應Base1和Base2 。在Derived類對象的內存布局中,首先是對應Base1的虛函數表指針,然后是Base1類的其他成員(如果有),接著是對應Base2的虛函數表指針,再后面是Base2類的其他成員(如果有),最后是Derived類自己的成員 。

對于對應Base1的虛函數表,其中Func1的地址會被Derived::Func1的地址覆蓋(因為Derived類重寫了Func1 ),Func2的地址保持為Base1::Func2的地址 。而新增的虛函數Func5會被添加到這個虛函數表的末尾 。對應Base2的虛函數表中,Func3和Func4的地址分別是Base2::Func3和Base2::Func4的地址,因為Derived類沒有重寫這兩個函數 。

假設在 64 位系統下,Derived類對象內存起始地址為0x1000:

第一個虛函數表指針(對應Base1 )位于0x1000 ,通過*(int*)0x1000獲取其虛函數表地址,假設為0x2000 。在這個虛函數表中,*(int*)0x2000是Derived::Func1的函數地址,*(int*)(0x2000 + 8)是Base1::Func2的函數地址,*(int*)(0x2000 + 16)是Derived::Func5的函數地址 。

第二個虛函數表指針(對應Base2 )位于0x1008 (64 位系統指針占 8 字節(jié)),通過*(int*)0x1008獲取其虛函數表地址,假設為0x3000 ,在這個虛函數表中,*(int*)0x3000是Base2::Func3的函數地址,*(int*)(0x3000 + 8)是Base2::Func4的函數地址 。這種復雜的結構使得多繼承在帶來強大功能的同時,也增加了理解和維護的難度 。

三、實戰(zhàn)案例分析

3.1多態(tài)在設計模式中的應用

多態(tài)作為面向對象編程的核心特性之一,在各種設計模式中發(fā)揮著舉足輕重的作用。它為設計模式提供了更加靈活和強大的解決方案,使得軟件系統的結構更加清晰、可維護性更強。下面我們就來探討一下多態(tài)在策略模式和工廠方法模式中的具體應用 。

(1)策略模式中的多態(tài)應用

策略模式是一種行為型設計模式,它定義了一系列算法,并將每個算法封裝起來,使它們可以相互替換。策略模式的核心在于將算法的選擇和使用與算法的具體實現分離開來,而多態(tài)正是實現這一分離的關鍵。

以一個簡單的計算器程序為例,我們可以使用策略模式和多態(tài)來實現不同的運算邏輯。首先,定義一個抽象的運算策略接口,其中包含一個用于執(zhí)行運算的方法:

class OperationStrategy {
public:
    virtual double execute(double num1, double num2) = 0;
};

然后,分別創(chuàng)建具體的運算策略類,如加法策略類、減法策略類、乘法策略類和除法策略類,它們都繼承自OperationStrategy接口,并實現execute方法:

class AddStrategy : public OperationStrategy {
public:
    double execute(double num1, double num2) override {
        return num1 + num2;
    }
};

class SubtractStrategy : public OperationStrategy {
public:
    double execute(double num1, double num2) override {
        return num1 - num2;
    }
};

class MultiplyStrategy : public OperationStrategy {
public:
    double execute(double num1, double num2) override {
        return num1 * num2;
    }
};

class DivideStrategy : public OperationStrategy {
public:
    double execute(double num1, double num2) override {
        if (num2 != 0) {
            return num1 / num2;
        }
        // 這里可以拋出異常或者返回一個特殊值表示錯誤
        return 0; 
    }
};

接下來,定義一個計算器類,它持有一個OperationStrategy指針,并通過該指針調用具體的運算策略:

class Calculator {
private:
    OperationStrategy* strategy;
public:
    Calculator(OperationStrategy* s) : strategy(s) {}
    double calculate(double num1, double num2) {
        return strategy->execute(num1, num2);
    }
};

在客戶端代碼中,我們可以根據需要選擇不同的運算策略,并將其傳遞給計算器對象,從而實現不同的運算:

int main() {
    OperationStrategy* addStrategy = new AddStrategy();
    Calculator addCalculator(addStrategy);
    double result1 = addCalculator.calculate(5, 3);
    std::cout << "5 + 3 = " << result1 << std::endl;

    OperationStrategy* subtractStrategy = new SubtractStrategy();
    Calculator subtractCalculator(subtractStrategy);
    double result2 = subtractCalculator.calculate(5, 3);
    std::cout << "5 - 3 = " << result2 << std::endl;

    OperationStrategy* multiplyStrategy = new MultiplyStrategy();
    Calculator multiplyCalculator(multiplyStrategy);
    double result3 = multiplyCalculator.calculate(5, 3);
    std::cout << "5 * 3 = " << result3 << std::endl;

    OperationStrategy* divideStrategy = new DivideStrategy();
    Calculator divideCalculator(divideStrategy);
    double result4 = divideCalculator.calculate(5, 3);
    std::cout << "5 / 3 = " << result4 << std::endl;

    // 釋放內存
    delete addStrategy;
    delete subtractStrategy;
    delete multiplyStrategy;
    delete divideStrategy;

    return 0;
}

在這個例子中,多態(tài)使得我們可以在運行時動態(tài)地選擇不同的運算策略,而無需修改計算器類的代碼。如果后續(xù)需要添加新的運算,比如求冪運算,只需要創(chuàng)建一個新的策略類并實現execute方法,然后在客戶端代碼中使用新的策略類即可,極大地提高了系統的靈活性和可擴展性 。

(2)工廠方法模式中的多態(tài)應用

工廠方法模式是一種創(chuàng)建型設計模式,它定義了一個用于創(chuàng)建對象的接口,但由子類決定實例化哪個類。工廠方法模式將對象的創(chuàng)建和使用分離,使得代碼更加靈活和可維護,而多態(tài)在其中起到了至關重要的作用。

假設我們正在開發(fā)一個游戲,游戲中有不同類型的角色,如戰(zhàn)士、法師和刺客。我們可以使用工廠方法模式和多態(tài)來創(chuàng)建這些角色。首先,定義一個抽象的角色類,作為所有具體角色類的基類:

class Character {
public:
    virtual void display() = 0;
};

然后,分別創(chuàng)建具體的角色類,如戰(zhàn)士類、法師類和刺客類,它們都繼承自Character類,并實現display方法:

class Warrior : public Character {
public:
    void display() override {
        std::cout << "This is a warrior" << std::endl;
    }
};

class Mage : public Character {
public:
    void display() override {
        std::cout << "This is a mage" << std::endl;
    }
};

class Assassin : public Character {
public:
    void display() override {
        std::cout << "This is an assassin" << std::endl;
    }
};

接下來,定義一個抽象的角色工廠類,其中包含一個純虛的工廠方法,用于創(chuàng)建角色對象:

class CharacterFactory {
public:
    virtual Character* createCharacter() = 0;
};

然后,創(chuàng)建具體的角色工廠類,如戰(zhàn)士工廠類、法師工廠類和刺客工廠類,它們都繼承自CharacterFactory類,并實現createCharacter方法:

class WarriorFactory : public CharacterFactory {
public:
    Character* createCharacter() override {
        return new Warrior();
    }
};

class MageFactory : public CharacterFactory {
public:
    Character* createCharacter() override {
        return new Mage();
    }
};

class AssassinFactory : public CharacterFactory {
public:
    Character* createCharacter() override {
        return new Assassin();
    }
};

在客戶端代碼中,我們可以通過具體的角色工廠類來創(chuàng)建不同類型的角色對象:

int main() {
    CharacterFactory* warriorFactory = new WarriorFactory();
    Character* warrior = warriorFactory->createCharacter();
    warrior->display();

    CharacterFactory* mageFactory = new MageFactory();
    Character* mage = mageFactory->createCharacter();
    mage->display();

    CharacterFactory* assassinFactory = new AssassinFactory();
    Character* assassin = assassinFactory->createCharacter();
    assassin->display();

    // 釋放內存
    delete warrior;
    delete mage;
    delete assassin;
    delete warriorFactory;
    delete mageFactory;
    delete assassinFactory;

    return 0;
}

在這個例子中,多態(tài)使得我們可以通過抽象的CharacterFactory類來創(chuàng)建不同類型的角色對象,而無需在客戶端代碼中直接實例化具體的角色類。當需要添加新的角色類型時,只需要創(chuàng)建一個新的具體角色類和對應的角色工廠類,而客戶端代碼幾乎不需要修改,提高了代碼的可維護性和可擴展性。

3.2虛函數表在編程中的實踐

(1)通過代碼訪問虛函數表

在 C++ 中,雖然直接訪問虛函數表并不是常見的操作,但通過了解如何訪問虛函數表,可以更深入地理解多態(tài)的實現機制 。下面是一個簡單的代碼示例,展示如何通過指針操作獲取虛函數表地址和虛函數地址,并調用虛函數:

#include <iostream>

class Base {
public:
    virtual void Func1() {
        std::cout << "Base::Func1" << std::endl;
    }
    virtual void Func2() {
        std::cout << "Base::Func2" << std::endl;
    }
};

typedef void(*FunPtr)();// 定義函數指針類型,用于指向虛函數

int main() {
    Base obj;
    // 獲取對象的虛函數表指針,由于虛函數表指針通常位于對象內存起始處,先將對象地址轉換為整數指針,再解引用獲取虛函數表指針
    int* vptr = reinterpret_cast<int*>(&obj);
    // 通過虛函數表指針獲取虛函數表的地址
    int vtable_address = *vptr;

    std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;

    // 獲取第一個虛函數(Func1)的地址,虛函數表是一個存儲虛函數指針的數組,每個指針占用4個字節(jié)(32位系統),所以將虛函數表地址轉換為整數指針后,解引用獲取第一個虛函數地址
    FunPtr func1_ptr = reinterpret_cast<FunPtr>(*(int*)vtable_address);
    // 調用第一個虛函數
    func1_ptr();

    // 獲取第二個虛函數(Func2)的地址,將指向第一個虛函數地址的指針偏移4個字節(jié)(32位系統),解引用獲取第二個虛函數地址
    FunPtr func2_ptr = reinterpret_cast<FunPtr>(*((int*)vtable_address + 1));
    // 調用第二個虛函數
    func2_ptr();

    return 0;
}

在這段代碼中,首先通過reinterpret_cast<int*>(&obj)將obj對象的地址轉換為整數指針,然后解引用得到虛函數表指針vptr 。通過*vptr獲取虛函數表的地址vtable_address 。接下來,通過將vtable_address轉換為FunPtr類型的函數指針,分別獲取并調用了虛函數表中的Func1和Func2函數 。

這種方式雖然可以直接操作虛函數表,但在實際開發(fā)中,通常不建議這樣做,因為這依賴于編譯器的實現細節(jié),可能導致代碼的可移植性變差 。不過,通過這種方式可以更直觀地了解虛函數表在內存中的布局和工作原理 。

(2)虛函數表在多態(tài)編程中的應用場景

虛函數表在多態(tài)編程中有著廣泛的應用,它使得 C++ 能夠實現不同類型對象的統一接口調用,大大提高了代碼的可擴展性和靈活性 。下面以一個圖形繪制系統為例,來說明虛函數表在實際項目中的應用 。

假設我們正在開發(fā)一個簡單的圖形繪制系統,需要繪制不同類型的圖形,如圓形、矩形和三角形 。我們可以定義一個抽象基類Shape,其中包含一個虛函數Draw用于繪制圖形 :

#include <iostream>

class Shape {
public:
    virtual void Draw() const = 0;
    virtual ~Shape() = default;
};

然后,分別定義Circle(圓形)、Rectangle(矩形)和Triangle(三角形)類,繼承自Shape類,并實現各自的Draw函數 :

class Circle : public Shape {
private:
    int m_radius;
public:
    Circle(int radius) : m_radius(radius) {}
    void Draw() const override {
        std::cout << "Drawing a circle with radius " << m_radius << std::endl;
    }
};

class Rectangle : public Shape {
private:
    int m_width;
    int m_height;
public:
    Rectangle(int width, int height) : m_width(width), m_height(height) {}
    void Draw() const override {
        std::cout << "Drawing a rectangle with width " << m_width << " and height " << m_height << std::endl;
    }
};

class Triangle : public Shape {
private:
    int m_base;
    int m_height;
public:
    Triangle(int base, int height) : m_base(base), m_height(height) {}
    void Draw() const override {
        std::cout << "Drawing a triangle with base " << m_base << " and height " << m_height << std::endl;
    }
};

在客戶端代碼中,我們可以使用Shape類型指針或引用來操作不同類型的圖形對象,無需關心具體的圖形類型 :

void DrawShapes(const Shape* shapes[], int count) {
    for (int i = 0; i < count; ++i) {
        shapes[i]->Draw();
    }
}

int main() {
    Circle circle(5);
    Rectangle rectangle(10, 5);
    Triangle triangle(8, 6);

    const Shape* shapes[] = { &circle, &rectangle, &triangle };
    int count = sizeof(shapes) / sizeof(shapes[0]);

    DrawShapes(shapes, count);

    return 0;
}

在這個例子中,DrawShapes函數接受一個Shape類型的指針數組和數組的大小,通過遍歷數組并調用每個Shape對象的Draw函數,實現了對不同類型圖形的統一繪制操作 。在運行時,根據每個指針實際指向的對象類型(Circle、Rectangle或Triangle),虛函數表會動態(tài)地確定調用哪個類的Draw函數,從而實現了多態(tài)性 。

如果后續(xù)需要添加新的圖形類型,如Square(正方形),只需要定義一個新的類繼承自Shape類并實現Draw函數,而無需修改DrawShapes函數和其他已有的代碼,大大提高了代碼的可擴展性和靈活性 。這就是虛函數表在多態(tài)編程中的強大之處,它使得代碼能夠以一種優(yōu)雅、靈活的方式處理各種不同類型的對象 。

四、虛析構函數(防止沒有析構到子類導致內存泄漏)

我們知道,有時會讓一個基類指針指向用 new 運算符動態(tài)生成的派生類對象;同時,用 new 運算符動態(tài)生成的對象都是通過 delete 指向它的指針來釋放的。如果一個基類指針指向用 new 運算符動態(tài)生成的派生類對象,而釋放該對象時是通過釋放該基類指針來完成的,就可能導致程序不正確。

例如下面的程序:

#include <iostream>
using namespace std;
class CShape  //基類
{
public:
    ~CShape() { cout << "CShape::destrutor" << endl; }
};
class CRectangle : public CShape  //派生類
{
public:
    int w, h;  //寬度和高度
    ~CRectangle() { cout << "CRectangle::destrutor" << endl; }
};
int main()
{
    CShape* p = new CRectangle;
    delete p;  //只根據 p 的類型是 CShape * 來決定應該調用 CShape 類的析構函數,這樣就無法析構到,子類的析構函數
    return 0;
}

程序的輸出結果如下:

CShape::destrutor

輸出結果說明,delete p;只引發(fā)了 CShape 類的析構函數被調用,沒有引發(fā) CRectangle 類的析構函數被調用。這是因為該語句是靜態(tài)聯編的,編譯器編譯到此時,不可能知道此時 p 到底指向哪個類型的對象,它只根據 p 的類型是 CShape * 來決定應該調用 CShape 類的析構函數。

按理說,delete p;會導致一個 CRectangle 類的對象消亡,應該調用 CRectangle 類的析構函數才符合邏輯,否則有可能引發(fā)程序的問題。

例如,假設程序需要對 CRetangle 類的對象進行計數,如果此處不調用 CRetangle 類的析構函數,就會導致計數不正確。再如,假設 CRectangle 類的對象在存續(xù)期間進行了動態(tài)內存分配,而釋放內存的操作都是在析構函數中進行的,如果此處不調用 CRetangle 類的析構函數,就會導致被釋放的對象中動態(tài)分配的內存以后再也沒有機會回收。

綜上所述,人們希望delete p;這樣的語句能夠聰明地根據 p 所指向的對象執(zhí)行相應的析構函數。實際上,這也是多態(tài)。為了在這種情況下實現多態(tài),C++ 規(guī)定,需要將基類的析構函數聲明為虛函數,即虛析構函數。

改寫上面程序中的 CShape 類,在析構函數前加 virtual 關鍵字,將其聲明為虛函數:

class CShape{
public:
    virtual ~CShape() { cout << "CShape::destrutor" << endl; }
};

則程序的輸出變?yōu)椋?/span>

CRectangle::destrutor
CShape::destrutor

說明 CRetangle 類的析構函數被調用了。實際上,派生類的析構函數會自動調用基類的析構函數,只要基類的析構函數是虛函數,那么派生類的析構函數不論是否用virtual關鍵字聲明,都自動成為虛析構函數。

一般來說,一個類如果定義了虛函數,則最好將析構函數也定義成虛函數,析構函數可以是虛函數,但是構造函數不能是虛函數。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2010-01-18 17:38:54

C++虛函數表

2022-07-18 15:32:37

C++虛函數表

2025-05-12 02:45:00

2024-04-22 13:22:00

虛函數象編程C++

2024-01-23 10:13:57

C++虛函數

2024-12-17 12:00:00

C++對象模型

2010-01-27 17:16:52

C++構造函數

2010-01-26 10:42:26

C++函數

2010-02-01 11:22:09

C++虛函數

2010-01-19 13:43:59

C++函數

2010-02-03 10:50:33

C++多態(tài)

2010-01-18 13:54:28

函數

2011-07-15 00:47:13

C++多態(tài)

2015-03-23 10:04:43

c++編譯器c++實現原理總結

2010-11-22 16:01:08

C++多態(tài)

2011-04-12 10:40:04

C++多態(tài)

2010-01-28 16:16:32

C++多態(tài)性

2010-01-21 09:34:57

C++語法

2010-01-27 16:05:06

C++堆棧

2010-01-27 10:36:54

C++虛函數
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 怀柔区| 江川县| 眉山市| 大姚县| 南昌市| 屏南县| 襄城县| 庆阳市| 彩票| 肥城市| 威远县| 巫山县| 彭泽县| 乌拉特前旗| 九龙坡区| 湖南省| 屏东县| 西华县| 延寿县| 中卫市| 乐业县| 成安县| 崇阳县| 米脂县| 五华县| 富宁县| 陵水| 东光县| 阜康市| 油尖旺区| 巴彦淖尔市| 沅陵县| 福鼎市| 进贤县| 西城区| 离岛区| 蓬莱市| 成安县| 商城县| 湾仔区| 浏阳市|