寫在前面
由於此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
類與結構體的關係
它們兩個的定義我就不在囉嗦了。在C語言中,類和結構體是一個東西,只是用的關鍵字不一樣罷了。不信咱們做一個實驗,看看編譯會不會報錯:
#include <iostream>
struct MyStruct
{
public:
MyStruct();
~MyStruct();
private:
};
class MyClass
{
public:
MyClass();
~MyClass();
private:
};
MyClass::MyClass()
{
}
MyClass::~MyClass()
{
}
MyStruct::MyStruct()
{
}
MyStruct::~MyStruct()
{
}
int main()
{
system("pause");
return 0;
}
結果編譯順利通過。如果還想繼續做深入的實驗,請自行研究。下面我們來介紹它們的本質。
彙編看類和結構體
類和結構體雖然沒有任何區別,但 通常會把只有資料的稱之為結構體,還有功能函式的稱之為類 。這句話我曾在(二)羽夏看C語言——容器 說明過。在此文章,我一般將用class
關鍵字稱之為類,用struct
關鍵字稱之為結構體,但腦子裡面一定要清楚,C語言中的結構體和類是一個東西。我們將從一下方面對類和結構體進行探討:
類的例項化
我們將用以下程式碼進行探討此問題:
#include <iostream>
class MyClass
{
public:
MyClass();
~MyClass();
int pa = 5;
private:
int a;
};
MyClass::MyClass()
{
a = 10;
}
MyClass::~MyClass()
{
}
int main()
{
MyClass cls;
system("pause");
return 0;
}
以下是反彙編結果,讓我們逐個分析類被例項化的過程:
如上圖所示,lea ecx,[ebp-10h]
就是取該類的指標,即為this
。這就是為什麼編譯器在寫類可以用this
的原因。下一個call
呼叫即為呼叫該類的建構函式。
上面的圖是call
呼叫後到的第一個程式碼塊,可以說明,當一個類例項化時,會先呼叫它的建構函式。
根據彙編可知,呼叫建構函式的時候,先初始化變數,然後繼續呼叫建構函式裡面的內容,繼而完成整個類的例項化。
類中有靜態變數或函式
我們將用以下程式碼進行實驗:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
//static int b;
//void test();
private:
int a = 6;
};
//int MyClass::b = 10;
//void MyClass::test()
//{
// cout << "test" << endl;
//}
int main()
{
MyClass cls;
cout << sizeof(MyClass) << endl;
//int tmp = cls.b;
//cls.test();
system("pause");
return 0;
}
一看就能明白,以上程式碼使用來檢視類大小的,我們可以用這種方式來判斷這個東西真正屬於不屬於類。執行後,結果如下:
8
請按任意鍵繼續. . .
然後,我們把b
的宣告和初始化以及呼叫去掉註釋,然後再執行一下,發現結果仍和上面的結果一樣。我們再看一下它的反彙編,跟到類例項化函式體內:
咦,咋找不到和b相關的任何東西呢,主函式也是沒有,在那個b
初始化處下斷點也下不住。那我們再看看區域性變數窗體裡看看有沒有與b
有關的訊息:
遺憾的是,偵錯程式裡面的區域性變數也不承認有b
這個東西。那好,我們唯一能做的是再看一下如何訪問這個b
的。
我們發現,b被翻譯成一個死地址,說明在類裡面宣告一個靜態變數和在類外面宣告一個靜態變數在彙編層面沒有任何區別,只是在C語言層面不同而已。
接下來看一下函式,我們重新把函式取消註釋。繼續做實驗,發現結果還是相同。然後我們看一下反彙編:
可以看到,函式同樣被翻譯成一個死地址,但在它之前還是將該類的this指標
傳遞給函式。如果將函式前面用static
修飾的話,看看反彙編會有什麼變化。
可以看到,函式直接被翻譯成一個死地址,但不會傳遞this指標
,這和在類外面宣告一個函式呼叫在彙編層面無異。
繼承
在類裡面十分重要的一個概念就是繼承。那麼繼承在彙編層面到底是什麼樣子呢?我們用以下程式碼進行驗證:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass建構函式被呼叫" << endl;;
}
private:
int a = 6;
};
class MyClassSub :public MyClass
{
public:
int pb = 15;
MyClassSub()
{
cout << "MyClassSub建構函式被呼叫" << endl;;
}
private:
int b = 16;
};
int main()
{
MyClassSub cls;
//int a = cls.pb;
//a = cls.pa;
system("pause");
return 0;
}
如下是輸出結果:
MyClass建構函式被呼叫
MyClassSub建構函式被呼叫
請按任意鍵繼續. . .
這個是我們從C語言層面對建構函式呼叫順序進行驗證,然後我們看一下反彙編:
根據反彙編,我們也同樣驗證此問題。然後我們再看一下變數會有什麼變化,先把被註釋掉的恢復進行驗證,把程式碼執行到建構函式剛好結束,然後在記憶體視窗輸入類的地址,可以得到如下結果:
由此可以看出,類的繼承是直接是把被繼承的類後面貼上子類的。那麼,如果子類有的變數父類也有呢?我們把int pb = 15
改為int pa = 15
,連同下面的程式碼改動,我們看一下結果。
可以看出,訪問pa
的時候直接訪問子類
的,而記憶體結構根本沒有發生任何變化。
我們最後再驗證最後一個問題:子類繼承預設訪問為私有的,如果我們把public
刪掉後會不會應該繼承後的記憶體結構呢?下一篇將揭曉答案。
虛表
我們從彙編層面觀察虛表是什麼,將用下面的彙編程式碼進行實驗:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass建構函式被呼叫" << endl;;
}
virtual void test();
private:
int a = 6;
};
void MyClass::test()
{
cout << "test" << endl;
}
class MyClassSub :MyClass
{
public:
int pa = 15;
MyClassSub()
{
cout << "MyClassSub建構函式被呼叫" << endl;;
}
void test();
private:
int b = 16;
};
void MyClassSub::test()
{
cout << "override test" << endl;
}
int main()
{
//請用指標例項化類,如果在堆疊例項化將會呼叫它的死地址
MyClassSub* cls = new MyClassSub();
cls->test();
system("pause");
return 0;
}
將會得到如下結果:
MyClass建構函式被呼叫
MyClassSub建構函式被呼叫
override test
請按任意鍵繼續. . .
在system("pause");
這行下斷點,然後執行。觀察區域性變數,看到如下圖:
__vfptr
就是虛表地址,有幾個虛擬函式就有幾個。如果被重寫,將會將虛表填充對應的地址。我們看看是如何呼叫該函式的。
通過彙編得出:通過虛表呼叫子類的test
函式。
拷貝建構函式
在C語言中,每個類都會自帶一個拷貝建構函式,我們看看拷貝建構函式為我們做了什麼,將用以下程式碼進行實驗:
#include <iostream>
using namespace std;
class MyClass
{
public:
int pa = 5;
MyClass()
{
cout << "MyClass建構函式被呼叫" << endl;;
}
private:
int a = 6;
};
int main()
{
MyClass cls;
MyClass* ci = new MyClass(cls);
system("pause");
return 0;
}
然後在合適的地方下個斷點,看反彙編:
從圖中可以看到new
和拷貝構造的過程,先呼叫new函式
申請8個位元組
的記憶體給類用,然後判斷有沒有成功,成功後把每個位元組對應複製到指定位置。
下一篇
(六)羽夏看C語言——函式