C++幕後故事(六)--函數我來調你了

這個章節咱們會學到如下3個知識點:html

1.不一樣的類型函數是怎麼調用的。
2.成員函數指針各個模型實現的原理是什麼以及各個指針的效率如何。
3.inline函數的注意事項。
linux

1.普通成員調用

看下面的代碼,這裏面我分別調用了類成員和全局函數設計模式

class NormalCall {
public:
    void Add(int number) {
        number+m_add;
    }
    static void ClassStatic() {
        cout << "ClassStatic" << endl;
    }
    virtual void ClassVirutal() {
        cout << "ClassVirutal" << endl;
    }
public:
    int m_add;
};

void Add(NormalCall *nc, int number) {
    nc->m_add + number;
}

void test_normal_call() {
    NormalCall nc;
    nc.Add(1);
    // 其實被編譯器轉成__ZN6NormalCall6AddEi(&nc, 1)
    // 編譯器在調用類的普通成員函數時,會在函數的參數中隱式添加了一個this指針,這個指針
    // 就是當前生成對象的首地址。同時對普通成員變量的存取也是經過this指針
    Add(&nc, 1);     // 調用全局函數
}

int main() {
    return 0;
}
複製代碼

把上面的源代碼保存文件main.cpp,而後在linux平臺上用g++ -Wall –g main.cpp –o main,再用nm main,就會導出main裏面的符號等等其餘東西。我把重要的東西拿出來看下。函數

080486f0 T __x86.get_pc_thunk.bx
08048820 T _Z16test_normal_callv
0804881b T _Z3AddP10NormalCalli
08048888 t _Z41__static_initialization_and_destruction_0ii
U _ZdlPv@@GLIBCXX_3.4
08048bea W _ZN10NormalCall12ClassVirutalEv
08048be4 W _ZN10NormalCall3AddEi

從這裏咱們這裏能夠看出,咱們寫代碼的時候名字就是Add,可是編譯完以後名稱全變了。_Z3AddP10NormalCalli咱們能夠猜想下就是咱們寫的Add(NormalCall, int)原型。_ZN10NormalCall3AddEi應該就是咱們的NormalCall成員函數Add(int)原型。你可能會奇怪,爲何C++編譯器編譯出來的名稱都變了,這種作法叫作name mangling(命名粉碎),實際上是爲了支持C++另一個特性,就是函數重載的特性。同時,也是C++確保調用普通成員函數,和調用全局函數的在效率上是一致的。工具

靜態成員函數調用

void test _static_call()
{
    NormalCall *pNC = new NormalCall();
    pNC->ClassVirutal();

    NormalCall NC;
NC.ClassStatic();

    pNC->ClassStatic();
    NormalCall::ClassStatic();

}
複製代碼

; 上面三種調用static函數的生成的反彙編代碼是一致的。
22C7D4 call NormalCall::ClassStatic (0221550h)
22C7D9 call NormalCall::ClassStatic (0221550h)
22C7DE call NormalCall::ClassStatic (0221550h)
佈局

總結:學習

1.靜態成員函數,沒有this指針。
沒法直接存取類中普通的非靜態成員變量。
3.調用方式能夠向類中普通的成員函數,也能夠用ClassName::StaticFunction。
4.能夠將靜態的成員函數在某些環境下當作回調函數使用。
5.靜態的成員函數不可以被聲明爲const、volatile和virtual。
測試

3.虛函數調用

關於虛函數在第四章作了專門的介紹,這裏就不在囉嗦了。ui

4.成員函數指針

從字面意思看,有兩點內容。this

1.是個指針
2.指向的類的成員函數

4.1普通成員函數指針

class Car {
public:
    void Run() { cout << "Car run" << endl; }
    static void Name() { cout << "lexus" << endl; }
};

void test_function_pointer() {
    // void (Car::*fRun)() = &Car::Run;
    // 能夠將成員函數指針分紅四步看
    void        // 1.返回值
    (Car::*      // 2.哪一個類的成員函數指針
    fRun)       // 3.函數指針名稱
    ()           // 4.參數列表
    = &Car::Run;

    Car car;
    (car.*fRun)();
}
複製代碼

從上面的代碼中看出,定義一個成員函數指針,只須要注意四步就行:

1.返回值。
2.哪一個類的成員函數指針。
3.函數指針名稱。
4.參數列表。

在注意調用的方式(car.fRun)(),它比日常調用car.Run()時候多了個

這個背後實現的原理:

每一個成員函數都一個固定的地址,把普通成員函數的地址賦值給一個函數指針,而後在調用函數指針的時候再把this指針當作參數傳遞進去。這個就和普通成員函數調用的原理是一致的。

4.2 靜態成員函數指針

void test_function_pointer() {
    void (*fName)() = &Car::Name;
    fName();
}
複製代碼

注意到沒有咱們在定義靜態成員函數時,沒有加上類名Car。這是由於靜態函數裏面沒有this指針,因此就形成了不須要加上類名Car,同時也形成靜態成員函數不能直接使用類的普通成員變量和函數。你可能發現類的靜態成員函數,和全局函數很是相似,其實本質上是同樣的,都是一個可調用的地址。

4.3 虛擬成員函數指針

上面的兩節,咱們看了普通成員函數指針和靜態成員函數指針,以爲比較簡單。接下來的重頭戲虛擬成員函數指針,這裏的套路更深也更難理解,且聽我一步步道來。

4.3.1單繼承模型下調用

class Animal {
public:
    virtual ~Animal() { cout << "~Animal" << endl; }
    virtual void Name() { cout << "Animal" << endl; }
};

class Cat : public Animal
{
public:
    virtual ~Cat() { cout << "~Cat" << endl; }
    virtual void Name() { cout << "Cat Cat" << endl; }
};

class Dog : public Animal
{
public:
    virtual ~Dog() { cout << "~Dog" << endl; }
    virtual void Run() { cout << "Dog Run" << endl; }
    virtual void Name() { cout << "Dog Dog" << endl; }
};

void test_virtual_fucntion_pointer() {
    Animal *animal = new Cat();
    void (Animal::*fName)() = &Animal::Name;
    printf("fName address %p\n", fName);
    // fName address 00FD1802
    (animal->*fName)();
    // Cat Cat

    // 打印Cat的虛表中的Name地址
    Cat *cat = new Cat();
    long *vtable_address = (long *)(*((long *)(cat)));
    printf("virtual Name address %p\n", (long *)vtable_address[1]);
    // virtual Name address 00FD1429

    // 編譯器在語法層面阻止咱們獲取析構函數地址
    // 可是咱們知道的在虛函數章節裏面,咱們能夠經過虛表的地址間接獲取析構函數地址
    // void (Animal::*Dtor)() = &Animal::~Animal;
    // (animal->*Dtor)();

    printf("fName address %p\n", fName);
    // fName address 00FD1802
    animal = new Dog();
    (animal ->*fName)();
    // Dog Dog

    // 打印Dog的虛表中的Name地址
    Dog *dog = new Dog();
    long *dog_vtable_address = (long *)(*((long *)(dog)));
    printf("virtual Name address %p\n", (long *)dog_vtable_address[1]);
    // virtual Name address 00FD1672
}
複製代碼

在代碼中咱們定義了一個變量fName。

void (Animal::*fName)() = &Animal::Name;

並賦值爲&Animal:Name;咱們再打印出Name的地址0x009F1802。

咱們先思考這個地址到底指向誰?

這個地址就是虛函數的地址?若是是,那麼它的地址是父類的?仍是子類,若是那麼編譯器又是怎麼我指向的是哪一個虛函數地址?若是不是,那麼又是個什麼地址?接下里咱們一步步的經過彙編代碼驗證猜測。

咱們在VS的調試模式下,將鼠標移動fName變量上就會顯示一串信息。

image

顯示的什麼thunk,vcall{4…},都是什麼玩意看不懂。反彙編走一遍,究竟是個什麼錘子。

如下是關鍵的彙編代碼:

(animal->*fName)();
00FDD66F  mov         esi,esp 
; 是否是條件反射了,將this指針地址放到ecx中
00FDD671  mov         ecx,dword ptr [animal]  
00FDD674  call        dword ptr [fName]  

function_semantic::Animal::`vcall'{4}':
00FD1802  jmp         function_semantic::Animal::`vcall'{4}' (0FD73BCh) 

; 拿到虛表首地址
00FD73BC  mov         eax,dword ptr [ecx]  
; 偏移地址,找到正確的虛函數地址
00FD73BE  jmp         dword ptr [eax+4] 

function_semantic::Cat::Name:
; 真正的虛函數地址
00FD1429  jmp         function_semantic::Cat::Name (0FD8FB0h)  
複製代碼

畫了一張圖解釋下:

image

首先,咱們不看藍色虛線的部分。此時並非直接找到虛函數地址,而是經過一箇中間層(黑色虛框部分)去找到。這種技術,在microsoft編譯器中被包裝了一個高大上的名詞叫作thunk。

咱們再看整張圖,你會發現和之前調用虛函數的方式(藍色虛線箭頭)相比,是否是就是多了一個thunk的調用過程。可是爲啥要多箇中間層,那不意味着效率又下降了?首先引入thunk是爲了尋找虛函數地址增長強大的靈活性。其次須要認可的是效率的確降低了,可是沒有降低的那麼厲害,這幾行代碼都是彙編級別的代碼,因此執行的效率仍是很高。 接下來我詳細解釋下是如何增長靈活性,仔細觀察上面的黃色高亮的代碼塊,爲了方便查看我摘抄下來。第一次看到下面的代碼,總以爲很是的彆扭。聲明的類型是父類的成員函數指針,最後調用的倒是子類重寫的虛函數打印的結果分別是Cat Cat,Dog Dog,非常神奇,並且fName是個變量在這個過程是不變化的,這是怎麼作到的。這背後就是thunk的功勞了。

Animal *animal = new Cat();
void (Animal::*fName)() = &Animal::Name;

(animal->*fName)();
// Cat Cat

animal = new Dog();
(animal ->*fName)();
// Dog Dog
複製代碼

那麼thunk到底什麼?

從彙編層看,thunk就是那麼幾行代碼。幹了一件很簡單的事,就是根據傳遞過來的ecx指針,找到虛表地址,在根據偏移量(這裏偏移爲4byte)找到正確的虛函數地址。因此ecx裏面就是保存了對象的首地址(也就是包括了vptr),根據不一樣的虛表就能找到不一樣的虛函數。

4.3.2 多繼承模型下調用

class Fly {
public:
    virtual ~Fly() { cout << "~Fly" << endl; }
    virtual void CanFly() { cout << "Fly" << endl; }
    void Distance() { cout << "Fly distance" << endl; }
};

class Fish : public Animal, public Fly
{
public:
    virtual ~Fish() { cout << "~Fish" << endl; }
    virtual void Name() { cout << "Fish" << endl; }
    virtual void CanFly() { cout << "Fish Fly" << endl; }
};

void test_mult_inherit_vir_fun_pointer() {
    void (Animal::*fName)() = &Animal::Name;
    
    void (Fly::*fFly)() = &Fly::CanFly;
    Fish *fish = new Fish();
    Fly *fishfly = fish; 
    (fishfly->*fFly)();

    Animal *animal = fish;
    (animal->*fName)();
}
複製代碼

這裏從反彙編的角度看,在單繼承下面都是調用thunk方法,和上面的沒啥區別。

4.3.3 虛擬繼承模型下調用

提早預警,這裏的模型更復雜了,你們必定要耐心看下去。

class Animal {
public:
    virtual ~Animal() { cout << "~Animal" << endl; }
    virtual void Name() { cout << "Animal" << endl; }
    void Size() { cout << "Animal Size" << endl; }
};

class BigTiger: public virtual Animal
{
public:
    virtual ~BigTiger() { cout << "~Big Tiger" << endl; }
    virtual void Name() { cout << "Big Tiger" << endl; }
};

class FatTiger: public virtual Animal
{
public:
    virtual ~FatTiger() { cout << "~Fat Tiger" << endl; }
    virtual void Name() { cout << "Fat Tiger" << endl; }
};

class Tiger: public BigTiger, public FatTiger
{
public:
    virtual ~Tiger() { cout << "~Tiger" << endl; }
    virtual void Name() { cout << "Tiger" << endl; }
    virtual void CanFly() { cout << "Tiger Fly" << endl; }
};

void test_virtual_mult_inherit_vir_fun_pointer() {
// 1.測試代碼
    // void (Animal::*fName)() = &Animal::Name;
    // 下面這句和上面註釋的一句是等價的
    void (BigTiger::*fName)() = &Animal::Name;
    Tiger *temptiger2   = new Tiger();
    BigTiger *bigtiger   = temptiger2;
(bigtiger->*fName)();
// 打印出:Tiger

// 2.測試代碼
    // void (FatTiger::*fFatName)() = &Animal::Name;
    // 下面這句和上面註釋的一句是等價的
    void (FatTiger::*fFatName)() = &FatTiger::Name;
    Tiger *temptiger = new Tiger();
    FatTiger *fattiger = temptiger;
(fattiger->*fFatName)();
// 打印出:Tiger
 }

int main() {
   test_virtual_mult_inherit_vir_fun_pointer();
}
複製代碼

上述的測試代碼,咱們先看看Tiger的內存佈局是什麼樣的。

把代碼copy拿出來保存爲main.cpp,在vs2013命令行工具中,cd到main.cpp所在的目錄,運行指令cl /d1 reportSingleClassLayoutTiger main.cpp。打印出以下內容:

image

image

增長了虛繼承以後,內存的模型複雜度立立刻升了一個檔次。上述表格看的不明顯,我花了幾張圖方便你們觀看。

image

image

image

好了,咱們畫圖Tiger相關的內存模型圖。接下來咱們看看這是指向虛成員函數指針是如何實現的。

咱們看下面這段代碼的執行流程。

// 1.測試代碼
// void (Animal::*fName)() = &Animal::Name;
// 下面這句和上面註釋的一句是等價的
void (BigTiger::*fName)() = &Animal::Name;
Tiger *temptiger2   = new Tiger();
BigTiger *bigtiger   = temptiger2;
(bigtiger->*fName)();
複製代碼

image

你們可能在第二步調整this指針的時候會很奇怪的,可是根據我debug模式下跟下來,vtordisp for vbase Animal 這個位置的值爲0。

image

那麼ecx = ecx-[ecx-4]等價於ecx=ecx-0仍是等於ecx自己,ecx裏面就保存了this指針的地址,最後再調用虛函數。這裏我也很好奇爲何這裏還有個調整this地址的問題。 還有個關於vtordisp的,我也沒有理解,從調試的過程看下來,就知道他參與了最後一次的this指針調整。這裏我貼出網上的一個地址討論這個的

www.cnblogs.com/fanzhidongy…

那麼上面的調用過程就是:

1.根據thunk找到正確的虛函數地址。
2.調整this指針的偏移,再調用第一步找到的虛函數地址。

4.4 成員函數指針總結

成員函數指針有兩種形態:

1.和C語言中同樣的函數指針。
2.thunk vcall的技術,就是幾行彙編代碼:

1.以調整this的地址。
2.能夠協助找到真正的虛函數地址。

不知道你們有沒有感受,這個thunk很是像橋接模式的思路,將橋的兩邊變化都隔離開,就是解耦,各自能夠隨意變化。

你們可能對學習了這節的成員函數指針以爲沒啥用處,其實這節的用處可大了。想一想C++11中的functional,bind是怎麼實現的。後面有機會的話經過functional重寫觀察者設計模式,讓你感嘆這個的強大。

同時這裏面還有其餘的模式組合(好比:虛繼承普通成員函數),我這裏就沒有一一的探討了,但願讀者對本身感興趣的部分動手實踐,或者和我討論也能夠。

最後咱們在比較下各類函數指針的效率如何:

image

5.inline函數調用

inline函數調用的過程當中,須要注意兩點:

5.1函數參數列表:

inline int max(int left, int right) {
    return left > right ? left : right;
}
複製代碼
// 調用方式
max(foo(), bar()+1)
複製代碼
// inline 被擴展以後
int t1;
int t2;
maxvale = (t1=foo()),(t2=bar()+1), t1 > t2 ? t1 : t2;
複製代碼

這樣作的話,其實會形成大量的臨時對象構造。若是你的對象須要大量的初始化操做,會帶來效率問題。

5.2局部變量

inline int max(int left, int right) {
// 添加臨時變量max_value
    int max_value = left > right ? left : right;
    return max_value;
}
複製代碼
{
    // 調用方式
    int local_var;
    int maxval;
    maxval = max(left, right);
}
複製代碼
// inline 被擴展以後
// max裏面的max_value會被mangling,如今假設爲__max_maxval
int __max_maxval;
maxval = (__max_maxval = left > right ? left : right), __max_maxval;
複製代碼

在inline函數中增長了臨時變量,看到最後inline展開的時候也會臨時對象的構造,就和上面的影響是同樣的,形成的效率損失。

6.總結:

image
相關文章
相關標籤/搜索