C++幕後故事(五)--數據你在哪裏?

讀者若是以爲我文章還不錯的,但願能夠多多支持下我,文章能夠轉發,可是必須保留原出處和原做者署名。更多內容請關注個人微信公衆號:cpp手藝人linux

這個章節咱們主要學習如下幾個知識點:ios

1.數據成員綁定時機。git

2.多種模型下數據成員佈局。windows

3.數據成員如何讀取的。微信

4.進程內存佈局函數

1.數據成員綁定時機

你們一看標題可能有點懵了,什麼叫數據成員的綁定時機。請隨我看段代碼,這段代碼節選自《深刻探索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。

在這裏能夠看出成員變量和成員函數參數的綁定時機是不一樣的。

2.數據成員佈局

2.1未繼承任何父類

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
}
複製代碼

未繼承任何的父類的類,類的成員變量都是按照聲明的順序排列的。根據打印出的地址,以下圖所示:

在這裏插入圖片描述

2.2單繼承無虛函數父類

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
}
複製代碼

單繼承無虛函數,先是父類聲明的前後順序,再按照子類的聲明的前後順序,根據打印出的地址,以下圖所示:

在這裏插入圖片描述

2.3 多重繼承父類

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
}
複製代碼

當類中出現多個繼承父類,成員變量的排列順序,按照繼承的前後順序排列。若下圖所示:

在這裏插入圖片描述

2.4 多重繼承父類+(單/雙)虛函數

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.若是繼承的父類都是有虛函數或者是都沒有虛函數,那麼都按照繼承的前後順序排列。

在這裏插入圖片描述
在這裏插入圖片描述

2.5 虛基類繼承無虛函數

將以下代碼保存爲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.6 虛繼承+虛函數繼承

在2.4中爲了探究虛繼承的內存佈局,我把Grand類中的虛析構函數刪除。這樣的設計是不合理的,這裏咱們給Grand加上虛析構函數。關於爲何父類中必定要虛析構函數能夠參考《第四章 虛函數原理》。

class Grand {
public:
    int G1;
    int G11;
    virtual ~Grand() {}
};
複製代碼

咱們再使用工具導出類佈局

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

能夠看出Grand中增長一個虛函數,在子類中Child3中的佈局中增長了一個vfptr指針,這個指針指向一個表,表中就存儲了虛函數的地址,其實就是咱們說的虛函數表,vfptr就是至關於咱們以前討論的vptr。

還剩下的幾種狀況虛繼承+虛函數的情形,都是相同的分析機制。這裏我就再也不分析了。

3.數據成員的讀取

前面咱們詳細的分析了各類模式下,類中內存中的佈局。既然咱們知道了數據在哪裏,那麼接下來討論數據是怎麼讀取的。

其實,咱們已經知道數據在內存的位置,將會很是有利於咱們理解對於成員變量的讀取,甚至能夠說咱們已經理解了一半。

3.1 數據成員

3.1.1 static成員

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  
複製代碼

從上述代碼中咱們看出,若是是靜態成員,無論你的調用方式如何。其實他們最後的彙編代碼都是同樣的。從側面能夠看出靜態數據是放在全局的數據區,和對象是沒有關係的。

3.1.2 no static成員

前面咱們學習了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表示的基址指針寄存器,該指針指向系統棧最上面一個棧幀的底部。

咱們作個表格對比下兩種:

在這裏插入圖片描述

採用的原理都是同樣的,都是利用成員變量的偏移量,只不過它們的增加的方式不同。堆往上增加,棧是往下增加。

3.2讀取的效率

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 模式下)看下他們的效率。

在這裏插入圖片描述

3.3 Data Member指針

指向類的成員變量指針,其實不是指針,實際的內容實際上是在整個類中的偏移值。舉個例子:

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++對象模型》裏面提到了這個能夠幫咱們看出成員變量的偏移位置,可是我以爲這個意義不大的。這個讓我很困惑。

4.進程內存佈局

變量在內存是如何分佈的,咱們藉助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導出的數據和上述代碼的打印的地址

在這裏插入圖片描述

我從網上找了張內存映射圖

在這裏插入圖片描述

5.總結

在這裏插入圖片描述
相關文章
相關標籤/搜索