偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

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

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

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

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

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

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

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

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

(1)靜態(tài)多態(tài)的實(shí)現(xiàn)

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

#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)的實(shí)現(xiàn)

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

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

以下段程序?yàn)槔?/span>

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

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;
}

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

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

class Shape {
public:
    virtual void draw() = 0; // 純虛函數(shù),使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;
}

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

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

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

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

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

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() {
    // 假設(shè)這里有一個變量表示角色類型
    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();
    }
}

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

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

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;
}

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

1.4多態(tài)的實(shí)現(xiàn)原理

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

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

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

(2)重寫(Overriding):通過繼承讓派生類重新實(shí)現(xiàn)基類的虛函數(shù)。在運(yùn)行時通過指針/引用的實(shí)際類型調(diào)用對應(yīng)的函數(shù)。

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):通過模板和泛型實(shí)現(xiàn)針對不同類型具有不同實(shí)現(xiàn)的函數(shù)。在編譯時通過傳入類型決定具體實(shí)現(xiàn)。

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

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

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

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

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

一個接口,多種方法:

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

二、什么是虛函數(shù)表?

2.1虛函數(shù)表是什么

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

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

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

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

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類分別生成各自的虛函數(shù)表。Character類的虛函數(shù)表中,Attack函數(shù)的地址指向基類中Attack函數(shù)的實(shí)現(xiàn)代碼;Warrior類的虛函數(shù)表,由于重寫了Attack函數(shù),所以表中Attack函數(shù)的地址指向Warrior類中重寫后的Attack函數(shù)實(shí)現(xiàn)代碼,Mage類同理。這樣,在程序運(yùn)行時,就能根據(jù)對象的實(shí)際類型,通過虛函數(shù)表準(zhǔn)確地找到并調(diào)用相應(yīng)的攻擊函數(shù)。

2.2為什么需要虛函數(shù)表

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

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

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

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

2.3在內(nèi)存中的布局

(1)對象內(nèi)存布局中的虛函數(shù)表指針

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

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

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

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

#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;

    // 獲取對象的地址并轉(zhuǎn)換為整數(shù)指針,用于讀取內(nèi)存中的數(shù)據(jù)
    int* ptr = reinterpret_cast<int*>(&dog);
    // 讀取對象內(nèi)存起始處的4個字節(jié),即為虛函數(shù)表指針的值
    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對象的地址轉(zhuǎn)換為整數(shù)指針,這樣就可以通過指針操作讀取對象內(nèi)存中的數(shù)據(jù) 。*ptr讀取的就是對象內(nèi)存起始處的 4 個字節(jié),也就是虛函數(shù)表指針的值 。通過輸出這個值,我們能直觀地看到虛函數(shù)表指針在對象內(nèi)存中的位置和它所指向的虛函數(shù)表地址。

(2)虛函數(shù)表自身在內(nèi)存中的位置

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

為了驗(yàn)證這一結(jié)論,我們來看下面的代碼示例:

#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;
    // 獲取對象的虛函數(shù)表指針
    int* vptr = reinterpret_cast<int*>(&obj);
    // 通過虛函數(shù)表指針獲取虛函數(shù)表的地址
    int vtable_address = *vptr;

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

    return 0;
}

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

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

(1)單繼承無覆蓋

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

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類包含兩個虛函數(shù)Func1和Func2,Derived類繼承自Base類,并且新增了兩個虛函數(shù)Func3和Func4 。此時,Derived類的虛函數(shù)表中,首先會按照聲明順序依次排列父類Base的虛函數(shù)Func1和Func2的地址,然后再接著排列子類Derived新增的虛函數(shù)Func3和Func4的地址 。

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

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

(2)單繼承有覆蓋

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

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類的虛函數(shù)表中,原本指向Base::Func1的函數(shù)地址會被替換為Derived::Func1的函數(shù)地址 。而Func2的地址保持不變,因?yàn)樗鼪]有被覆蓋 。新增的虛函數(shù)Func3和Func4依然按照順序排在后面 。

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

(3)多繼承情況

多繼承時,虛函數(shù)表的結(jié)構(gòu)變得更加復(fù)雜 。假設(shè)有如下代碼:

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類會擁有兩個虛函數(shù)表,分別對應(yīng)Base1和Base2 。在Derived類對象的內(nèi)存布局中,首先是對應(yīng)Base1的虛函數(shù)表指針,然后是Base1類的其他成員(如果有),接著是對應(yīng)Base2的虛函數(shù)表指針,再后面是Base2類的其他成員(如果有),最后是Derived類自己的成員 。

對于對應(yīng)Base1的虛函數(shù)表,其中Func1的地址會被Derived::Func1的地址覆蓋(因?yàn)镈erived類重寫了Func1 ),F(xiàn)unc2的地址保持為Base1::Func2的地址 。而新增的虛函數(shù)Func5會被添加到這個虛函數(shù)表的末尾 。對應(yīng)Base2的虛函數(shù)表中,F(xiàn)unc3和Func4的地址分別是Base2::Func3和Base2::Func4的地址,因?yàn)镈erived類沒有重寫這兩個函數(shù) 。

假設(shè)在 64 位系統(tǒng)下,Derived類對象內(nèi)存起始地址為0x1000:

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

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

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

3.1多態(tài)在設(shè)計模式中的應(yīng)用

多態(tài)作為面向?qū)ο缶幊痰暮诵奶匦灾唬诟鞣N設(shè)計模式中發(fā)揮著舉足輕重的作用。它為設(shè)計模式提供了更加靈活和強(qiáng)大的解決方案,使得軟件系統(tǒng)的結(jié)構(gòu)更加清晰、可維護(hù)性更強(qiáng)。下面我們就來探討一下多態(tài)在策略模式和工廠方法模式中的具體應(yīng)用 。

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

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

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

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

然后,分別創(chuàng)建具體的運(yùn)算策略類,如加法策略類、減法策略類、乘法策略類和除法策略類,它們都繼承自O(shè)perationStrategy接口,并實(shí)現(xiàn)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指針,并通過該指針調(diào)用具體的運(yùn)算策略:

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

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

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;

    // 釋放內(nèi)存
    delete addStrategy;
    delete subtractStrategy;
    delete multiplyStrategy;
    delete divideStrategy;

    return 0;
}

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

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

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

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

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

然后,分別創(chuàng)建具體的角色類,如戰(zhàn)士類、法師類和刺客類,它們都繼承自Character類,并實(shí)現(xiàn)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類,并實(shí)現(xiàn)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();

    // 釋放內(nèi)存
    delete warrior;
    delete mage;
    delete assassin;
    delete warriorFactory;
    delete mageFactory;
    delete assassinFactory;

    return 0;
}

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

3.2虛函數(shù)表在編程中的實(shí)踐

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

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

#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)();// 定義函數(shù)指針類型,用于指向虛函數(shù)

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

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

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

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

    return 0;
}

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

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

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

虛函數(shù)表在多態(tài)編程中有著廣泛的應(yīng)用,它使得 C++ 能夠?qū)崿F(xiàn)不同類型對象的統(tǒng)一接口調(diào)用,大大提高了代碼的可擴(kuò)展性和靈活性 。下面以一個圖形繪制系統(tǒng)為例,來說明虛函數(shù)表在實(shí)際項(xiàng)目中的應(yīng)用 。

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

#include <iostream>

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

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

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類型指針或引用來操作不同類型的圖形對象,無需關(guān)心具體的圖形類型 :

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函數(shù)接受一個Shape類型的指針數(shù)組和數(shù)組的大小,通過遍歷數(shù)組并調(diào)用每個Shape對象的Draw函數(shù),實(shí)現(xiàn)了對不同類型圖形的統(tǒng)一繪制操作 。在運(yùn)行時,根據(jù)每個指針實(shí)際指向的對象類型(Circle、Rectangle或Triangle),虛函數(shù)表會動態(tài)地確定調(diào)用哪個類的Draw函數(shù),從而實(shí)現(xiàn)了多態(tài)性 。

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

四、虛析構(gòu)函數(shù)(防止沒有析構(gòu)到子類導(dǎo)致內(nèi)存泄漏)

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

例如下面的程序:

#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;  //只根據(jù) p 的類型是 CShape * 來決定應(yīng)該調(diào)用 CShape 類的析構(gòu)函數(shù),這樣就無法析構(gòu)到,子類的析構(gòu)函數(shù)
    return 0;
}

程序的輸出結(jié)果如下:

CShape::destrutor

輸出結(jié)果說明,delete p;只引發(fā)了 CShape 類的析構(gòu)函數(shù)被調(diào)用,沒有引發(fā) CRectangle 類的析構(gòu)函數(shù)被調(diào)用。這是因?yàn)樵撜Z句是靜態(tài)聯(lián)編的,編譯器編譯到此時,不可能知道此時 p 到底指向哪個類型的對象,它只根據(jù) p 的類型是 CShape * 來決定應(yīng)該調(diào)用 CShape 類的析構(gòu)函數(shù)。

按理說,delete p;會導(dǎo)致一個 CRectangle 類的對象消亡,應(yīng)該調(diào)用 CRectangle 類的析構(gòu)函數(shù)才符合邏輯,否則有可能引發(fā)程序的問題。

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

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

改寫上面程序中的 CShape 類,在析構(gòu)函數(shù)前加 virtual 關(guān)鍵字,將其聲明為虛函數(shù):

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

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

CRectangle::destrutor
CShape::destrutor

說明 CRetangle 類的析構(gòu)函數(shù)被調(diào)用了。實(shí)際上,派生類的析構(gòu)函數(shù)會自動調(diào)用基類的析構(gòu)函數(shù),只要基類的析構(gòu)函數(shù)是虛函數(shù),那么派生類的析構(gòu)函數(shù)不論是否用virtual關(guān)鍵字聲明,都自動成為虛析構(gòu)函數(shù)。

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

責(zé)任編輯:武曉燕 來源: 深度Linux
相關(guān)推薦

2010-01-18 17:38:54

C++虛函數(shù)表

2022-07-18 15:32:37

C++虛函數(shù)表

2025-05-12 02:45:00

2024-01-23 10:13:57

C++虛函數(shù)

2024-04-22 13:22:00

虛函數(shù)象編程C++

2025-09-29 01:15:00

2025-09-15 02:00:00

2024-12-17 12:00:00

C++對象模型

2010-01-27 17:16:52

C++構(gòu)造函數(shù)

2010-01-26 10:42:26

C++函數(shù)

2010-02-01 11:22:09

C++虛函數(shù)

2010-01-19 13:43:59

C++函數(shù)

2010-02-03 10:50:33

C++多態(tài)

2011-07-15 00:47:13

C++多態(tài)

2015-03-23 10:04:43

c++編譯器c++實(shí)現(xiàn)原理總結(jié)

2010-01-18 13:54:28

函數(shù)

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-27 10:36:54

C++虛函數(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號