找回密碼
 立即注冊(cè)

QQ登錄

只需一步,快速開始

搜索
查看: 1952|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

c++類對(duì)象內(nèi)存模型

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:107189 發(fā)表于 2016-3-5 17:43 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
c++類對(duì)象內(nèi)存模型是一個(gè)比較抓狂的問(wèn)題,主要是C++特性太多了,所以必須建立一個(gè)清晰的分析層次。一般而言,講到C++對(duì)象,都比較容易反應(yīng)到以下這個(gè)圖表:  
這篇文章,就以這個(gè)表格作為分析和行文的策略的縱向指導(dǎo);橫向上,兼以考慮無(wú)繼承、單繼承、多重繼承及虛擬繼承四方面情況,這樣一來(lái),思維層次應(yīng)該算是比較清晰了。
1、C++類數(shù)據(jù)成員的內(nèi)存模型
1.1 無(wú)繼承情況
實(shí)驗(yàn)最能說(shuō)明問(wèn)題了,首先考慮下面一個(gè)簡(jiǎn)單的程序1:

#include<iostream>

class memtest
{
public:
    memtest(int _a, double _b) : a(_a), b(_b) {}
    inline void print_addr(){
        std::cout<<"Address of a and b is:\n\t\t"<<&a<<"\n\t\t" <<&b<<"\n";
    }
    inline void print_sta_mem(){
        std::cout<<"Address of static member c is:\n\t\t"<<&c<<"\n";
    }

private:
    int a;
    double b;
    static int c;
};

int memtest::c = 8;

int main()
{
    memtest m(1,1.0);
    std::cout<<"Address of m is : \n\t\t"<< &m<<"\n";
    m.print_addr();
    m.print_sta_mem();
    return 0;
}
在GCC4.4.5下編譯,運(yùn)行,結(jié)果如下:

可以發(fā)現(xiàn)以下幾點(diǎn):
1.       非靜態(tài)數(shù)據(jù)成員a的存儲(chǔ)地址就是從類的實(shí)例在內(nèi)存中的地址中(本例中均為0xbfadfc64)開始的,之后的double b也緊隨其后,在內(nèi)存中連續(xù)存儲(chǔ);
2.       對(duì)于靜態(tài)數(shù)據(jù)成員c,則出現(xiàn)在了一個(gè)很“莫名其妙”的地址0x804a028上,與類的實(shí)例的地址看上去那是八竿子打不著;
其實(shí)不做這個(gè)測(cè)試,關(guān)于C++數(shù)據(jù)成員存儲(chǔ)的問(wèn)題也都是C++ Programmer的常識(shí),對(duì)于非靜態(tài)數(shù)據(jù)成員,一般編譯器都是按其在類中聲明的順序存儲(chǔ),而且數(shù)據(jù)成員的起始地址就是類得實(shí)例在內(nèi)存中的起始地址,這個(gè)在上面的測(cè)試中已經(jīng)很明顯了。對(duì)非靜態(tài)數(shù)據(jù)成員的讀寫,我們可以這樣想,其實(shí)C++程序完全可以轉(zhuǎn)換成對(duì)應(yīng)的C程序來(lái)編寫,有一些C++編譯器編譯C++程序時(shí)就是這樣做的。對(duì)非靜態(tài)數(shù)據(jù)成員的讀寫也可以借助這個(gè)等價(jià)的C程序來(lái)理解?紤]下面代碼段2:
// C++ code
struct foo{
public:
   int get_data() const{ return data; }
   void set_data(int _data){ data = _data;}
private:
   int data;
};

foo f();
int d = f.get_data();
如果要你用C你會(huì)怎么實(shí)現(xiàn)呢?
// C code
struct foo{
   int data;
};
int get_foo_data(const foo* pFoo){ return pFoo->data;}
void set_foo_data(foo* pFoo, int _data){ pFoo->data = _data;}

foo f;
f.data = 8;
foo* pF = &f;
int d = get_foo_data(pF);
在C程序中,我們要實(shí)現(xiàn)同樣的功能,必須是要往函數(shù)的參數(shù)列表中壓入一個(gè)指針作為實(shí)參。實(shí)際上C++在處理非靜態(tài)數(shù)據(jù)成員的時(shí)候也是這樣的,C++必須借助一個(gè)直接的或暗喻的實(shí)例指針來(lái)讀寫這些數(shù)據(jù),這個(gè)指針,就是大名鼎鼎的 this指針。有了this指針,當(dāng)我們要讀寫某個(gè)數(shù)據(jù)時(shí),就可以借助一個(gè)簡(jiǎn)單的指針運(yùn)算,即this指針的地址加上該數(shù)據(jù)成員的偏移量,就可以實(shí)現(xiàn)讀寫了。這個(gè)偏移量由C++編譯器為我們計(jì)算出來(lái)。
對(duì)于靜態(tài)數(shù)據(jù)成員,如果在static_mem.cpp中加入下面一條語(yǔ)句:
std::cout<<”Size of class memtest is :  ”<<sizeof(memtest)<<”\n”;
我們得到的輸出是:12。也就是說(shuō),class的大小僅僅是一個(gè)int 和一個(gè)double所占用的內(nèi)存之和。這很簡(jiǎn)單,也很明顯,靜態(tài)數(shù)據(jù)成員沒有存儲(chǔ)在類實(shí)例的地址空間中,它被C++編譯器弄到外面去了也就是程序的data segment中,因?yàn)殪o態(tài)數(shù)據(jù)成員不在類的實(shí)例當(dāng)中,所以也就不需要this指針的幫忙了。
1.2 單繼承與多重繼承的情況
由于我們還沒有討論類函數(shù)成員的情況,尤其,虛函數(shù),在這一部分我們不考慮繼承中的多態(tài)問(wèn)題,也就是說(shuō),這里的父類沒有虛函數(shù)——雖然這在實(shí)際中幾乎就是禁手。如此,我們的討論簡(jiǎn)潔很多了。
在C++繼承模型中,一個(gè)子類的內(nèi)存模型可以看成就是父類的各數(shù)據(jù)成員與自己新添加的數(shù)據(jù)成員的總和。請(qǐng)看下面的程序段3。
class father
{
public:
   // constructors destructor
   // access functions
   // operations
private:
   int age;
   char sex;
   std::string phone_number;
};

class child : public father
{
public:
   // ...
private:
   std::string twitter_url; // 兒子時(shí)髦,有推號(hào)
};
這里sizeof(father)和sizeof(child)分別是12和16(GCC 4.4.5)。先看sizeof(father)吧,int占4 bytes,char占1byte,std::string再占4 bytes,系統(tǒng)再將char圓整到4的倍數(shù)個(gè)字節(jié),所以一共就是12 bytes了,對(duì)于child類,由于它僅僅引入了一個(gè)std::string,所以在12的基礎(chǔ)上加上std::string的4字節(jié)就是16字節(jié)了。
在單繼承不考慮多態(tài)的情況下,數(shù)據(jù)成員的布局是很簡(jiǎn)單的。用一個(gè)圖來(lái)說(shuō)明,如下。


多重繼承一般都被公認(rèn)為C++復(fù)雜性的證據(jù)之一,但是就數(shù)據(jù)成員而言,其實(shí)也很簡(jiǎn)單,多重繼承的復(fù)雜性主要是指針類型轉(zhuǎn)換與環(huán)形繼承鏈的問(wèn)題,這些內(nèi)容都將在第二部分講述。
假設(shè)有下面三個(gè)類,如下面的程序段4所示,繼承結(jié)構(gòu)關(guān)系如圖:
class A{
public:
   // ...
private:
   int a;
   double b;
};

class B{
public:
   // ...
private:
   char c;
};

class C : public A, public B
public:
   // ...
private:
   float f;
};


那么,對(duì)應(yīng)的內(nèi)存布局就是圖4所示。


1.3 虛繼承
多重繼承的一個(gè)語(yǔ)意上的副作用就是它必須支持某種形式的共享子對(duì)象繼承,所謂共享,其實(shí)就是環(huán)形繼承鏈問(wèn)題。最經(jīng)典的例子就是標(biāo)準(zhǔn)庫(kù)本身的iostream繼承族。
class ios{...};
class istream : public ios {...};
class ostream : public ios {...};
class iostream : public istream, public ostream {...};
無(wú)論是istream還是ostream都含有一個(gè)ios類型的子對(duì)象。然而在iostream的對(duì)象布局中,我們只需要一個(gè)這樣的ios子對(duì)象就可以了,由此,新語(yǔ)法虛擬繼承就引入了。
虛擬繼承中,關(guān)于對(duì)象的數(shù)據(jù)成員內(nèi)存布局問(wèn)題有多種策略,在Inside the C++ Object Model中提出了三種流行的策略,而且Lippman寫此書的時(shí)候距今天已經(jīng)很遙遠(yuǎn)了,現(xiàn)代編譯器到底如何實(shí)現(xiàn)我也講不太清楚,等哪天去翻翻GCC的實(shí)現(xiàn)手冊(cè)再論,今天先前一筆債在這。
2、C++類函數(shù)成員的內(nèi)存模型
2.1 關(guān)于C++指針類型
要理解好C++類的函數(shù)成員的內(nèi)存模型,尤其是虛函數(shù)的實(shí)現(xiàn)機(jī)制,一定要對(duì)指針的概念非常清晰,指針是絕對(duì)的利器,無(wú)論是編寫代碼還是研究?jī)?nèi)部各種機(jī)制的實(shí)現(xiàn)機(jī)理,這是由計(jì)算機(jī)體系結(jié)構(gòu)決定的。先給一段代碼,標(biāo)記為代碼段5:
class foo{
  //...
};
int a(1);
double b(2.0);
foo f = foo();

int* pa = &a;
double* pb = &b;
foo* pf = &f;
我們知道,int指針的內(nèi)容是一個(gè) 表征int數(shù)據(jù)結(jié)構(gòu) 的地址,foo指針的內(nèi)容就是一個(gè) 表征foo數(shù)據(jù)結(jié)構(gòu) 的地址。那么,系統(tǒng)是如何分別對(duì)待這些看上去就是0101的地址的呢?同樣是一個(gè) 1000110100...10100,我怎么知道這個(gè)地址就一個(gè)int 數(shù)據(jù)結(jié)構(gòu)的地址呢?它NN的拼什么就不是一個(gè) foo 數(shù)據(jù)結(jié)構(gòu)的地址呢?我只有知道了它是int,我才知道應(yīng)該取出從1000110100...10100開始的4個(gè)byte,對(duì)不對(duì)?
所以我就想——強(qiáng)調(diào)一下,我也只是在猜想——一定是指針的數(shù)據(jù)類型(比如int*,還是foo*?)里面保存了相關(guān)的信息,這些信息告訴系統(tǒng),我要的是一個(gè)int,你給我取連續(xù)的4個(gè)byte出來(lái);我要的是一個(gè)foo結(jié)構(gòu),你給我取XX個(gè)連續(xù)的byte出來(lái)…
簡(jiǎn)單地說(shuō),指針類型中包含了一個(gè)類似于 sizeof 的信息,或者其他的輔助信息——至少我們先這么來(lái)理解,至于系統(tǒng)到底怎么實(shí)現(xiàn)的,那是《編譯原理》上艱深的理論和GCC浩繁的代碼里黑客們的神跡了。這個(gè)sizeof的信息就告訴了系統(tǒng)你應(yīng)該拿幾個(gè)(連續(xù))地址上的字節(jié)返回給我。例如,int* pInt的值為0xbfadfc64,那么系統(tǒng)根據(jù)int*這個(gè)指針的類型,就知道應(yīng)該把從0xbfadfc64到0xbfadfc68的這一段內(nèi)存上的數(shù)據(jù)取出來(lái)返回。
回到C++的話題上,假設(shè)下面的代碼段6,其實(shí)就是前面代碼段3,為了閱讀的方便copy過(guò)來(lái)一下。
class father
{
public:
   // constructors destructor
   // access functions
   // operations
private:
   int age;
   char sex;
   std::string phone_number;
};

class child : public father
{
public:
   // ...
private:
   std::string twitter_url; // 兒子時(shí)髦,有推號(hào)
};
現(xiàn)在我進(jìn)行下面的調(diào)用:
child c();
father* pF = &c;
child* pC = &c;
std::string tu;

tu = pF->twitter_url;// 這個(gè)調(diào)用是非法的,原因我們后面說(shuō),暫且將這一行標(biāo)記為(*)
tu = pC->twitter_url;
if(child* pC1 = dynamic_cast<child*>(pF))
    tu = pC1->twitter_url;
對(duì)于(*)行,其實(shí)原因就是我們前面所說(shuō)的,指針類型中包含了一個(gè)類似于sizeof 的信息,或者其他的輔助信息,對(duì)比圖5,我們可以這樣子想,一個(gè)father類型object嵌套在了一個(gè)child類型的object里面,因?yàn)橹羔橆愋陀幸粋(gè)sizeof的信息,這個(gè)信息決定了一個(gè)pF類型的指針只能取到12個(gè)連續(xù)字節(jié)的內(nèi)容,(*)試圖訪問(wèn)到這個(gè)12個(gè)字節(jié)之外的內(nèi)容,當(dāng)然也就要報(bào)錯(cuò)了。
我得說(shuō)明一句,這樣子想只是一種理解上的自由(而且我認(rèn)為這樣理解,從結(jié)論和效果上講是靠譜的),到底是不是這樣子,我還并沒有調(diào)查清楚。


這里,我們先調(diào)查了一下指針訪問(wèn)類的數(shù)據(jù)成員,還沒有涉及到函數(shù)成員,但其實(shí)這才是本部分的核心內(nèi)容。OK,馬不停蹄趁熱打鐵,接下來(lái)我們就說(shuō)這個(gè)故事。

2.2 靜態(tài)函數(shù)成員
與靜態(tài)數(shù)據(jù)成員一樣,靜態(tài)函數(shù)成員從實(shí)現(xiàn)的角度上講,最大的特點(diǎn)就是編譯器在處理靜態(tài)函數(shù)成員的時(shí)候不會(huì)講一個(gè)this指針壓入其參數(shù)列表,回顧代碼段2,一般的成員函數(shù)都會(huì)壓入一個(gè)this到參數(shù)列表的。這個(gè)實(shí)現(xiàn)的不同,決定了靜態(tài)函數(shù)成員們?cè)S多不一樣的特性。
如果取一個(gè)靜態(tài)函數(shù)成員的地址,獲得的就是其在內(nèi)存中的地址,由于它們沒有this指針,所以其地址類型并不是一個(gè)指向類成員函數(shù)的特別的指針。
也由于沒有了this指針這一本質(zhì)特點(diǎn),靜態(tài)函數(shù)成員有了以下的語(yǔ)法特點(diǎn):
l  它不能直接讀寫class內(nèi)的非靜態(tài)成員,無(wú)論是數(shù)據(jù)成員還是函數(shù)成員;
l  它不能聲明為const或是virtual;
l  它不是由類的實(shí)例來(lái)調(diào)用的,而是類作用域界定符;
這里,我想起了《大學(xué)》上一段話:物有本末,事有終始,知所先后,則近道矣”,這話太TMD妙了,凡事入乎其內(nèi),外面的什么東西都是浮云,就像《越獄》里的Micheal看到一面墻就想得到里面的鋼筋螺絲,這時(shí)候這面墻已經(jīng)不是一面墻了。如果只是生硬地去記憶上面那些東西,那是何其痛苦的事情,也幾乎不可能,但是一旦“入乎其內(nèi)”了,這些東西就真的很簡(jiǎn)單了。
靜態(tài)函數(shù)成員的特點(diǎn)賦予了它一些有趣的應(yīng)用場(chǎng)合,比如它可以成為一個(gè)回調(diào)函數(shù),MFC大量應(yīng)用了這一點(diǎn);它也可以成功地應(yīng)用線程函數(shù)身上。
2.3 非靜態(tài)函數(shù)成員
還是可以回到代碼段3,其實(shí)這個(gè)代碼段已經(jīng)給出了非靜態(tài)成員函數(shù)的實(shí)現(xiàn)機(jī)制。
1.       改寫非靜態(tài)成員函數(shù)的函數(shù)原型,壓入一個(gè)額外的this指針到成員函數(shù)的參數(shù)列表中,目的就是提供一個(gè)訪問(wèn)類的實(shí)例的非靜態(tài)數(shù)據(jù)/函數(shù)成員的渠道;
2.       將每一個(gè)對(duì)非靜態(tài)數(shù)據(jù)/函數(shù)成員的讀寫操作改為經(jīng)由this指針來(lái)讀寫;
3.       最驚訝的一步是,將成員函數(shù)改寫為一個(gè)外部函數(shù)——Gotcha!這就是為什么sizeof(Class)的時(shí)候不會(huì)將非虛函數(shù)地址指針計(jì)算進(jìn)去的原因,因?yàn)?非靜態(tài))成員函數(shù)都被搬到類的外面去了,并借助Name Mangling算法將函數(shù)名轉(zhuǎn)化為一個(gè)全局唯一的名字。
對(duì)于第3點(diǎn),有一個(gè)明顯的好處就是,對(duì)類成員函數(shù)的調(diào)用就和一般的函數(shù)調(diào)用幾乎沒任何開銷上的差異,幾乎從C++投胎開始,效率就成為了C++的極致追求之一。


分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享淘帖 頂 踩
回復(fù)

使用道具 舉報(bào)

本版積分規(guī)則

小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表