我愛學習網-上傳
當前位置: 主頁 > 文庫 > C++ >

c++ 內存模型和指針

時間:2020-09-20 16:32來源:我愛學習網 作者:apple 點擊:

對象的內存模型(無繼承)

類是創建對象的模板,不占用內存空間;而對象是實實在在的數據,需要內存來儲存。對象在創建時會在堆或棧上分配內存。

不同對象的成員變量的值可能不同,需要單獨分配內存來存儲,但是不同對象的成員函數的代碼是一樣的。所以編譯器會將成員變量和成員函數分開存儲:分別為每個對象的成員變量分配內存,但是所有對象都共享同一段函數代碼。


類是一中復雜的數據類型,在計算類的大小的時候只計算成員變量的大小,并不包含成員函數。例如:

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    void setname(char *name);
    void setage(int age);
    void setscore(float score);
    void show();
};

int main(){
    //在棧上創建對象
    Student stu;
    cout<

運行結果:

12

12

12

假設 stu 的起始地址為 0X1000,那么該對象的內存分布如下圖所示:

m_name、m_age、m_score 按照聲明的順序依次排列,和結構體非常類似,也會有內存對齊的問題。

內存對齊

這里順便提一下內存對齊。

計算機內存是以字節(Byte)為單位劃分的,理論上CPU可以訪問任意編號的字節,但實際情況并非如此。CPU 通過地址總線來訪問內存,一次能處理幾個字節的數據,就命令地址總線讀取幾個字節的數據。32 位的 CPU 一次可以處理4個字節(尋址步長)的數據,那么每次就從內存讀取4個字節的數據;少了浪費主頻,多了沒有用。64位的處理器也是這個道理,每次讀取8個字節。

將一個數據盡量放在一個步長之內,避免跨步長存儲,這稱為內存對齊。在32位編譯模式下,默認以4字節對齊;在64位編譯模式下,默認以8字節對齊。

 

  • 結構體中的內存對齊

在系統默認的情況下,結構體會進行對齊,對齊的原則如下:

原則A:結構體的成員,第一個成員在偏移0的位置,之后的每個成員的起始位置必須是當前成員大小的整數倍;

原則B:如果結構體A含有結構體成員B,那么B的起始位置必須是B中最大元素大小整數倍地址;

原則C:結構體的總大小,必須是內部最大成員的整數倍;

 

typedef struct?

{?

? int a;?

? double b;?

? char c?

}?

首先偏移量0是int大?。?)的倍數,因此直接分配,因為偏移量4不是double大?。?)的倍數,所以先擴充再分配,接著因為偏移量16是char大?。?)的倍數,所以直接分配。最后因為總大小17不是8的倍數,所以再擴充到24。

 

  • 聯合體中的內存對齊

聯合體的內存不會為了所有成員安排,而是只取最大的成員的所需內存大小,每次只能使用其中一個成員。但是有一個問題:

typedef union?

{?

? char a;?

? int[5] b;?

? double c;?

}?

當然只取最大的int數組的大小20沒錯,但是double是8字節的,而此時聯合體已經按int的4字節對齊了,所以還要額外多加4字節的內存來保證8的倍數。所以最后結果是24。

所以聯合體的內存除了取最大成員內存外,還要保證是所有成員類型size的最小公倍數。

 

對象的內存模型(有繼承,無名字遮蔽)

class A{
...
private:
    int m_a;
    int m_b;
...
};

class B:public A{
...
private:
    int m_c;
...
};

int main(){
    A obj_a(99, 10);
    B obj_b(84, 23, 95);
}

obj_a 是基類對象,obj_b 是派生類對象。假設 obj_a 的起始地址為 0X1000,那么它的內存分布如下圖所示:

假設 obj_b 的起始地址為 0X1100,那么它的內存分布如下圖所示:


可以發現,基類的成員變量排在前面,派生類的排在后面。

 

對象的內存模型(有名字遮蔽)

在上述代碼基礎上添加如下代碼:

class C:public B{
...
private:
    int m_b;
    int m_c;
    int m_d;
}

int main(){
...
    C obj_c(12,2,32,45);
}

假設 obj_c 的起始地址為 0X1300,那么它的內存分布如下圖所示:


多繼承下的內存模型

//基類A
class A{
public:
    A(int a, int b);
protected:
    int m_a;
    int m_b;
};

//基類B
class B{
public:
    B(int b, int c);
protected:
    int m_b;
    int m_c;
};

class C: public A, public B{
public:
    C(int a, int b, int c, int d);
public:
    void display();
private:
    int m_a;
    int m_c;
    int m_d;
};

int main(){
    C obj_c(10, 20, 30, 40);
}

A、B 是基類,C 是派生類,假設 obj_c 的起始地址是 0X1000,那么 obj_c 的內存分布如下圖所示:


基類對象的排列順序和繼承時聲明的順序相同。

虛繼承下的內存模型

如今虛繼承下的解決方案:VC解決方案,有點類似于虛函數。

VC 引入了虛基類表,如果某個派生類有一個或多個虛基類,編譯器就會在派生類對象中安插一個指針,指向虛基類表。虛基類表其實就是一個數組,數組中的元素存放的是各個虛基類的偏移。

 

假設 A 是 B 的虛基類,那么各對象的內存模型如下圖所示:


假設 A 是 B 的虛基類,同時 B 又是 C 的虛基類,那么各對象的內存模型如下圖所示:


虛繼承表中保存的是所有虛基類(包括直接繼承和間接繼承到的)相對于當前對象的偏移,這樣通過派生類指針訪問虛基類的成員變量時,不管繼承層次都多深,只需要一次間接轉換就可以。

 

另外,這種方案還可以避免有多個虛基類時讓派生類對象額外背負過多的指針。例如,假設 A、B、C、D 類的繼承關系為:


那么 obj_d 的內存模型如下圖所示:


 

存在虛函數時的內存模型

詳情見虛函數

 

玩轉指針之突破訪問權限

在弄清楚了C++中的內存模型,我們便可以借助指針突破訪問權限的限制,訪問private、protected屬性的成員變量。

class A{
...
private:
    int m_a;
    int m_b;
    int m_c;
};

int main(){
    A obj(10, 20, 30);
    int a = obj.m_a;  //Compile Error
    A *p = new A(40, 50, 60);
    int b = p->m_b;  //Compile Error
    return 0;
}

這段代碼說明了,無論通過對象變量還是對象指針,都不能訪問 private 屬性的成員變量。

 

使用偏移:

在對象的內存模型中,成員變量和對象的開頭位置會有一定的距離。以上面的 obj 為例,它的內存模型為:


圖中假設 obj 對象的起始地址為 0X1000,m_a、m_b、m_c 與對象開頭分別相距 0、4、8 個字節,我們將這段距離稱為偏移(Offset)。一旦知道了對象的起始地址,再加上偏移就能夠求得成員變量的地址,知道了成員變量的地址和類型,也就能夠輕而易舉地知道它的值。

 

當通過對象指針訪問成員變量時,編譯器實際上也是使用這種方式來取得它的值。為了說明問題,我們不妨將上例中成員變量的訪問權限改為 public,再來執行第 13 行的語句:

int b = p->m_b;

此時編譯器內部會發生類似下面的轉換:

int b = *(int*)( (int)p + sizeof(int) );

若在上述代碼中直接用這個替換即可突破訪問權限的限制。

 

向上轉型(將派生類賦值給基類)

類其實也是一種數據類型,也可以發生數據類型轉換,不過這種轉換只有在基類和派生類之間才有意義,并且只能將派生類賦值給基類,包括將派生類對象賦值給基類對象、將派生類指針賦值給基類指針、將派生類引用賦值給基類引用,這在 C++ 中稱為向上轉型(Upcasting)。相應地,將基類賦值給派生類稱為向下轉型(Downcasting)。

將派生類對象賦值給基類對象

#include 
using namespace std;
//基類
class A{
public:
    A(int a);
public:
    void display();
public:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="<

運行結果:

Class A: m_a=10

Class B: m_a=66, m_b=99

----------------------------

Class A: m_a=66

Class B: m_a=66, m_b=99

 

賦值的本質是將現有的數據寫入已分配好的內存中,對象的內存只包含了成員變量,所以對象之間的賦值是成員變量的賦值,成員函數不存在賦值問題。運行結果也有力地證明了這一點,雖然有a=b;這樣的賦值過程,但是 a.display() 始終調用的都是 A 類的 display() 函數。換句話說,對象之間的賦值不會影響成員函數,也不會影響 this 指針。

 

將派生類對象賦值給基類對象時,會舍棄派生類新增的成員,也就是“大材小用”,如下圖所示:


這種轉換關系是不可逆的,只能用派生類對象給基類對象賦值,而不能用基類對象給派生類對象賦值。理由很簡單,基類不包含派生類的成員變量,無法對派生類的成員變量賦值。同理,同一基類的不同派生類對象之間也不能賦值。

 

將派生類指針賦值給基類指針

除了可以將派生類對象賦值給基類對象(對象變量之間的賦值),還可以將派生類指針賦值給基類指針(對象指針之間的賦值)。我們先來看一個多繼承的例子,繼承關系為:


#include 
using namespace std;

//基類A
class A{
public:
    A(int a);
public:
    void display();
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
    cout<<"Class A: m_a="< display();
    pb = pd;
    pb -> display();
    pc = pd;
    pc -> display();
    cout

運行結果:

 

Class A: m_a=4

Class B: m_a=4, m_b=40

Class C: m_c=400

-----------------------

pa=0x9b17f8

pb=0x9b17f8

pc=0x9b1800

pd=0x9b17f8

 

本例中定義了多個對象指針,并嘗試將派生類指針賦值給基類指針。與對象變量之間的賦值不同的是,對象指針之間的賦值并沒有拷貝對象的成員,也沒有修改對象本身的數據,僅僅是改變了指針的指向。

1) 通過基類指針訪問派生類的成員

請讀者先關注第 68 行代碼,我們將派生類指針 pd 賦值給了基類指針 pa,從運行結果可以看出,調用 display() 函數時雖然使用了派生類的成員變量,但是 display() 函數本身卻是基類的。也就是說,將派生類指針賦值給基類指針時,通過基類指針只能使用派生類的成員變量,但不能使用派生類的成員函數,這看起來有點不倫不類,究竟是為什么呢?第 71、74 行代碼也是類似的情況。

 

pa 本來是基類 A 的指針,現在指向了派生類 D 的對象,這使得隱式指針 this 發生了變化,也指向了 D 類的對象,所以最終在 display() 內部使用的是 D 類對象的成員變量,相信這一點不難理解。

 

編譯器雖然通過指針的指向來訪問成員變量,但是卻不通過指針的指向來訪問成員函數:編譯器通過指針的類型來訪問成員函數。對于 pa,它的類型是 A,不管它指向哪個對象,使用的都是 A 類的成員函數,因為函數調用是根據類名、參數名等重新命名來調用的。

 

總結:

  • 編譯器通過指針來訪問成員變量,指針指向哪個對象就使用哪個對象的數據;
  • 編譯器通過指針的類型來訪問成員函數,指針屬于哪個類的類型就使用哪個類的函數。

 

2) 賦值后值不一致的情況

本例中我們將最終派生類的指針 pd 分別賦值給了基類指針 pa、pb、pc,按理說它們的值應該相等,都指向同一塊內存,但是運行結果卻有力地反駁了這種推論,只有 pa、pb、pd 三個指針的值相等,pc 的值比它們都大。也就是說,執行pc = pd;語句后,pc 和 pd 的值并不相等。

 

這非常出乎我們的意料,按照我們通常的理解,賦值就是將一個變量的值交給另外一個變量,不會出現不相等的情況,究竟是什么導致了 pc 和 pd 不相等呢?下一節會解釋。

 

將派生類引用賦值給基類引用

引用在本質上是通過指針的方式實現的,所以基類的引用也可以指向派生類的對象,并且它的表現和指針是類似的。

將派生類指針賦給基類指針

將派生類的指針賦值給基類的指針時也是類似的道理,編譯器也可能會在賦值前進行處理。

 

首先要明確的一點是,對象的指針必須要指向對象的起始位置。對于 A 類和 B 類來說,它們的子對象的起始地址和 D 類對象一樣,所以將 pd 賦值給 pa、pb 時不需要做任何調整,直接傳遞現有的值即可;而 C 類子對象距離 D 類對象的開頭有一定的偏移,將 pd 賦值給 pc 時要加上這個偏移,這樣 pc 才能指向 C 類子對象的起始位置。也就是說,執行pc = pd;語句時編譯器對 pd 的值進行了調整,才導致 pc、pd 的值不同。

 

 

------分隔線----------------------------
    ?分享到??
看看啦
主站蜘蛛池模板: 波多野结衣在线观看一区| 久久久av波多野一区二区| 国产免费一区二区视频| 中文字幕VA一区二区三区| 精品福利一区3d动漫| 人妻体内射精一区二区三区| 色精品一区二区三区| 无码精品不卡一区二区三区| 精品少妇一区二区三区视频| 亚洲爽爽一区二区三区| 日韩一区二区三区在线| 中文字幕无线码一区二区| 日韩人妻无码一区二区三区99| 久久99精品免费一区二区| 丰满人妻一区二区三区视频| 日本免费一区二区三区最新vr| 伊人久久精品无码麻豆一区| 色国产精品一区在线观看| 精品无码国产一区二区三区AV| 久久99热狠狠色精品一区| 久久se精品一区二区影院| 国产情侣一区二区三区| 久久一区二区三区99| 熟女性饥渴一区二区三区| 亚洲一区二区女搞男| 国产在线不卡一区二区三区| 中文字幕永久一区二区三区在线观看 | 日韩视频在线观看一区二区| 国产一区二区在线| 精品国产日韩亚洲一区在线| 加勒比精品久久一区二区三区| 国产成人av一区二区三区不卡| 国产日韩精品一区二区三区| 精品一区二区三区在线视频观看| 免费观看日本污污ww网站一区| 久久无码人妻精品一区二区三区| 国产波霸爆乳一区二区| 3d动漫精品一区视频在线观看| 日韩最新视频一区二区三| 国产精品久久久久一区二区| 国产一区在线视频观看|