1. 程式人生 > >C++ 物件模型詳細講解(特別容易理解)

C++ 物件模型詳細講解(特別容易理解)

感謝,Thanks!

                                         四 堆疊與函式呼叫

一 C++程式記憶體分配

1) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,一般使用暫存器來存取,效率很高,但是分配的記憶體容量有限。 
2) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意多少的記憶體,程式設計師自己負責在何時用free或delete來釋放記憶體。動態記憶體的生存期由程式設計師自己決定,使用非常靈活。 
3) 從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static變數。 
4) 文字常量分配在文字常量區,程式結束後由系統釋放。 
5)程式程式碼區。

經典例項:(程式碼來自網路高手,沒有找到原作者)

Code 
#include <string>int a=0;    //全域性初始化區char *p1;   //全域性未初始化區 void main() 

    
int b;//char s[]="abc";   //char *p2;         //char *p3="123456";   //123456"0在常量區,p3在棧上。static int c=0;   //全域性(靜態)初始化區    p1 = (char*)malloc(10); 
    p2 
= (char*)malloc(20);   //分配得來得10和20位元組的區域就在堆區。
    strcpy(p1,"123456");   //123456"0放在常量區,編譯器可能會將它與p3所向"123456"0"優化成一個地方。}

二 三種記憶體物件的比較 
棧物件的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程式設計師操心;而且棧物件的建立速度一般 較堆物件快,因為分配堆物件時,會呼叫operator new操作,operator new會採用某種記憶體空間搜尋演算法,而該搜尋過程可能是很費時間的,產生棧物件則沒有這麼麻煩,它僅僅需要移動棧頂指標就可以了。但是要注意的是,通常棧 空間容量比較小,一般是1MB~2MB,所以體積比較大的物件不適合在棧中分配。特別要注意遞迴函式中最好不要使用棧物件,因為隨著遞迴呼叫深度的增加, 所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢位,這樣就會產生執行時錯誤。 
堆物件建立和銷燬都要由程式設計師負責,所以,如果 處理不好,就會發生記憶體問題。如果分配了堆物件,卻忘記了釋放,就會產生記憶體洩漏;而如 果已釋放了物件,卻沒有將相應的指標置為NULL,該指標就是所謂的“懸掛指標”,再度使用此指標時,就會出現非法訪問,嚴重時就導致程式崩潰。但是高效 的使用堆物件也可以大大的提高程式碼質量。比如,我們需要建立一個大物件,且需要被多個函式所訪問,那麼這個時候建立一個堆物件無疑是良好的選擇,因為我們 通過在各個函式之間傳遞這個堆物件的指標,便可以實現對該物件的共享,相比整個物件的傳遞,大大的降低了物件的拷貝時間。另外,相比於棧空間,堆的容量要 大得多。實際上,當實體記憶體不夠時,如果這時還需要生成新的堆物件,通常不會產生執行時錯誤,而是系統會使用虛擬記憶體來擴充套件實際的實體記憶體。
靜態儲存區。所有的靜態物件、全域性物件都於靜態儲存區分配。關於全域性物件,是在main()函式執行前就分配好了的。其實,在main()函式中的顯示代 碼執行之前,會呼叫一個由編譯器生成的_main()函式,而_main()函式會進行所有全域性物件的的構造及初始化工作。而在main()函式結束之 前,會呼叫由編譯器生成的exit函式,來釋放所有的全域性物件。比如下面的程式碼:

void main(void) 

… …// 顯式程式碼 
}

實際上,被轉化成這樣:

void main(void) 

_main(); //隱式程式碼,由編譯器產生,用以構造所有全域性物件 
… … // 顯式程式碼 
… … 
exit() ; // 隱式程式碼,由編譯器產生,用以釋放所有全域性物件 
}

  除了全域性靜態物件,還有區域性靜態物件通和class的靜態成員,區域性靜態物件是在函式中定義的,就像棧物件一樣,只不過,其前面多了個 static關鍵字。區域性靜態物件的生命期是從其所在函式第一次被呼叫,更確切地說,是當第一次執行到該靜態物件的宣告程式碼時,產生該靜態區域性物件,直到 整個程式結束時,才銷燬該物件。class的靜態成員的生命週期是該class的第一次呼叫到程式的結束。

三 函式呼叫與堆疊

1)編譯器一般使用棧來存放函式的引數,區域性變數等來實現函式呼叫。有時候函式有巢狀呼叫,這個時候棧中會有多個函式的資訊,每個函式佔用一個連續的區域。一個函式佔用的區域被稱作幀(frame)。同時棧是執行緒獨立的,每個執行緒都有自己的棧。例如下面簡單的函式呼叫:

另外函式堆疊的清理方式決定了當函式呼叫結束時由呼叫函式或被呼叫函式來清理函式幀,在VC中對函式棧的清理方式由兩種:

引數傳遞順序 誰負責清理引數佔用的堆疊
__stdcall 從右到左 被調函式
__cdecl 從右到左 呼叫者

2) 有了上面的知識為鋪墊,我們下面細看一個函式的呼叫時堆疊的變化:

程式碼如下:

Code
int Add(int x, int y)
{
    
return x + y;
}

void main()
{
    
int *pi = new int(10);
    
int *pj = new int(20);
    
int result = 0;
    result 
= Add(*pi,*pj);
    delete pi;
    delete pj;
}

對上面的程式碼,我們分為四步,當然我們只畫出了我們的程式碼對堆疊的影響,其他的我們假設它們不存在,哈哈!

第一,int *pi = new int(10);   int *pj = new int(20);   int result = 0; 堆疊變化如下:

第二,Add(*pi,*pj);堆疊如下:

第三,將Add的結果給result,堆疊如下:

第四,delete pi;    delete pj; 堆疊如下:

第五,當main()退出後,堆疊如下,等同於main執行前,哈哈!


四 完!

感謝,Thanks!

                                      五 sizeof與記憶體佈局

有了前面幾節的鋪墊,本節開始摸索C++的物件的記憶體佈局,平臺為windows32位+VS2008。

一 內建型別的size

內建型別,直接上程式碼,幫助大家加深記憶:

Code 
void TestBasicSizeOf() 

    cout 
<< __FUNCTION__ << endl; 

    cout 
<< "  sizeof(char)= " << sizeof ( char ) << endl; 
    cout 
<< "  sizeof(int)= " << sizeof ( int ) << endl; 
    cout 
<< "  sizeof(float)= " << sizeof ( float ) << endl; 
    cout 
<< "  sizeof(double)= " << sizeof ( double ) << endl; 

    cout 
<< "  sizeof('$')=" << sizeof ( '$' ) << endl; 
    cout 
<< "  sizeof(1)= " << sizeof ( 1 ) << endl; 
    cout 
<< "  sizeof(1.5f)= " << sizeof ( 1.5f ) << endl; 
    cout 
<< "  sizeof(1.5)= " << sizeof ( 1.5 ) << endl; 

    cout 
<< "  sizeof(Good!)= " << sizeof ( "Good!" ) << endl ; 

    
char  str[] = "CharArray!"
    
int  a[10];  
    
double  xy[10]; 
    cout 
<< "  char str[] = ""CharArray!""," << " sizeof(str)= " << sizeof (str) << endl; 
    cout 
<< "  int a[10]," << " sizeof(a)= " << sizeof (a) << endl; 
    cout 
<< "  double xy[10]," << " sizeof(xy)= " <<sizeof (xy) << endl; 

    cout 
<< "  sizeof(void*)= " << sizeof(void*<< endl; 
}

執行結果如下:


二 struct/class的大小

在C++中我們知道struct和class的唯一區別就是預設的訪問級別不同,struct預設為public,而class的預設為 private。所以考慮物件的大小,我們均以struct為例。對於struct的大小對於初學者來說還確實是個難回答的問題,我們就通過下面的一個 struct定義加逐步的變化來引出相關的知識。

程式碼如下:

Code
struct st1
{
    
short number;
    
float math_grade;
    
float Chinese_grade;
    
float sum_grade;
    
char  level;
}; //20

struct st2
{
    
char  level;
    
short number;
    
float math_grade;
    
float Chinese_grade;
    
float sum_grade;
};//16

#pragma pack(1)struct st3
{
    
char  level;
    
short number;
    
float math_grade;
    
float Chinese_grade;
    
float sum_grade;
}; //15
#pragma pack() void TestStructSizeOf()
{
    cout 
<< __FUNCTION__ << endl;

    cout 
<< "  sizeof(st1)= " << sizeof (st1) << endl;
    cout 
<< "  offsetof(st1,number) " << offsetof(st1,number) << endl;
    cout 
<< "  offsetof(st1,math_grade) " << offsetof(st1,math_grade) << endl;
    cout 
<< "  offsetof(st1,Chinese_grade) " << offsetof(st1,Chinese_grade) << endl;
    cout 
<< "  offsetof(st1,sum_grade) " << offsetof(st1,sum_grade) << endl;
    cout 
<< "  offsetof(st1,level) " << offsetof(st1,level) << endl;

    cout 
<< "  sizeof(st2)= " << sizeof (st2) << endl;
    cout 
<< "  offsetof(st2,level) " << offsetof(st2,level) << endl;
    cout 
<< "  offsetof(st2,number) " << offsetof(st2,number) << endl;
    cout 
<< "  offsetof(st2,math_grade) " << offsetof(st2,math_grade) << endl;
    cout 
<< "  offsetof(st2,Chinese_grade) " << offsetof(st2,Chinese_grade) << endl;
    cout 
<< "  offsetof(st2,sum_grade) " << offsetof(st2,sum_grade) << endl;


    cout 
<< "  sizeof(st3)= " << sizeof (st3) << endl;
    cout 
<< "  offsetof(st3,level) " << offsetof(st3,level) << endl;
    cout 
<< "  offsetof(st3,number) " << offsetof(st3,number) << endl;
    cout 
<< "  offsetof(st3,math_grade) " << offsetof(st3,math_grade) << endl;
    cout 
<< "  offsetof(st3,Chinese_grade) " << offsetof(st3,Chinese_grade) << endl;
    cout 
<< "  offsetof(st3,sum_grade) " << offsetof(st3,sum_grade) << endl;
}

執行結果如下;

基於上面的對struct的測試,我們是不是有些驚呆哦,對於C++的初學者更是情不自禁的說:“我靠!原來順序不同所佔空間都不同啊,還有那個 pack是啥東東啊?”,其實這裡蘊含了一個記憶體對齊的問題,在計算機的底層進行記憶體的讀寫的時候,如果記憶體對齊的話可以提高讀寫效率,下面是VC的預設 規則:

1) 結構體變數的首地址能夠被其最寬基本型別成員的大小所整除; 
2) 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍, 如有需要編譯器會在成員之間加上填充位元組(internal adding); 
3) 結構體的總大小為結構體最寬基本型別成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組(trailing padding)。

當然VC提供了工程選項/Zp[1|2|4|8|16]可以修改對齊方式,當然我們也可以在程式碼中對部分型別實行特殊的記憶體對齊方式,修改方式為#pragma pack( n ),n為位元組對齊 
數,其取值為1、2、4、8、16,預設是8,取消修改用#pragma pack(),如果結構體某成員的sizeof大於你設定的,則按你的設定來對齊

三 struct的巢狀

1)例項:

Code
struct A
{
    
int i;
    
char c;
    
double d;
    
short s;
}; 
// 24struct B
{
    
char cc;
    A a;
    
int ii;
}; 
// 40

佈局:(使用VS的未釋出的編譯選項/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)

2)例項:

Code
#pragma pack(4)struct A2
{
    
int i;
    
char c;
    
double d;
    
short s;
}; 
// 20#pragma pack()struct B2
{
    
char cc;
    A2 a;
    
int ii;
}; 
// 28

佈局:(使用VS的未釋出的編譯選項/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)

總結:

  由於結構體的成員可以是複合型別,比如另外一個結構體,所以在尋找最寬基本型別成員時,應當包括複合型別成員的子成員,而不是把複合成員看成是一個整體。但在確定複合型別成員的偏移位置時則是將複合型別作為整體看待。

四 空struct/class和const,static成員

例項:

Code
struct empty{}; // 1struct constAndStatic
{
    
const int i;
    
static char c;
    
const double d;
    
static void TestStatic(){}
    
void TestNoStatic(){}
}; 
// 16

佈局:(使用VS的未釋出的編譯選項/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)

上面的例項中empty的大小為1,而constAndStatic的大小為16。

總結:

因為static成員和函式其實是類層次的,不在物件中分配空間,而成員函式其實是被編譯為全域性函數了,所以也不在物件中。

五 本節完,下次探討虛擬函式對記憶體佈局的影響!

感謝,Thanks!

                                        六 單繼承與虛擬函式表

一 單繼承

1) 程式碼:

Code
#include <iostream>using namespace std;

class A
{
public:
    
void f1(){cout << "A::f1" << endl;}
    
void f2(){cout << "A::f2" << endl;}
    
virtual void v1(){cout << "A::v1" << endl;}
    
virtual void v2(){cout << "A::v2" << endl;}
    
int x;
};

class B : public A
{
public:
    
void f2(){cout << "B::f2" << endl;} // 覆蓋void v2(){cout << "B::v2" << endl;} // 重寫void f3(){cout << "B::f3" << endl;} 
    
virtual void v3(){cout << "B::v3" << endl;}
    
int y;
};

class C : public B
{
public:
    
void f3(){cout << "C::f3" << endl;} // 覆蓋void v1(){cout << "C::v1" << endl;} // 重寫void v3(){cout << "C::v3" << endl;} // 重寫int z;
};

2)類圖:

3)VS2008的編譯選項檢視佈局:

4)視覺化表示:

5)程式碼驗證:

Codetypedef void (*Fun)();

void PrintVTable(A *pA)
{
    
int *pVT = (int*)*(int*)(pA);
    Fun
* pF = (Fun*)(pVT + 0);
    
int iLength = 0;
    
while(*pF != NULL)
    {
        (
*pF)();
        
++iLength;
        pF 
= (Fun*)(pVT + iLength);
    }
}
void PrintMembers(A *pA)
{
    
int *= (int*)(pA);
    
int i = 1;
    
while(i <= 3)
    {
        cout 
<< *(p+i) << endl;
        i
++;
    }
}
void TestVT()
{
    A 
*pA = new C();
    C 
*pC = dynamic_cast<C*>(pA);
    pC
->= 10;
    pC
->= 20;
    pC
->= 30;
    PrintVTable(pA);
    PrintMembers(pA);
    delete pA;
}

6)驗證程式碼執行結果:

7)總結:

單繼承的物件的佈局,第一個為虛擬函式表指標vtbl,其後為成員且先基類後子類,虛擬函式表裡包含了所有的虛擬函式的地址,以NULL結束。虛擬函式如果子類有重寫,就由子類的重新的代替。

二 單繼承執行時型別轉化

1)程式碼驗證:

Code
void TestDynamicCast()
{
    A 
*pA = new C();
    cout 
<< "A:" << pA << endl;
    B 
*pB = dynamic_cast<B*>(pA);
    cout 
<< "B:" << pB << endl;
    C 
*pC = dynamic_cast<C*>(pA);
    cout 
<< "C:" << pC << endl;
}

2)驗證程式碼執行結果:

3)總結:

我們上面看了單繼承的記憶體佈局,而這樣的記憶體佈局也就決定了當dynamic_cast的時候,都還是同一地址,不需要做指標的移動。只是型別的改變即所能訪問的範圍的改變。

三 完!

感謝,Thanks!

                                     七 多繼承與虛擬函式表

一 多重繼承

1) 程式碼:

Code
#include <iostream>using namespace std;

class B1
{
public:
    
int x;
    
virtual void v1(){ cout << "B1::v1" << endl; }
    
void f1(){cout << "B1::f1" << endl; }
};

class B2
{
public:
    
int y;
    
virtual void v2(){ cout << "B2::v2" << endl; }
    
void f2(){ cout << "B2::f2" << endl; }
};

class B3
{
public:
    
int z;
    
virtual void v3(){ cout << "B3::v3" << endl; }
    
void f3(){ cout << 

相關推薦

C++ 物件模型詳細講解特別容易理解

感謝,Thanks!                                          四 堆疊與函式呼叫 一 C++程式記憶體分配 1) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,

《深度探索C++物件模型》筆記建構函式、拷貝構造和初始化列表

歡迎檢視系列部落格: --------------------------------------------------------------------------------------------------------------         看了這一章

Centos7.4伺服器安裝Laravel5.7詳細講解2018-10-27

一、在阿里雲或者騰訊雲選擇Centos7併購買伺服器 二、安裝寶塔面板和php執行環境 1、輸入命令 yum install -y wget && wget -O install.sh http://download.bt.cn/install/inst

WIN|U盤修復詳細講解親測可用

U盤在日常生活中的角色也算是至關重要的了吧,但是在使用U盤的時候難免也會遇到一些問題。 這個問題大家應該都遇到過了吧。但是點選【格式化磁碟】按照提醒的步驟還是不能恢復,那該怎麼辦?今天給大家詳細講解一下遇到這樣問題如何去修復。 使用工具:USboot(傳送門)

hdu5687容易理解

字典樹模板題。涉及到字典樹的插入,查詢,刪除操作。 最開始使用的是動態陣列來解,這樣做的話,如果測試樣例先是刪除操作,就會報陣列溢位錯誤。奉上字典樹模板 開始使用這個模板時,說這個模板容易爆記憶體溢位,不過多慮了(大概開350萬才會爆),除此之外時間也稍微久了一點,但是理

看懂二叉樹的三種遍歷比較容易理解

轉載:http://blog.csdn.net/soundwave_/article/details/53120766二叉樹的遍歷分為以下三種:先序遍歷:遍歷順序規則為【根左右】中序遍歷:遍歷順序規則為【左根右】後序遍歷:遍歷順序規則為【左右根】什麼是【根左右】?就是先遍歷根

深度探索C++物件模型1——物件1

(1)一個類物件至少佔用一個位元組的記憶體空間,哪怕是一個空類          為什麼sizeof(空類)=1,而不等於0?        

【深度探索C++物件模型2.5bitwise和memberwise

在看《深入探索C++物件模型》這本書的時候,我看見了bitwise senimatics和memberwise senimatics,看的時候還不清楚這兩個是什麼意思,書本上直接使用的是英文,所以我的直譯就是位逐次語意和成員逐次語意,經過一番百度後才發現原來就是簡單的淺拷貝和深拷貝的區別。

【深度探索C++物件模型1關於物件

哎 再開新坑,希望19年能把開的這幾個坑都填上。 class : 類 class object : 類物件 1 C++物件模型 簡單來說,C++物件模型的例項的組成包括下面幾個部分: Nonstatic data members與**virtual pointer(vpt

C++物件模型之記憶體佈局3

轉載地址:https://mp.weixin.qq.com/s/dTyAC2IQ50c9nmQGOC0c2A   經過兩天的摸索,今天終於搞清楚C++物件模型.前兩篇C++物件模型之記憶體佈局(2)C++物件模型之記憶體佈局(1)(請戳我)已經講解了單繼承,多重繼承和多繼承的物件模

C++物件模型之記憶體佈局2

轉載地址:https://mp.weixin.qq.com/s/UQhTAXIHffN3Now4_utb6g   在C++物件模型之記憶體佈局(1)一文中分別講了無多型和有多型時單繼承的物件記憶體佈局,這篇文章將深入講解多重繼承和多繼承.   多重繼承 &nb

C++物件模型之記憶體佈局1

轉載地址: https://mp.weixin.qq.com/s/LMJ4Hsa1hmued2egk9uWMQ   如果想學習在linux或者在linux平臺下開發,學習C/或C++是非常好的選擇.俗話說,術業有專攻,學一門技術,就儘量學得深,也可以作為行走江湖,混口飯吃的一項本領

C++筆記 第五十一課 C++物件模型分析---狄泰學院

如果在閱讀過程中發現有錯誤,望評論指正,希望大家一起學習,一起進步。 學習C++編譯環境:Linux 第五十一課 C++物件模型分析(下) 1.繼承物件模型 在C++編譯器的內部類可以理解為結構體 子類是由父類成員疊加子類新成員得到的 51-1 繼承物件模型初探 #

C++筆記 第五十課 C++物件模型分析---狄泰學院

如果在閱讀過程中發現有錯誤,望評論指正,希望大家一起學習,一起進步。 學習C++編譯環境:Linux 第五十課 C++物件模型分析(上) 1.迴歸本質 class是一種特殊的struct 在記憶體中class依舊可以看做變數的集合 class與struct遵循相同的記憶體對齊

哈夫曼樹詳細講解帶例題和C語言程式碼實現——全註釋

** 哈夫曼樹詳細講解(帶例題和C語言程式碼實現——全註釋) ** 定義 哈夫曼樹又稱最優二叉樹,是一種帶權路徑長度最短的二叉樹。所謂樹的帶權路徑長度,就是樹中所有的葉結點的權值乘上其到根結點的 路徑長度(若根結點為0層,葉結點到根結點的路徑長度為葉結點

C++物件模型之記憶體佈局三虛繼承

經過兩天的摸索,今天終於搞清楚C++物件模型.前兩篇已經講解了單繼承,多重繼承和多繼承的物件模型.今天講解菱形繼承,雖然過程艱難,但是收穫豐富. 簡單虛繼承物件模型 首先編寫如下的測試程式: 1

《深度探索c++物件模型》筆記總結

首先先明確一個宗旨及兩個概念: 宗旨:C++在佈局及存取時間上主要的額外負擔是由virtual引起的 1.虛擬函式:C++多型的基本實現,沒什麼好說的,詳細見如下打包筆記:虛擬函式 2.虛基類:用來處理菱形繼承時候,在派生類中資料有重複的問題,見筆記:虛基類 ------------

《深度探索C++物件模型》學習總結——前言與導讀

前言 Foundation專案:為了構建大系統而努力定義的一個新的開發模型。 ALF:一種一面物件層次結構,提供一個永久的、以語意為基礎的表現法。 Simplifier的工作:轉換內部的程式表現。 任何物件模型都需要的轉換風味(?): 1. 與

從零開始學C++之虛擬函式與多型:虛擬函式表指標、虛解構函式、object slicing與虛擬函式、C++物件模型

#include <iostream>using namespace std;class CObject {public:     virtual void Serialize()     {         cout << "CObject::Serialize ..." <&

《深度探索C++物件模型》讀書筆記

Lippman早期在貝爾實驗室,和C++發明者Bjarne Stroustrup設計了全世界第一套C++編譯器cfront,還著有經典的C++入門書Ensential C++和C++ Primer。 全書基本以cfront的設計方法為基礎,討論編譯器如何處理C