這個章節咱們會學到如下3個知識點:html
1.不一樣的類型函數是怎麼調用的。
2.成員函數指針各個模型實現的原理是什麼以及各個指針的效率如何。
3.inline函數的注意事項。
linux
看下面的代碼,這裏面我分別調用了類成員和全局函數設計模式
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。
測試
關於虛函數在第四章作了專門的介紹,這裏就不在囉嗦了。ui
從字面意思看,有兩點內容。this
1.是個指針
2.指向的類的成員函數
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指針當作參數傳遞進去。這個就和普通成員函數調用的原理是一致的。
void test_function_pointer() {
void (*fName)() = &Car::Name;
fName();
}
複製代碼
注意到沒有咱們在定義靜態成員函數時,沒有加上類名Car。這是由於靜態函數裏面沒有this指針,因此就形成了不須要加上類名Car,同時也形成靜態成員函數不能直接使用類的普通成員變量和函數。你可能發現類的靜態成員函數,和全局函數很是相似,其實本質上是同樣的,都是一個可調用的地址。
上面的兩節,咱們看了普通成員函數指針和靜態成員函數指針,以爲比較簡單。接下來的重頭戲虛擬成員函數指針,這裏的套路更深也更難理解,且聽我一步步道來。
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變量上就會顯示一串信息。
顯示的什麼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)
複製代碼
畫了一張圖解釋下:
首先,咱們不看藍色虛線的部分。此時並非直接找到虛函數地址,而是經過一箇中間層(黑色虛框部分)去找到。這種技術,在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),根據不一樣的虛表就能找到不一樣的虛函數。
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方法,和上面的沒啥區別。
提早預警,這裏的模型更復雜了,你們必定要耐心看下去。
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。打印出以下內容:
增長了虛繼承以後,內存的模型複雜度立立刻升了一個檔次。上述表格看的不明顯,我花了幾張圖方便你們觀看。
好了,咱們畫圖Tiger相關的內存模型圖。接下來咱們看看這是指向虛成員函數指針是如何實現的。
咱們看下面這段代碼的執行流程。
// 1.測試代碼
// void (Animal::*fName)() = &Animal::Name;
// 下面這句和上面註釋的一句是等價的
void (BigTiger::*fName)() = &Animal::Name;
Tiger *temptiger2 = new Tiger();
BigTiger *bigtiger = temptiger2;
(bigtiger->*fName)();
複製代碼
你們可能在第二步調整this指針的時候會很奇怪的,可是根據我debug模式下跟下來,vtordisp for vbase Animal 這個位置的值爲0。
那麼ecx = ecx-[ecx-4]等價於ecx=ecx-0仍是等於ecx自己,ecx裏面就保存了this指針的地址,最後再調用虛函數。這裏我也很好奇爲何這裏還有個調整this地址的問題。 還有個關於vtordisp的,我也沒有理解,從調試的過程看下來,就知道他參與了最後一次的this指針調整。這裏我貼出網上的一個地址討論這個的
那麼上面的調用過程就是:
1.根據thunk找到正確的虛函數地址。
2.調整this指針的偏移,再調用第一步找到的虛函數地址。
成員函數指針有兩種形態:
1.和C語言中同樣的函數指針。
2.thunk vcall的技術,就是幾行彙編代碼:1.以調整this的地址。
2.能夠協助找到真正的虛函數地址。
不知道你們有沒有感受,這個thunk很是像橋接模式的思路,將橋的兩邊變化都隔離開,就是解耦,各自能夠隨意變化。
你們可能對學習了這節的成員函數指針以爲沒啥用處,其實這節的用處可大了。想一想C++11中的functional,bind是怎麼實現的。後面有機會的話經過functional重寫觀察者設計模式,讓你感嘆這個的強大。
同時這裏面還有其餘的模式組合(好比:虛繼承普通成員函數),我這裏就沒有一一的探討了,但願讀者對本身感興趣的部分動手實踐,或者和我討論也能夠。
最後咱們在比較下各類函數指針的效率如何:
inline函數調用的過程當中,須要注意兩點:
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;
複製代碼
這樣作的話,其實會形成大量的臨時對象構造。若是你的對象須要大量的初始化操做,會帶來效率問題。
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展開的時候也會臨時對象的構造,就和上面的影響是同樣的,形成的效率損失。