C++之虛函數

才高八斗船帆ub 發佈 2022-12-08T11:06:29.417391+00:00

首先我們來回顧一下虛函數,在C++中是使用virtual關鍵字修飾的函數就是虛函數,下面是一個簡單的虛函數例子:虛函數必須在基類中實現,如果不實現的話就會編譯報錯。

都說面向對象的三大特性是封裝、繼承、多態。C++作為一門面向對象程式語言,肯定也是具備了面向對象的三大特性,那麼在C++中是如何實現多態的呢?

在C++中是通過虛函數動態綁定的方式實現多態的。

虛函數與純虛函數

首先我們來回顧一下虛函數,在C++中是使用virtual關鍵字修飾的函數就是虛函數,下面是一個簡單的虛函數例子:

class Base{
public:
    // 虛函數,必須實現,否則編譯報錯
    virtual void f1() const{
        std::cout << "我是Base的f1函數" << std::endl;
    }
};

class A:public Base{
    void f1() const override{
        std::cout << "我是A的f1函數" << std::endl;
    }
};

虛函數必須在基類中實現,如果不實現的話就會編譯報錯。

如果我們不想實現虛函數的話可以將其聲明為純虛函數,純虛函數的聲明方式如下:

virtual 返回值類型 函數名(函數參數) = 0;

聲明了純虛函數的類稱為抽象類,繼承抽象類的最終子類必須父類的純虛函數,否則不能生產對應的類對象。以下是一個純虛函數的例子:

class Base{
public:
    // 虛函數,必須實現,否則編譯報錯
    virtual void f1() const{
        std::cout << "我是Base的f1函數" << std::endl;
    }

    // 純虛函數,不需要實現
    virtual void f2() const = 0;
};

class A:public Base{
public:
    void f1() const override{
        std::cout << "我是A的f1函數" << std::endl;
    }

    void f2() const override{
        std::cout << "我是A的純虛函數f2" << std::endl;
    }
};

有了虛函數我們就能通過基類的的指針進行動態綁定,在運行時訪問到子類的函數,但是動態綁定只能發生在指針或引用上。例如在以下的例子中,函數test3是不會訪問到子類的函數的, 它訪問的函數依然是基類的虛函數,也就是說它沒有發生動態綁定,因為它既不是指針也不是引用。

class Base{
public:
    // 虛函數,必須實現,否則編譯報錯
    virtual void f1() const{
        std::cout << "我是Base的f1函數" << std::endl;
    }
};

class A:public Base{
public:
    void f1() const override{
        std::cout << "我是A的f1函數" << std::endl;
    }
};

// 引用傳遞參數
void test1(const Base &base){
    base.f1();
}

// 指針傳遞參數
void test2(Base *base){
    base->f1();
}

// 非引用、非指針傳遞參數
void test3(Base base){
    base.f1();
}

int main(int arg,char** argv) {
    Base *base =  new A();
    test1(*base);
    test2(base);
    test3(*base);
    return 0;
}

【騰訊文檔】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音視頻流媒體高級開發-資料領取FFmpegWebRTCRTMPRTSPHLSRTP鎾斁鍣�-闊寵棰戞祦濯掍綋楂樼駭寮€鍙�-璧勬枡棰嗗彇



運行列印結果:

我是A的f1函數
我是A的f1函數
我是Base的f1函數

如果一個類可能會被繼承,這個類的析構函數應該被聲明為一個虛函數,否則會引發內存泄漏。例如以下例子:

class Base{
public:
    // 虛函數,必須實現,否則編譯報錯
    virtual void f1() const{
        std::cout << "我是Base的f1函數" << std::endl;
    }

    ~Base(){
        std::cout << "Base的析構函數" << std::endl;
    }
};

class A:public Base{
public:
    void f1() const override{
        std::cout << "我是A的f1函數" << std::endl;
    }

    ~A(){
        std::cout << "A的析構函數" << std::endl;
    }
};

int main(int arg,char** argv) {
    Base *base =  new A();
    delete base;
    return 0;
}

運行輸出如下:

Base的析構函數

從運行結果可以看出A沒有被正確析構,這是因為它的基類Base的析構函數沒有被聲明為虛函數的原因,此時只要我們把Base類的析構函數聲明為虛函數即可修復這個內存泄漏的問題,也就是:

class Base{
public:
    // 虛函數,必須實現,否則編譯報錯
    virtual void f1() const{
        std::cout << "我是Base的f1函數" << std::endl;
    }

    // 可能被繼承的類的析構函數應該是一個虛函數
   virtual ~Base(){
        std::cout << "Base的析構函數" << std::endl;
    }
};

虛函數總結:

1、當我們在派生類中覆蓋了某個虛函數時,可以再一次使用virtual關鍵字指出該函數的性質。然而這麼做並非必須,因為一旦某個函數被聲明成虛函數,則在所有派生類中它都是虛函數。

2、虛函數只有在引用或者指針調用時才會發生動態綁定;

3、基類的析構函數需要聲明為虛函數;

4、虛函數必須要在基類實現,不實現,編譯會報錯;

5、如果子類沒有實現父類的純虛函數,則該子類不能被構造成一個對象。

多態實現原理-虛函數表

通過上面的例子我們知道了在C++中通過引用或指針的形式進行虛函數的動態綁定而實現多態,那麼動態綁定在C++中是如何實現呢?答案是虛函數表。

所謂的虛函數表就是:

當編譯器在編譯過程中遇到virtual關鍵字時,它不會對函數調用進行綁定,而是為包含虛函數的類建立一張虛函數表Vtable。在虛函數表中,編譯器按照虛函數的聲明順序依次保存虛函數地址。同時,編譯器會在類中添加一個隱藏的虛函數指針VPTR,指向虛函數表。在創建對象時,將虛函數指針VPTR放置在對象的起始位置,為其分配空間,並調用構造函數將其初始化為虛函數表地址。需要注意的是,虛函數表不占用對象空間。

虛函數表總結:

1、單繼承下的虛函數表

虛函數表中的指針順序,按照虛函數聲明的順序排序;基類的虛函數指針在派生類的前面。

2、多繼承下的虛函數表

多繼承關係下會有多個虛函數表,也會有多個指向不同虛函數表的指針;

關鍵字: