寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。本人非計算機專業,可能對本教程涉及的事物沒有了解的足夠深入,如有錯誤,歡迎批評指正。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 (一)羽夏看C語言——簡述 ,方便學習本教程。
C/C++結構體❓和類❗
C
沒有類這個東西,C++
有。C
有結構體但只能包含數據,不能有函數,但C++
可以。如果讀者只想了解C
的匯編底層的話,就不要繼續了,以免浪費時間。
類與結構體的關系
它們兩個的定義我就不在啰嗦了。在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個字節
的內存給類用,然后判斷有沒有成功,成功后把每個字節對應復制到指定位置。