讀者若是以爲我文章還不錯的,但願能夠多多支持下我,文章能夠轉發,可是必須保留原出處和原做者署名。更多內容請關注個人微信公衆號:cpp手藝人。 linux
這個章節咱們主要學習如下幾個知識點:ios
1.數據成員綁定時機。git
2.多種模型下數據成員佈局。windows
3.數據成員如何讀取的。微信
4.進程內存佈局函數
你們一看標題可能有點懵了,什麼叫數據成員的綁定時機。請隨我看段代碼,這段代碼節選自《深刻探索C++對象模型》工具
extern float x;
class Point3D {
public:
Point3D(float, float, float);
float X() { return x; }
void X(float new_x) { x = new_x; }
private:
float x, y, z;
};
複製代碼
若是我調用了Point3D的X()返回的這個x是Point3D的成員變量值,仍是外部定義的x。如今看來的話,很顯然返回的Point3D的成員變量值。可是在C++誕生沒多久的時代,編譯器返回的是外部定義的x。這樣的結果對於如今的你來講是否是有點驚訝。因此你如今看到以下的代碼風格,請不要驚訝也不要彷徨。注意下面的高亮部分,將成員變量的定義提早,這樣就防護了以前的成員綁定問題。佈局
extern float x;
class Point3D {
private:
float x, y, z;
public:
Point3D(float, float, float);
float X() { return x; }
void X(float new_x) { x = new_x; }
};
複製代碼
能夠看出來現代C++編譯器,在綁定成員變量時,是延遲到整個類解析完以後纔會進行成員變量的綁定。 可是你們請注意,這裏有一個小坑。就是成員函數的參數列表綁定時機。 看以下的代碼:學習
#include <string>
using std::string;
typedef string mytype;
class TestDataAnalyse {
public:
int Fun();
// 對於成員函數參數的解析,編譯器是第一次遇到這個類型的mytype類型的時候被決定的
// mytype第一次遇到的時候就看到了type string mytype
void CopyValue(mytype value);
private:
int tempvalue;
typedef int mytype;
};
void TestDataAnalyse::CopyValue(mytype value)
{
tempvalue = value;
}
void test_data_analyse() {
TestDataAnalyse data;
data.Fun();
}
複製代碼
編譯器報錯的錯誤: error C2511: 「void TestDataAnalyse::CopyValue(TestDataAnalyse::mytype)」:「TestDataAnalyse」中沒有找到重載的成員函數看到這樣的錯誤,你可能找半天都找不到頭緒。mytype我已經定義爲int類型的,可是爲何說沒有找到重載的成員函數,這和重載函數有什麼關係。測試
緣由就是:成員函數參數的綁定是利用就近原理,當解析到它的時候,它的定義就是最近的定義這個類型的類型。什麼意思呢,就是mytype這個類型最近定義這個類型的爲string類型(從上往下解析)。CopyValue中將string類型複製給int類型,固然是編譯失敗。這個時候咱們把類中嵌套的定義typedef int mytype,這句話放在類的開始處,就能夠避免這個問題,這個時候mytype的類型就爲int。
在這裏能夠看出成員變量和成員函數參數的綁定時機是不一樣的。
class Child {
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_a = 0x010FFD14
// m_b = 0x010FFD18
// m_c = 0x010FFD1C
// m_d = 0x010FFD20
// m_e = 0x010FFD24
}
複製代碼
未繼承任何的父類的類,類的成員變量都是按照聲明的順序排列的。根據打印出的地址,以下圖所示:
class Base {
public:
int m_base_a;
int m_base_b;
};
class Child : public Base
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_base_a = 0x%p\n", &child.m_base_a);
printf("m_basse_b = 0x%p\n", &child.m_base_b);
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_base_a = 0x00B3FCDC
// m_basse_b = 0x00B3FCE0
// m_a = 0x00B3FCE4
// m_b = 0x00B3FCE8
// m_c = 0x00B3FCEC
// m_d = 0x00B3FCF0
// m_e = 0x00B3FCF4
}
複製代碼
單繼承無虛函數,先是父類聲明的前後順序,再按照子類的聲明的前後順序,根據打印出的地址,以下圖所示:
class Base3 {
public:
int m_base3_a;
int m_base3_b;
};
class Child : public Base, public Base3
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_layout() {
Child child;
printf("m_base_a = 0x%p\n", &child.m_base_a);
printf("m_basse_b = 0x%p\n", &child.m_base_b);
printf("m_base3_a = 0x%p\n", &child.m_base3_a);
printf("m_basse3_b = 0x%p\n", &child.m_base3_b);
printf("m_a = 0x%p\n", &child.m_a);
printf("m_b = 0x%p\n", &child.m_b);
printf("m_c = 0x%p\n", &child.m_c);
printf("m_d = 0x%p\n", &child.m_d);
printf("m_e = 0x%p\n", &child.m_e);
// m_base_a = 0x010FF6B0
// m_basse_b = 0x010FF6B4
// m_base3_a = 0x010FF6B8
// m_basse3_b = 0x010FF6BC
// m_a = 0x010FF6C0
// m_b = 0x010FF6C4
// m_c = 0x010FF6C8
// m_d = 0x010FF6CC
// m_e = 0x010FF6D0
}
複製代碼
當類中出現多個繼承父類,成員變量的排列順序,按照繼承的前後順序排列。若下圖所示:
class Base {
public:
int m_base_a;
int m_base_b;
virtual ~Base() {}
};
class Base3 {
public:
int m_base3_a;
int m_base3_b;
// virtual ~Base3() {}
};
class Child : public Base, public Base3
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
int main() {
return 0;
}
複製代碼
將上面的代碼保存爲member_layout.cpp,咱們利用在第四節文章裏面的用到的工具,在windows菜單找到vs2013 開發人員命令提示工具,雙擊進入命令行的界面。
運行cl /d1 reportSingleClassLayoutChild member_layout.cpp,在不改變繼承的順序下,手動的添加和刪除虛析構函數,導出的結果以下:
從表對比能夠看出,繼承的父類有沒有虛函數是會影響子類的成員的佈局。
1.子類繼承了多個父類,其中有多個父類有虛函數,會優先排列有虛函數的父類而且按照繼承的前後順序排列,其次再排序無虛函數的父類。
2.若是繼承的父類都是有虛函數或者是都沒有虛函數,那麼都按照繼承的前後順序排列。
將以下代碼保存爲member_layout.cpp,注意在這裏爲了探究內存佈局,我刪除了父類的全部虛函數,這樣的設計不合理的,你們請注意。
class Grand {
public:
int G1;
int G11;
};
class Parent1 : virtual public Grand
{
public:
int P1;
};
class Parent2: virtual public Grand
{
public:
int P2;
};
class Child3: public Parent1, public Parent2
{
public:
int C3;
};
int main() {
return 0;
}
複製代碼
使用vs2013開發人員命令提示工具,定位你本身的member_layout.cpp目錄,好比個人:J:\code\code_git\ \polymorphism_virtual\source,輸入命令:cl /d1 reportSingleClassLayoutChild3 member_layout.cpp,Child3表示你要導出的類佈局。
我截取重要的內容貼出來:
從表中咱們能夠清晰的看出來,Child3虛繼承以後的佈局。從Child3角度每一個父類都會帶一個vbptr(虛基類表指針),它指向一個虛基類表。咱們先畫出內存結構圖的 虛基類表中包含兩項,第一項咱們先跳過不解釋,第二項是什麼意思呢? 咱們來寫段測試代碼看看:void test_virtual_base_table() {
Child3 c3;
c3.G1 = 0;
c3.G11 = 11;
c3.P1 = 1;
c3.P2 = 2;
c3.C3 = 3;
}
複製代碼
咱們反彙編看下:
00C1DF48 push 1
00C1DF4A lea ecx,[c3] ; 虛基類表指針複製給ecx
00C1DF4D call data_semantics::Child3::Child3 (0C11758h)
c3.G1 = 0;
00C1DF52 mov eax,dword ptr [c3] ; 經過基類表指針-->找到虛基類表首地址
c3.G1 = 0;
00C1DF55 mov ecx,dword ptr [eax+4] ; 跳過虛基類表的前4個字節,找到第二項
00C1DF58 mov dword ptr c3[ecx],0 ; c3+[ecx]偏移,複製給G1所在內存地址爲0
c3.G11 = 11;
00C1DF60 mov eax,dword ptr [c3] ; 經過基類表指針-->找到虛基類表首地址
00C1DF63 mov ecx,dword ptr [eax+4] ; 跳過虛基類表的前4個字節,找到第二項
00C1DF66 mov dword ptr [ebp+ecx-20h],0Bh ; 複製給G1所在內存地址爲0BH,ebp-20h其實爲P1的地址
c3.P1 = 1;
00C1DF6E mov dword ptr [ebp-20h],1
c3.P2 = 2;
00C1DF75 mov dword ptr [ebp-18h],2
c3.C3 = 3;
00C1DF7C mov dword ptr [ebp-14h],3
複製代碼
若是你閱讀過《第四章 虛函數的原理》,你會發現這個對虛函數的尋址機制是徹底相同的。 都是根據指針->虛表->虛表中的內容,看下圖的成員變量的偏移量值。
咱們看下Child3中G1是怎麼尋址的,看下圖所示。 因此能夠得出結論,子類虛基類表中的第二項內容其實存儲是個偏移量。虛基類中的內容是放到子類的最後面,而後子類根據虛基類表中的偏移找到虛基類成員的位置。一樣的道理G11也是按照這種原理尋址的。須要注意下mov dword ptr [ebp+ecx-20h],ebp-20h其實P1的首地址,因此從P1的地址開始(跳過前面的vbptr 4個字節)+20正好就是G11的地址。
Child3中的Parent1,Parent2中都包含一個vbptr。這和vptr是十分相似的原理,懂了虛函數的原理,那麼你搞懂虛基類表也是很是容易的。
在2.4中爲了探究虛繼承的內存佈局,我把Grand類中的虛析構函數刪除。這樣的設計是不合理的,這裏咱們給Grand加上虛析構函數。關於爲何父類中必定要虛析構函數能夠參考《第四章 虛函數原理》。
class Grand {
public:
int G1;
int G11;
virtual ~Grand() {}
};
複製代碼
咱們再使用工具導出類佈局
能夠看出Grand中增長一個虛函數,在子類中Child3中的佈局中增長了一個vfptr指針,這個指針指向一個表,表中就存儲了虛函數的地址,其實就是咱們說的虛函數表,vfptr就是至關於咱們以前討論的vptr。
還剩下的幾種狀況虛繼承+虛函數的情形,都是相同的分析機制。這裏我就再也不分析了。
前面咱們詳細的分析了各類模式下,類中內存中的佈局。既然咱們知道了數據在哪裏,那麼接下來討論數據是怎麼讀取的。
其實,咱們已經知道數據在內存的位置,將會很是有利於咱們理解對於成員變量的讀取,甚至能夠說咱們已經理解了一半。
static成員是屬於整個類的,並不單獨屬於某一個實例化的對象。咱們接着借用上面的代碼Child3類,在裏面增長几行代碼。
class Child3: public Parent1, public Parent2
{
public:
int C3;
static int s_Age;
};
int Child3::s_Age = -1;
void test_call_member() {
Child3::s_Age = 0;
Child3 c3;
c3.s_Age = 1;
Child3 *pc3 = new Child3();
pc3->s_Age = 2;
}
複製代碼
接下來咱們看下test_call_member反彙編的調用方式有什麼不一樣。
Child3::s_Age = 0;
0092ADC0 mov dword ptr ds:[93B000h],0
Child3 c3;
0092ADCA push 1
0092ADCC lea ecx,[c3]
0092ADCF call data_semantics::Child3::Child3 (09217A8h)
0092ADD4 mov dword ptr [ebp-4],0
c3.s_Age = 1;
0092ADDB mov dword ptr ds:[93B000h],1
Child3 *pc3 = new Child3();
0092AE26 mov dword ptr ds:[93B000h],2
複製代碼
從上述代碼中咱們看出,若是是靜態成員,無論你的調用方式如何。其實他們最後的彙編代碼都是同樣的。從側面能夠看出靜態數據是放在全局的數據區,和對象是沒有關係的。
前面咱們學習了static成員的是怎麼找到,可是和實例對象相關的成員咱們該怎麼定位呢。
咱們看下面的代碼:
class Child4 {
public:
int m1;
int m2;
int m3;
int m4;
};
void test_member_initialize() {
Child4 *c5 = new Child4();
c5->m1 = 0;ss
c5->m2 = 1;
c5->m3 = 2;
c5->m4 = 3;
Child4 c4;
c4.m1 = 0;
c4.m2 = 1;
c4.m3 = 2;
c4.m4 = 3;
}
複製代碼
接下來咱們經過反彙編代碼看下是如何定位成員變量。
; 第一段
c5->m1 = 0;
011AC94F mov eax,dword ptr [c5]
011AC952 mov dword ptr [eax],0
c5->m2 = 1;
011AC958 mov eax,dword ptr [c5]
011AC95B mov dword ptr [eax+4],1
c5->m3 = 2;
011AC962 mov eax,dword ptr [c5]
011AC965 mov dword ptr [eax+8],2
c5->m4 = 3;
011AC96C mov eax,dword ptr [c5]
011AC96F mov dword ptr [eax+0Ch],3
; 第二段
Child4 c4;
c4.m1 = 0;
011AC976 mov dword ptr [c4],0
c4.m2 = 1;
011AC97D mov dword ptr [ebp-1Ch],1
c4.m3 = 2;
011AC984 mov dword ptr [ebp-18h],2
c4.m4 = 3;
011AC98B mov dword ptr [ebp-14h],3
複製代碼
咱們先看第一段彙編代碼,首先[c5]中保存對象的首地址給eax,對eax解引用而後賦值0。再對(eax+4)解引用賦值1,再對(eax+8)解引用賦值2,再對(eax+12)解引用賦值3。我就能夠看出這裏根據對象的首地址+成員變量的偏移,就能找到對應的對象而後進行讀取操做。
這時咱們再看第二段的彙編代碼,你們可能就奇怪了,爲何第二種的定位方式第一種不一樣呢。
其實這裏定位的原理是一致的。第一種方式,c5對象是new出來,也就是說它的內存是堆中的,堆區是個很是靈活的區域申請和釋放都是本身能夠控制的,同時它的增加的方向是從低地址->高地址。而第二種方式呢,c4是在棧中申請的內存,相比較堆呢,它的可控性就弱了不少,並且它的增加方向是從高地址->低地址,這一點和堆是徹底相反的,記住ebp表示的基址指針寄存器,該指針指向系統棧最上面一個棧幀的底部。
咱們作個表格對比下兩種:
採用的原理都是同樣的,都是利用成員變量的偏移量,只不過它們的增加的方式不同。堆往上增加,棧是往下增加。
class Base6 {
public:
int m_base6_a;
virtual ~Base6() {}
static int s_base6_b;
};
int Base6::s_base6_b = 0;
class Base4 : virtual public Base6
{
public:
int m_base4_a;
int m_base4_b;
static int s_base4_c;
virtual ~Base4() {}
};
int Base4::s_base4_c = 0;
class Base5 : virtual public Base6
{
public:
int m_base5_a;
int m_base5_b;
static int s_base5_c;
virtual ~Base5() {}
};
int Base5::s_base5_c = 0;
class Child5 : public Base4, public Base5
{
public:
int m_a;
int m_b;
int m_c;
int m_d;
int m_e;
};
void test_member_effective() {
Child5::s_base6_b = 10;
Child5::s_base4_c = 1;
Child5::s_base5_c = 2;
Child5 c5;
c5.m_a = 3;
c5.m_base4_a = 4;
c5.m_base5_a = 5;
c5.m_base6_a = 6;
}
複製代碼
咱們從彙編的角度(debug 模式下)看下他們的效率。
指向類的成員變量指針,其實不是指針,實際的內容實際上是在整個類中的偏移值。舉個例子:
class Class7 {
public:
virtual void VirFunc() {}
int m_a;
int m_b;
int m_c;
int m_d;
};
void test_member_point() {
printf("&Class7::m_a:%x\n", &Class7::m_a);
printf("&Class7::m_b:%x\n", &Class7::m_b);
printf("&Class7::m_c:%x\n", &Class7::m_c);
printf("&Class7::m_d:%x\n", &Class7::m_d);
// &Class7::m_a:4
// &Class7::m_b:8
// &Class7::m_c:c
// &Class7::m_d:10
// 用法
int Class7::*cpoint = &Class7::m_a;
Class7 c7;
c7.*cpoint = 1;
// *(&c7+cpoint) = 1;
}
複製代碼
從代碼中能夠看出來,打印的是成員變量的偏移值。值的注意下就是這裏打印的偏移值是從4開始的,由於Class7含有虛函數,存在一個虛表指針。
咱們再看下成員指針的初始化和使用,int Class7::*cpoint = &Class7::m_a; Class7 c7; c7.*cpoint = 1;用法其實很簡單,可是困擾個人是這個成員變量指針到底有啥用。我嘗試google下也沒有發現有價值的資料,從《深刻探索C++對象模型》裏面提到了這個能夠幫咱們看出成員變量的偏移位置,可是我以爲這個意義不大的。這個讓我很困惑。
變量在內存是如何分佈的,咱們藉助linux上nm(names)命令看下,代碼以下
#include <stdio.h>
#include <iostream>
using std::cout;
using std::endl;
int g1;
int g2;
int g3 = 3;
int g4 = 4;
int g5;
int g6 = 1;
static int gs_7;
static int gs_8 = 0;
static int gs_9 = 1;
void g_func() {
return;
}
class MyClass {
public:
int m_i;
static int m_si;
int m_j;
// 聲明
static int m_sj;
int m_k;
static int m_sk;
};
// 定義
int MyClass::m_sj = 1;
int main(int argc ,char *argv[]) {
int temp_i = 0;
printf("tempi address = %p\n", &temp_i);
printf("g1 address = %p\n", &g1);
printf("g2 address = %p\n", &g2);
printf("g3 address = %p\n", &g3);
printf("g4 address = %p\n", &g4);
printf("g5 address = %p\n", &g5);
printf("g6 address = %p\n", &g6);
printf("gs_7 address = %p\n", &gs_7);
printf("gs_8 address = %p\n", &gs_8);
printf("gs_9 address = %p\n", &gs_9);
printf("MyClass::m_sj address = %p\n", &(MyClass::m_sj));
printf("g_func() address = %p\n", g_func);
printf("main() address = %p\n", main);
cout << "g_test " << (void *)g_test << endl;
return 0;
}
複製代碼
1.使用g++ process_member_layout -o process_memeber_layout
2.在使用nm process_memeber_layout導出的數據以下所示。
這裏解釋下nm中間字段的含義
咱們再對比下nm導出的數據和上述代碼的打印的地址
我從網上找了張內存映射圖