1. 程式人生 > >C++多型(上)——虛擬函式、虛表

C++多型(上)——虛擬函式、虛表

OOP的核心思想是多型性(polymorphism)。其含義是“多種形態”。我們把具有繼承關係的多個型別稱為多型型別。引用或指標的靜態型別動態型別不同這一事實正是C++語言支援多型性的根本所在。

多型性:當用於面向物件程式設計的範疇時,多型性的含義是指程式能通過引用或指標的動態型別來獲取型別特定行為的能力。

多型性在C++中是通過虛擬函式來實現的。

首先我們先來強調幾組概念:
靜態多型和動態多型、靜態聯編和動態聯編、靜態型別和動態型別

1. 靜態型別和動態型別

靜態型別——在編譯時是已知的,它是在變數宣告時的型別或表示式生成的型別。
動態型別——物件在執行時的型別。引用所引物件或指標所指物件的動態型別可能與該引用或指標的靜態型別不同。基類的指標或引用可以指向一個派生類物件,在這種情況下,靜態型別是基類的引用(或指標),而動態型別是派生類的引用(或指標)。

Devese d;
Base *pb = &d;  //靜態型別是基類的指標,動態型別是派生類的指標

2. 靜態聯編和動態聯編

靜態聯編——也稱靜態繫結或早期聯編,比如之前提到的函式過載,運算子過載。它是在編譯過程彙總進行的聯編。
動態聯編——也稱動態繫結,直到執行時才知道到底呼叫函式的哪個版本。在C++語言中,動態繫結的意思是在執行時根據引用或指標所繫結物件的實際型別來選擇執行虛擬函式的某一個版本。

3. 靜態多型和動態多型

(下圖中“多型多型”應該是“動態多型”,實在抱歉)

這裡寫圖片描述

虛擬函式

虛擬函式——用於定義型別特定行為的成員函式。通過指標或引用對虛擬函式的呼叫直到執行時才被解析,依據是引用或指標所繫結物件的實際型別。

虛擬函式的用法格式:
virtual 函式返回型別 函式名 (引數列表) {函式體}

當我們使用基類的指標或引用基類中定義的一個函式時,我們並不知道該函式真正作用的物件是什麼型別,因為它可能是一個基類的物件也可能是一個派生類的物件。如果該函式是虛擬函式,則直到執行時才會決定到底執行哪個版本,判斷的依據是引用或指標所繫結物件的實際型別;反之,如果是非虛擬函式,對非虛擬函式的呼叫在編譯時進行繫結。
類似的,通過物件的函式呼叫也在編譯時繫結。物件的型別是確定不變的,我們無論如何都不可能令物件的靜態型別和動態型別不一致。因此,通過物件進行的函式呼叫將在編譯時被繫結到該物件所屬類中的函式版本上。

——摘自《C++ Primer》

看下面一段程式碼:

class Father
{
public:
    void fun()
    {
        cout << "Father::fun()" << endl;
    }
protected:
    int _f;
};

class Child  :public Father
{
public:
    void fun()
    {
        cout << "Child::fun()" << endl;
    }
protected:
    int _c;
}; 

int main()
{
    Child c;
    Father f;
    Father *pf = &c;
    pf->fun();
    system("pause");
    return 0;
}

輸出的結果是:
這裡寫圖片描述
上面程式中fun函式是非虛擬函式,所以呼叫函式在編譯時進行繫結,所以呼叫基類中的fun()函式。

下面我們將fun()函式設為虛擬函式,看結果有何不同?

class Father
{
public:
    virtual void fun()  //定義為虛擬函式
    {
        cout << "Father::fun()" << endl;
    }
protected:
    int _f;
};

class Child  :public Father
{
public:
    virtual void fun()  //定義為虛擬函式
    {
        cout << "Child::fun()" << endl;
    }
protected:
    int _c;
}; 

int main()
{
    Child c;
    Father f;
    Father *pf = &c;
    pf->fun();
    system("pause");
    return 0;

}

執行結果:
這裡寫圖片描述

兩次的結果明顯不同,這次呼叫的是派生類的fun()函式,因為是虛擬函式呼叫在執行時才知道呼叫函式哪個版本。
Father *pf = &c; //指標pf的靜態型別是基類指標,但是動態型別是派生類指標,而pf指標所繫結的物件的真是型別是派生類指標。所以呼叫派生類類中的fun()函式。

再看下面一段程式碼:

lass Father
{
public:
    virtual void fun() //虛擬函式
    {
        cout << "Father::fun()" << endl;
    }
protected:
    int _f;
};

class Child  :public Father
{
public:
    virtual void fun()   //虛擬函式
    {
        cout << "Child::fun()" << endl;
    }
protected:
    int _c;
}; 

int main()
{
    Child c;
    Father f;
    f.fun();
    c.fun();
    system("pause");
    return 0;

}

執行結果:
這裡寫圖片描述

class Father
{
public:
    void fun()
    {
        cout << "Father::fun()" << endl;
    }
protected:
    int _f;
};

class Child  :public Father
{
public:
    void fun()
    {
        cout << "Child::fun()" << endl;
    }
protected:
    int _c;
}; 

int main()
{
    Child c;
    Father f;
    f.fun();
    c.fun();
    system("pause");
    return 0;

}

執行結果:
這裡寫圖片描述
上面兩段程式碼說明:
通過物件呼叫函式時,與是不是虛擬函式無關,因為函式呼叫是在編譯時繫結的。繫結的是所屬類中的函式版本。

虛擬函式工作原理

我們先來了解一個概念:虛擬函式表
虛擬函式表:虛擬函式表又稱虛表(vtbl),是一塊連續的記憶體,編譯器會為每一個含有虛擬函式的類建立一個虛表,該虛表將被所有該類的所有物件共享,裡面儲存的是該類的虛擬函式的地址。虛表的大小是N*4(N個虛擬函式,一個虛擬函式佔一行,最後以0結尾)。虛擬函式的實現就是通過虛表來實現的。之前講到只有虛擬函式才能被覆蓋,就是指的是虛表中虛擬函式地址被覆蓋。
在有虛擬函式的類例項化時,編譯器分配了指向該表的指標的記憶體(虛擬函式表指標vptr),簡單一點就是便一起給每個物件添加了一個隱藏成員,這個隱藏成員中儲存了一個指向虛擬函式表的指標。這意味著可以通過類例項化的地址得到虛表,然後遍歷其中的函式指標,並呼叫相應的函式。
注:
1,基類物件含有一個指標,該指標指向基類中所有虛擬函式的地址表。派生類物件將含有一個指向獨立地址表的指標。
2,如果派生類提供了基類虛擬函式的重定義,該虛擬函式表將儲存新函式的地址。即就是虛擬函式覆蓋實際是對虛擬函式表中的虛擬函式的地址的覆蓋。
3,如果派生類定義了新的虛擬函式,則該函式的地址將被加入到虛擬函式表中。注意,無論類中是一個還是多個虛擬函式,都只需在物件中新增一個地址成員,只是表的大小不同。

下面我們通過一個例子來看虛擬函式機制記憶體佈局

class Father
{
public:
    virtual void fun1()
    {
        cout << "Father::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Father::fun2()" << endl;
    }

    int _f;
};

class Child  :public Father
{
public:
    int _c;
}; 

int main()
{
    Father f;
    f._f = 1;
    Child c;
    c._c = 2;
    system("pause");
    return 0;
}

這裡寫圖片描述
我們可以發現當例項化時編譯器就為物件生成了一個隱藏成員(虛表指標),也就是本例中的“1c 8c c8 00”.後面通過看虛表指標指向的虛表,可以發現有兩個地址,他們從上到下分別就是fun1(),fun2()的地址,第三行是0,虛表就是以NULL結尾的。
那我們來畫一下記憶體佈局:
這裡寫圖片描述

下面我們來在派生類中對基類中的虛擬函式fun2()進行重定義,也就是覆蓋。看看會有什麼不同。

class Father
{
public:
    virtual void fun1()
    {
        cout << "Father::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Father::fun2()" << endl;
    }

    int _f;
};

class Child  :public Father
{
public:
    virtual void fun2()   //覆蓋基類中函式fun2()
    {
        cout << "Child::fun2()" << endl;
    }
    int _c;
}; 

int main()
{
    Father f;
    f._f = 1;
    Child c;
    c._c = 2;
    system("pause");
    return 0;
}

這裡寫圖片描述

下面這幅圖就能證明上面這幅圖標註的正確性

這裡寫圖片描述
下面將派生類中覆蓋基類虛擬函式fun2()的函式改為虛擬函式fun3(),看看會出現什麼情況?

class Father
{
public:
    virtual void fun1()
    {
        cout << "Father::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Father::fun2()" << endl;
    }

    int _f;
};

class Child  :public Father
{
public:
    virtual void fun3()
    {
        cout << "Child::fun3()" << endl;
    }
    int _c;
}; 

int main()
{
    Father f;
    f._f = 1;
    Child c;
    c._c = 2;
    system("pause");
    return 0;

}

這裡寫圖片描述
看上面這個例子,說明如果派生類定義了新的虛擬函式,則該函式的地址將被加入到虛擬函式表中。
今天就講到這,大家應該對虛擬函式、虛表有個清晰的認識。這裡是面試常問的。

相關推薦

C++——虛擬函式

OOP的核心思想是多型性(polymorphism)。其含義是“多種形態”。我們把具有繼承關係的多個型別稱為多型型別。引用或指標的靜態型別和動態型別不同這一事實正是C++語言支援多型性的根本所在。 多型性:當用於面向物件程式設計的範疇時,多型性的含義是指

c++學習總結——虛擬函式

一、學習總結     在面向物件程式設計中,多型性是指一個名字,多種語義;或者介面相同,多種實現。過載函式是多型性的一種簡單形式。C++為類體系提供一種靈活的多型機制——虛擬函式。虛擬函式允許函式呼叫與函式體的聯絡在執行時才進行,成為動態聯編。類、繼承和多型,提供了對軟體重用性

北京大學MOOC C++學習筆記虛擬函式

虛擬函式: 在類的定義中,前面有 virtual 關鍵字的成員函式就是虛擬函式。 class base { virtual int get() ; }; int base::get() { } virtual 關鍵字只用在類定義裡的函式宣告中,寫函式體時不用。 多型的表現

C++】—動態

一、多型 1、 概念:同一事物表現出的多種形態,同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。在執行時,可以通過指向基類的指標,來呼叫實現派生類中的方法。 2、 舉例子: #include<windows.h> class WashRoom { pu

C++程式碼閱讀——虛擬函式

C++程式碼閱讀之虛擬函式 virtual bool sendPoseAdjustRequest(int request_code, bool switch_x,bool switch_

c++的原理 以及虛擬函式詳解

c++中多型的原理 要實現多型,必然在背後有自己的實現機制,如果不瞭解其背後的機制,就很難對其有更深的理解。 一個多型的例子 class Person{ public: virtual void Drink() { cout << "drink water" &

C語言面向物件程式設計:虛擬函式3

 在《 C++ 程式設計思想》一書中對虛擬函式的實現機制有詳細的描述,一般的編譯器通過虛擬函式表,在編譯時插入一段隱藏的程式碼,儲存型別資訊和虛擬函式地址,而在呼叫時,這段隱藏的程式碼可以找到和實際物件一致的虛擬函式實現。     我們在這裡提供

從零開始學C++之虛擬函式虛擬函式指標解構函式object slicing與虛擬函式C++物件模型圖

#include <iostream>using namespace std;class CObject {public:     virtual void Serialize()     {         cout << "CObject::Serialize ..." <&

C++】——再探&不同繼承下帶有虛擬函式的物件模型

一、虛擬函式 1、概念:簡單地說,那些被virtual關鍵字修飾的成員函式,就是虛擬函式。虛擬函式的作用,用專業術語來解釋就是實現多型。 2、程式碼示例: class Base { public: virtual void TestFunc1() {

C++類的繼承關係——繼承未重寫虛擬函式

   首先,在介紹有虛擬函式的多繼承(未重寫虛擬函式),先介紹一下以下概念。        多型:多種形態,簡單地說父類的指標或引用呼叫重寫的虛擬函式,當父類的指標或引用指向父類物件呼叫的就是父類的虛擬函式,指向子類物件時呼叫的就是是子類的虛擬函式        虛擬

C#方法,抽象,介面實現

轉自   淺談C# 多型的魅力(虛方法,抽象,介面實現) 前言:我們都知道面向物件的三大特性:封裝,繼承,多型。封裝和繼承對於初學者而言比較好理解,但要理解多型,尤其是深入理解,初學者往往存在有很多困惑,為什麼這樣就可以?有時候感覺很不可思

C++中動實現之虛擬函式指標

1、靜多型與命名傾軋,動多型與虛擬函式: (1)概述: 我們知道,C++的多型有靜多型(Static polymorphism)與動多型(Dynamic polymorphism)之分,靜多型是依靠函式過載(function overloading)實現的,

C++繼承與

一、繼承相關概念   1.繼承:繼承是面向物件複用的重要手段。通過繼承定義一個類,繼承是型別之間的關係建模,共享共有的東西,實現各自本質不同的東西。     2.成員訪問限定符與繼承的關係    

c++的靜態和動態筆記

多型(polymorphism)一詞最初來源於希臘語polumorphos,含義是具有多種形式或形態的情形。在程式設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。和純粹的面向物件程式設計語言不同,C++中的多型有著更廣泛的含義。除了常見

C++精進篇之―封裝繼承及訪問許可權及物件建立詳解

         面向物件的三個基本特徵是:封裝、繼承、多型。其中,封裝可以隱藏實現細節,使得程式碼模組化;繼承可以擴充套件已存在的程式碼模組(類);它們的目的都是為了——程式碼重用。而多型則是為了實現另一個目的——介面重用!                        

c++知識歸納】繼承與

        c++是基於面向物件的程式語言,面向物件的三大特性為:封裝、繼承和多型。本文對繼承與多型的知識點進行了總結歸納,這部分內容對於學習c++程式語言是非常重要的,文章加入我的個人理解,希望

JavaScript高級程序設計之引用類

5-0 歸並 高級 new es5 順序 回調函數 比較 並且 引用類型是比較復雜的一種類型。在面向對象的編程中,會經常用到引用類型,可以說它是編程語言的核心。在JS中內置了幾種引用類型,下邊會進行一一的介紹。 內置引用類型 Object類型 1、聲明方式:直接使用new操

課堂筆記 ------python數據類

組類型 字典類 十六 吃飯 聲明 數值類型 元組類型 小數 三引號 python數據類型 python中包含6中標準類型:1.Number 數值類型2.String 字符串類型3.List 列表類型4.Tuple 元組類型5.Dict 字典類型6.Set

通過C學Python1關於語言數值類和變量

而在 有一種 float char 有用 運行時 復數運算 單獨的數 數值類型 強類型語言,不可能出現“程序執行出錯後仍可繼續執行,並且該錯誤導致的問題使後續執行可能出現任意行為”這類錯誤,這類錯誤的例子,如C裏的緩沖區溢出、Jump到錯誤地址。 弱類型語言,類型檢查更不嚴

詳解C#泛

安全 情況 重用 模板 信息 普通 cast 綁定 封閉式   一、C#中的泛型引入了類型參數的概念,類似於C++中的模板,類型參數可以使類型或方法中的一個或多個類型的指定推遲到實例化或調用時,使用泛型可以更大程度的重用代碼、保護類型安全性並提高性能;可以創建自定義的泛型類