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();