久久久久久久999_99精品久久精品一区二区爱城_成人欧美一区二区三区在线播放_国产精品日本一区二区不卡视频_国产午夜视频_欧美精品在线观看免费

 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 1944|回復: 0
打印 上一主題 下一主題
收起左側

c++類對象內存模型

[復制鏈接]
跳轉到指定樓層
樓主
ID:107189 發表于 2016-3-5 17:43 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
c++類對象內存模型是一個比較抓狂的問題,主要是C++特性太多了,所以必須建立一個清晰的分析層次。一般而言,講到C++對象,都比較容易反應到以下這個圖表:  
這篇文章,就以這個表格作為分析和行文的策略的縱向指導;橫向上,兼以考慮無繼承、單繼承、多重繼承及虛擬繼承四方面情況,這樣一來,思維層次應該算是比較清晰了。
1、C++類數據成員的內存模型
1.1 無繼承情況
實驗最能說明問題了,首先考慮下面一個簡單的程序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下編譯,運行,結果如下:

可以發現以下幾點:
1.       非靜態數據成員a的存儲地址就是從類的實例在內存中的地址中(本例中均為0xbfadfc64)開始的,之后的double b也緊隨其后,在內存中連續存儲;
2.       對于靜態數據成員c,則出現在了一個很“莫名其妙”的地址0x804a028上,與類的實例的地址看上去那是八竿子打不著;
其實不做這個測試,關于C++數據成員存儲的問題也都是C++ Programmer的常識,對于非靜態數據成員,一般編譯器都是按其在類中聲明的順序存儲,而且數據成員的起始地址就是類得實例在內存中的起始地址,這個在上面的測試中已經很明顯了。對非靜態數據成員的讀寫,我們可以這樣想,其實C++程序完全可以轉換成對應的C程序來編寫,有一些C++編譯器編譯C++程序時就是這樣做的。對非靜態數據成員的讀寫也可以借助這個等價的C程序來理解。考慮下面代碼段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你會怎么實現呢?
// 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程序中,我們要實現同樣的功能,必須是要往函數的參數列表中壓入一個指針作為實參。實際上C++在處理非靜態數據成員的時候也是這樣的,C++必須借助一個直接的或暗喻的實例指針來讀寫這些數據,這個指針,就是大名鼎鼎的 this指針。有了this指針,當我們要讀寫某個數據時,就可以借助一個簡單的指針運算,即this指針的地址加上該數據成員的偏移量,就可以實現讀寫了。這個偏移量由C++編譯器為我們計算出來。
對于靜態數據成員,如果在static_mem.cpp中加入下面一條語句:
std::cout<<”Size of class memtest is :  ”<<sizeof(memtest)<<”\n”;
我們得到的輸出是:12。也就是說,class的大小僅僅是一個int 和一個double所占用的內存之和。這很簡單,也很明顯,靜態數據成員沒有存儲在類實例的地址空間中,它被C++編譯器弄到外面去了也就是程序的data segment中,因為靜態數據成員不在類的實例當中,所以也就不需要this指針的幫忙了。
1.2 單繼承與多重繼承的情況
由于我們還沒有討論類函數成員的情況,尤其,虛函數,在這一部分我們不考慮繼承中的多態問題,也就是說,這里的父類沒有虛函數——雖然這在實際中幾乎就是禁手。如此,我們的討論簡潔很多了。
在C++繼承模型中,一個子類的內存模型可以看成就是父類的各數據成員與自己新添加的數據成員的總和。請看下面的程序段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; // 兒子時髦,有推號
};
這里sizeof(father)和sizeof(child)分別是12和16(GCC 4.4.5)。先看sizeof(father)吧,int占4 bytes,char占1byte,std::string再占4 bytes,系統再將char圓整到4的倍數個字節,所以一共就是12 bytes了,對于child類,由于它僅僅引入了一個std::string,所以在12的基礎上加上std::string的4字節就是16字節了。
在單繼承不考慮多態的情況下,數據成員的布局是很簡單的。用一個圖來說明,如下。


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

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

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


那么,對應的內存布局就是圖4所示。


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

int* pa = &a;
double* pb = &b;
foo* pf = &f;
我們知道,int指針的內容是一個 表征int數據結構 的地址,foo指針的內容就是一個 表征foo數據結構 的地址。那么,系統是如何分別對待這些看上去就是0101的地址的呢?同樣是一個 1000110100...10100,我怎么知道這個地址就一個int 數據結構的地址呢?它NN的拼什么就不是一個 foo 數據結構的地址呢?我只有知道了它是int,我才知道應該取出從1000110100...10100開始的4個byte,對不對?
所以我就想——強調一下,我也只是在猜想——一定是指針的數據類型(比如int*,還是foo*?)里面保存了相關的信息,這些信息告訴系統,我要的是一個int,你給我取連續的4個byte出來;我要的是一個foo結構,你給我取XX個連續的byte出來…
簡單地說,指針類型中包含了一個類似于 sizeof 的信息,或者其他的輔助信息——至少我們先這么來理解,至于系統到底怎么實現的,那是《編譯原理》上艱深的理論和GCC浩繁的代碼里黑客們的神跡了。這個sizeof的信息就告訴了系統你應該拿幾個(連續)地址上的字節返回給我。例如,int* pInt的值為0xbfadfc64,那么系統根據int*這個指針的類型,就知道應該把從0xbfadfc64到0xbfadfc68的這一段內存上的數據取出來返回。
回到C++的話題上,假設下面的代碼段6,其實就是前面代碼段3,為了閱讀的方便copy過來一下。
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; // 兒子時髦,有推號
};
現在我進行下面的調用:
child c();
father* pF = &c;
child* pC = &c;
std::string tu;

tu = pF->twitter_url;// 這個調用是非法的,原因我們后面說,暫且將這一行標記為(*)
tu = pC->twitter_url;
if(child* pC1 = dynamic_cast<child*>(pF))
    tu = pC1->twitter_url;
對于(*)行,其實原因就是我們前面所說的,指針類型中包含了一個類似于sizeof 的信息,或者其他的輔助信息,對比圖5,我們可以這樣子想,一個father類型object嵌套在了一個child類型的object里面,因為指針類型有一個sizeof的信息,這個信息決定了一個pF類型的指針只能取到12個連續字節的內容,(*)試圖訪問到這個12個字節之外的內容,當然也就要報錯了。
我得說明一句,這樣子想只是一種理解上的自由(而且我認為這樣理解,從結論和效果上講是靠譜的),到底是不是這樣子,我還并沒有調查清楚。


這里,我們先調查了一下指針訪問類的數據成員,還沒有涉及到函數成員,但其實這才是本部分的核心內容。OK,馬不停蹄趁熱打鐵,接下來我們就說這個故事。

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


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

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規則

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

Powered by 單片機教程網

快速回復 返回頂部 返回列表
主站蜘蛛池模板: 国产一区二区三区视频 | 国产三级在线观看播放 | 精品综合 | 国产精品一级 | 黄色网址免费看 | 亚洲区一 | 免费日韩av网站 | 国内精品伊人久久久久网站 | 一区二区三区高清在线观看 | 中文字幕在线一区 | 人人人干| 精品综合| 91视频进入 | 精品国产乱码久久久久久a丨 | 欧美一区二区三区 | 久久中文字幕视频 | 美女张开腿露出尿口 | 午夜大片 | 日韩欧美一级片 | 午夜免费看视频 | 久久国产精品久久久久久久久久 | 蜜桃在线一区二区三区 | 国产一区二区电影网 | 玖玖国产精品视频 | 高清视频一区二区三区 | 亚洲国产成人精品女人久久久 | 欧美激情视频一区二区三区免费 | 亚洲精品在线播放 | 国产精品一区二区三区99 | 精品日韩在线 | 99re在线视频精品 | 精品一区二区三区电影 | 永久免费在线观看 | 天天操操 | 欧美v免费| 91资源在线 | 美女一级a毛片免费观看97 | 三级黄色片在线 | 天堂va在线观看 | 欧美在线观看一区 | 中文字幕免费观看 |