對象模型是面向?qū)ο蟪绦蛟O(shè)計(jì)語言的重要方面,會直接影響面向?qū)ο笳Z言編寫程序的運(yùn)行機(jī)制以及對內(nèi)存的使用機(jī)制,因此了解對象模型是進(jìn)行程序性能優(yōu)化的基礎(chǔ)。只有深入理解C++對象模型,才能避免程序開發(fā)過程中一些不易發(fā)現(xiàn)的內(nèi)存錯(cuò)誤,從而改善程序性能,提高程序質(zhì)量。
成都創(chuàng)新互聯(lián)公司致力于互聯(lián)網(wǎng)網(wǎng)站建設(shè)與網(wǎng)站營銷,提供成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、網(wǎng)站開發(fā)、seo優(yōu)化、網(wǎng)站排名、互聯(lián)網(wǎng)營銷、小程序制作、公眾號商城、等建站開發(fā),成都創(chuàng)新互聯(lián)公司網(wǎng)站建設(shè)策劃專家,為不同類型的客戶提供良好的互聯(lián)網(wǎng)應(yīng)用定制解決方案,幫助客戶在新的全球化互聯(lián)網(wǎng)環(huán)境中保持優(yōu)勢。通常,計(jì)算機(jī)程序由代碼和數(shù)據(jù)組成,因此代碼和數(shù)據(jù)也是影響程序所需內(nèi)存的主要因素。代碼是程序運(yùn)行的指令,比如數(shù)學(xué)運(yùn)算、比較、跳轉(zhuǎn)以及函數(shù)調(diào)用,其大小通常由程序的功能和復(fù)雜度決定,正確地使用程序編寫技巧以及編程語言的特性可以優(yōu)化所生成的代碼的大?。粩?shù)據(jù)是代碼要處理的對象。
程序占用的內(nèi)存區(qū)通常分為五種:全局/靜態(tài)數(shù)據(jù)區(qū)、常量數(shù)據(jù)區(qū)、代碼區(qū)、棧、堆。
程序的代碼存儲在代碼區(qū)中,而程序的數(shù)據(jù)則根據(jù)數(shù)據(jù)種類的不同存儲在不同的內(nèi)存區(qū)中。C++語言中,數(shù)據(jù)有不同的分類方法,例如常量和變量,全局?jǐn)?shù)據(jù)和局部數(shù)據(jù),靜態(tài)數(shù)據(jù)和非靜態(tài)數(shù)據(jù)。此外,程序運(yùn)行過程中動態(tài)產(chǎn)生和釋放的數(shù)據(jù)也要存放在不同的內(nèi)存區(qū)。
不同內(nèi)存區(qū)存儲的數(shù)據(jù)如下:
(1)全局/靜態(tài)數(shù)據(jù)區(qū)存儲全局變量以及靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)。
(2)常量數(shù)據(jù)區(qū)存儲程序中的常量字符串等。
(3)棧中存儲自動變量或者局部變量,以及傳遞函數(shù)參數(shù)等,而堆是用戶程序控制的存儲區(qū),存儲動態(tài)產(chǎn)生的數(shù)據(jù)。
不同類型的數(shù)據(jù)在內(nèi)存存儲位置的示例如下:
#include
#include
using namespace std;
int g_GolbalVariable = 100;
int main()
{
int localVariable = 1;
static int staticLocalVariable = 200;
const int constLocalVariable = 100;
char* pLocalString1 = "pLocalString1";
const char* pLocalString2 = "pLocalString2";
int* pNew = new int[5]; // 16字節(jié)對齊
char* pMalloc = (char*)malloc(1);
printf( "GolbalVariable: 0x%x\n", &g_GolbalVariable);
printf( "Static Variable: 0x%x\n", &staticLocalVariable);
printf( "LocalString1: 0x%x\n", pLocalString1);
printf( "const LocalString2: 0x%x\n", pLocalString2);
printf( "const LocalVariable: 0x%x\n", &constLocalVariable);
printf( "New: 0x%x\n", pNew);
printf( "Malloc: 0x%x\n", pMalloc);
printf( "LocalVariable: 0x%x\n", &localVariable);
return 0;
}
上述代碼定義了8個(gè)變量,一個(gè)全局變量GolbalVariable,一個(gè)靜態(tài)局部變量staticLocalVariable,六個(gè)局部變量。在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行,程序輸出結(jié)果如下:
GolbalVariable: 0x60105c
Static Variable: 0x601060
LocalString1: 0x4009a0
const LocalString2: 0x4009ae
const LocalVariable: 0xdbd23ef8
New: 0x182b010
Malloc: 0x182b030
LocalVariable: 0xdbd23efc
全局變量、靜態(tài)變量和局部靜態(tài)變量存儲在全局/靜態(tài)數(shù)據(jù)區(qū)。
字符串常量存儲在常量數(shù)據(jù)區(qū),pLocalString1指向的字符串"pLocalString1"的長度是13字節(jié),加上結(jié)束符’\0’,共計(jì)14個(gè)字節(jié),存儲在0x4009a0開始的14個(gè)字節(jié)內(nèi)存空間;存儲pLocalString2的字符串"pLocalString2"時(shí),從0x4009ae地址開始,因此,沒有進(jìn)行內(nèi)存對齊處理。程序中的其它字符串常量,如printf中的格式化串通常也存儲在常量數(shù)據(jù)區(qū)。
通過new、malloc獲得的內(nèi)存是堆的內(nèi)存。通過new申請5個(gè)int所需的內(nèi)存,但由于內(nèi)存邊界需要字節(jié)對齊(堆上分配內(nèi)存時(shí)按16字節(jié)對齊),因此申請5個(gè)int共計(jì)20個(gè)字節(jié),但占據(jù)32字節(jié)的內(nèi)存。通過malloc申請1個(gè)字節(jié)的內(nèi)存,申請1個(gè)字節(jié)時(shí)會從32字節(jié)后開始分配。
內(nèi)存對齊雖然會浪費(fèi)部分內(nèi)存,但由于CPU在對齊方式下運(yùn)行較快,因此內(nèi)存對齊對于程序性能是有益的。C++語言中struct、union、class在編譯時(shí)也會對成員變量進(jìn)行內(nèi)存對齊處理,開發(fā)人員可以使用#progma pack()或者編譯器的編譯選項(xiàng)來控制對struct、union、class的成員變量按多少字節(jié)對齊,或者關(guān)閉對齊。
全局/靜態(tài)存儲區(qū)、常量數(shù)據(jù)區(qū)在程序編譯階段已經(jīng)分配好,在整個(gè)程序運(yùn)行過程中始終存在,用于存儲全局變量、靜態(tài)變量,以及字符串常量等。其中字符串常量存儲的區(qū)域是不可修改的內(nèi)存區(qū)域,試圖修改字符串常量會導(dǎo)致程序異常退出。
char* pLocalString1 = "hello world";
pLocalString1[0] = 'H';// 試圖修改不可修改的內(nèi)存區(qū)
全局/靜態(tài)數(shù)據(jù)區(qū)除了全局變量,還有靜態(tài)變量。C語言中可以定義靜態(tài)變量,靜態(tài)變量在第一次進(jìn)入作用域時(shí)被初始化,后續(xù)再次進(jìn)入作用域時(shí)不必初始化。C++語言中,可以定義靜態(tài)變量,也可以定義類的靜態(tài)成員變量,類的靜態(tài)成員變量用來在類的多個(gè)對象間共享數(shù)據(jù)。類的靜態(tài)成員變量存儲在全局/靜態(tài)數(shù)據(jù)區(qū),并且只有一份拷貝,由類的所有對象共享。如果通過全局變量在類的多個(gè)對象間共享數(shù)據(jù)則會破壞類的封裝性。
#include
#include
class A
{
public:
int value;
static int nCounter;
A()
{
nCounter++;
}
~A()
{
nCounter--;
}
};
int A::nCounter = 0;
int main()
{
A a;
A b;
printf("number of A: %d\n", A::nCounter);
printf("non-static class member: 0x%x\n", &a.value);
printf("non-static class member: 0x%x\n", &b.value);
printf("static class member: 0x%x\n", &a.nCounter);
printf("static class member: 0x%x\n", &b.nCounter);
return 0;
}
上述代碼,類A定義了一個(gè)靜態(tài)成員變量nCounter用于對類A的對象進(jìn)行計(jì)數(shù),類A也定義了一個(gè)成員變量value,在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行,程序輸出結(jié)果如下:
number of A: 2
non-static class member: 0x99a457c0
non-static class member: 0x99a457b0
static class member: 0x601048
static class member: 0x601048
對象a和對象b中的value成員變量的地址不同,而靜態(tài)成員變量nCounter的地址相同。類A的每一個(gè)對象會有自己的value存儲空間,在棧上分配;類A的所有對象共享一個(gè)nCounter的存儲空間,在全局/靜態(tài)數(shù)據(jù)區(qū)分配。
在C/C++語言中,當(dāng)開發(fā)人員在函數(shù)內(nèi)部定義一個(gè)變量,或者向某個(gè)函數(shù)傳遞參數(shù)時(shí),變量和參數(shù)存儲在棧中。當(dāng)退出變量的作用域時(shí),棧上的存儲單元會被自動釋放。當(dāng)開發(fā)人員通過malloc申請一塊內(nèi)存或使用new創(chuàng)建一個(gè)對象時(shí),申請的內(nèi)存或?qū)ο笏嫉膬?nèi)存在堆上分配。開發(fā)人員需要記錄得到的地址,并在不再需要時(shí)負(fù)責(zé)釋放內(nèi)存空間。
#include
#include
using namespace std;
int g_GolbalVariable = 100;
int main()
{
int localVariable = 1;
static int staticLocalVariable = 200;
const int constLocalVariable = 100;
char* pLocalString1 = "pLocalString1";
const char* pLocalString2 = "pLocalString2";
int* pNew = new int[5]; // 16字節(jié)對齊
char* pMalloc = (char*)malloc(1);
printf( "GolbalVariable: 0x%x\n", &g_GolbalVariable);
printf( "Static Variable: 0x%x\n", &staticLocalVariable);
printf( "LocalString1: 0x%x\n", pLocalString1);
printf( "const LocalString2: 0x%x\n", pLocalString2);
printf( "const LocalVariable: 0x%x\n", &constLocalVariable);
printf( "New: 0x%x\n", pNew);
printf( "Malloc: 0x%x\n", pMalloc);
printf( "LocalVariable: 0x%x\n", &localVariable);
return 0;
}
上述代碼中,通過new在堆上申請5個(gè)int的所需的內(nèi)存空間,將獲得的地址記錄在棧上的變量pNew中;通過malloc在堆上申請1字節(jié)的內(nèi)存空間,將獲得的地址記錄在棧上的變量pMalloc中。
int* pNew = new int[5]; // 16字節(jié)對齊
char* pMalloc = (char*)malloc(1);
在main函數(shù)結(jié)束時(shí),pNew和pMalloc自身是棧上的內(nèi)存單元,會被自動釋放,但pNew和pMalloc所指向的內(nèi)存是堆上的,雖然指向堆空間的pNew和pMalloc指針變量已經(jīng)不存在,但相應(yīng)的堆空間內(nèi)存不會被自動釋放,造成內(nèi)存泄露。通過new申請的堆內(nèi)存空間需要使用delete進(jìn)行釋放,使用malloc獲得的堆空間內(nèi)存需要使用free進(jìn)行釋放。
既然棧上的內(nèi)存空間不存內(nèi)存泄露的問題,而堆上的內(nèi)存容易引起內(nèi)存泄露,為什么要使用堆上的內(nèi)存呢?因?yàn)楹芏鄳?yīng)用程序需要?jiǎng)討B(tài)管理地管理數(shù)據(jù)。此外,棧的大小有限制,占用內(nèi)存較多的對象或數(shù)據(jù)只能分配在堆空間。
棧和堆的區(qū)別如下:
(1)大小
通常,程序使用棧的大小是固定的,由編譯器決定,開發(fā)人員可以通過編譯器選項(xiàng)指定棧的大小,但通常棧都不會太大。
#include
#include
int main()
{
char buf[8 * 1024 * 1024];
printf("%x\n", buf);
return 0;
}
RHEL 7.3系統(tǒng)中默認(rèn)的棧大小為8MB,在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行,程序會運(yùn)行時(shí)出錯(cuò),原因是棧溢出。
堆的大小通常只受限于系統(tǒng)有效的虛擬內(nèi)存的大小,因此可以用來分配創(chuàng)建一些占用內(nèi)存較大的對象或數(shù)據(jù)。
(2)效率
棧上的內(nèi)存是系統(tǒng)自動分配的,壓棧和出棧都有相應(yīng)的指令進(jìn)行操作,因此效率較高,并且分配的內(nèi)存空間是連續(xù)的,不會產(chǎn)生內(nèi)存碎片;堆上的內(nèi)存是由開發(fā)人員來動態(tài)分配和回收的。當(dāng)開發(fā)人員通過new或malloc申請堆上的內(nèi)存空間時(shí),系統(tǒng)需要按照一定的算法在堆空間中尋找合適大小的空閑堆,并修改相應(yīng)的維護(hù)堆空閑空間的鏈表,然后返回地址給程序。因此,效率幣棧要低,此外還容易產(chǎn)生內(nèi)存碎片。
如果程序在堆上申請5個(gè)100字節(jié)大小的內(nèi)存塊,然后釋放其中不連續(xù)的兩個(gè)內(nèi)存塊,此時(shí)當(dāng)需要在堆上申請一個(gè)150字節(jié)大小的內(nèi)存塊時(shí),則無法充分利用剛剛釋放的兩個(gè)小內(nèi)存塊。由此可見,連續(xù)創(chuàng)建和刪除占用內(nèi)存較小的對象或數(shù)據(jù)時(shí),很容易在堆上造成內(nèi)存碎片,使得內(nèi)存的使用效率降低。
從C++對象模型角度看,對象就是內(nèi)存中的一塊區(qū)域。根據(jù)C++標(biāo)準(zhǔn),一個(gè)對象可以通過定義變量創(chuàng)建,或者通過new操作符創(chuàng)建,或者通過實(shí)現(xiàn)來創(chuàng)建。如果一個(gè)對象通過定義在某個(gè)函數(shù)內(nèi)的變量或者需要的臨時(shí)變量來創(chuàng)建,是棧上的一個(gè)對象;如果一個(gè)對象是定義在全局范圍內(nèi)的變量,則對象存儲在全局/靜態(tài)數(shù)據(jù)區(qū);如果一個(gè)對象通過new操作符創(chuàng)建,存儲在堆空間。
對面向?qū)ο蟮腃++程序設(shè)計(jì),程序運(yùn)行過程中的大部分?jǐn)?shù)據(jù)應(yīng)該封裝在對象中,而程序的行為也由對象的行為決定。因此,深入理解C++對象的內(nèi)部結(jié)構(gòu),從而正確地設(shè)計(jì)和使用對象,對于設(shè)計(jì)開發(fā)高性能的C++程序很重要。
對象的生命周期是指對象從創(chuàng)建到銷毀的過程,創(chuàng)建對象時(shí)要占用一定的內(nèi)存空間,而對象要銷毀后要釋放對應(yīng)的內(nèi)存空間,因此整個(gè)程序占用的內(nèi)存空間也會隨著對象的創(chuàng)建和銷毀而動態(tài)的發(fā)生變化。深入理解對象的生命周期會幫助分析程序?qū)?nèi)存的消耗情況,從而找到改進(jìn)方法。
對象的創(chuàng)建有三種方式,不同方式所創(chuàng)建對象的生命周期各有不同,創(chuàng)建對象的三種方式如下:
(1)通過定義變量創(chuàng)建對象
(2)通過new操作符創(chuàng)建對象
(3)通過實(shí)現(xiàn)創(chuàng)建對象
通過定義變量創(chuàng)建對象時(shí),變量的作用域決定了對象的生命周期。當(dāng)進(jìn)入變量的作用域時(shí),對象被創(chuàng)建;退出變量的作用域時(shí),對象被銷毀。全局變量的作用域時(shí)整個(gè)程序,被聲明為全局對象的變量在程序調(diào)用main函數(shù)前被創(chuàng)建,當(dāng)程序退出main函數(shù)后,全局對象才會被銷毀。靜態(tài)變量作用域不是整個(gè)程序,但靜態(tài)變量存儲在全局/靜態(tài)數(shù)據(jù)區(qū),在程序開始時(shí)已經(jīng)分配好,因此聲明為靜態(tài)變量的對象在第一次進(jìn)入作用域時(shí)會被創(chuàng)建,直到程序退出時(shí)被銷毀。
#include
#include
class A
{
public:
A()
{
printf("A Created\n");
}
~A()
{
printf("A Destroyed\n");
}
};
class B
{
public:
B()
{
printf("B Created\n");
}
~B()
{
printf("B Destroyed\n");
}
};
A globalA;
void test()
{
printf("test()------------------------->\n");
A localA;
static B localB;
printf("test()<-------------------------\n");
}
int main()
{
printf("main()------------------------->\n");
test();
test();
static B localB;
printf("main()<-------------------------\n");
return 0;
}
上述代碼中定義了一個(gè)A的全局對象globalA,一個(gè)A的局部對象localA,一個(gè)B的靜態(tài)局部對象localB,localA和localB的作用域?yàn)閠est函數(shù)。
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行結(jié)果如下:
A Created
main()------------------------->
test()------------------------->
A Created
B Created
test()<-------------------------
A Destroyed
test()------------------------->
A Created
test()<-------------------------
A Destroyed
B Created
main()<-------------------------
B Destroyed
B Destroyed
A Destroyed
根據(jù)程序運(yùn)行結(jié)果,全局對象globalA在main函數(shù)開始前被創(chuàng)建,在main函數(shù)退出后被銷毀;靜態(tài)對象localB在第一次進(jìn)入作用域時(shí)被創(chuàng)建,在main函數(shù)退出后被銷毀,如果程序從來沒有進(jìn)入到其作用域,則靜態(tài)對象不會被創(chuàng)建;局部對象在進(jìn)入作用域時(shí)被創(chuàng)建,在退出作用域時(shí)被銷毀。
通過new創(chuàng)建的對象會一直存在,直到被delete銷毀。即使指向?qū)ο蟮闹羔槺讳N毀,但還沒有調(diào)用delete,對象仍然會一直存在,占據(jù)這堆空間,直到程序退出,因此會造成內(nèi)存泄露。
#include
#include
class A
{
public:
A()
{
printf("A Created\n");
}
~A()
{
printf("A Destroyed\n");
}
};
A* createA()
{
return new A();
}
void deleteA(A* p)
{
delete p;
p = NULL;
}
int main()
{
A* pA = createA();
pA = createA();
deleteA(pA);
return 0;
}
上述代碼中,createA函數(shù)使用new操作符創(chuàng)建了一個(gè)A對象,并將返回地址記錄在pA指針變量中;然后再次使用createA函數(shù)創(chuàng)建了一個(gè)A對象,將返回地址記錄在pA指針變量中,此時(shí)pA指針將指向第二次創(chuàng)建的A對象,第一次創(chuàng)建的A對象已經(jīng)沒有指針指向。使用deleteA銷毀對象時(shí),銷毀的是第二次創(chuàng)建的A對象,第一次創(chuàng)建的A對象會一直存在,直到程序退出,并且即使在程序退出時(shí),第一次創(chuàng)建的A對象的析構(gòu)函數(shù)仍然不會被調(diào)用,最終造成內(nèi)存泄露。
通過實(shí)現(xiàn)創(chuàng)建對象通常是指一些隱藏的中間臨時(shí)變量的創(chuàng)建和銷毀。中間臨時(shí)變量的生命周期很短,不易被開發(fā)人員察覺,通常是造成性能下降的瓶頸,特別是占用內(nèi)存多、創(chuàng)建速度慢的對象。
中間臨時(shí)對象通常是通過拷貝構(gòu)造函數(shù)創(chuàng)建的。
#include
#include
class A
{
public:
A()
{
printf("A Created\n");
}
A(const A& other)
{
printf("A Created with copy\n");
}
~A()
{
printf("A Destroyed\n");
}
};
A getA(A a)
{
printf("before\n");
A b;
return b;
}
int main()
{
A a;
a = getA(a);
return 0;
}
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行結(jié)果如下:
A Created
A Created with copy
before
A Created
A Destroyed
A Destroyed
A Destroyed
getA函數(shù)的參數(shù)和返回值都是通過值傳遞的,在調(diào)用getA是需要把實(shí)參復(fù)制一份,壓入getA函數(shù)的棧中(對于某些C++編譯器,getA函數(shù)的返回值也要拷貝一份放在棧中,在getA函數(shù)調(diào)用結(jié)束時(shí),參數(shù)出棧就會返回給調(diào)用者)。因此,在調(diào)用getA函數(shù)時(shí),需要構(gòu)造一個(gè)a的副本,調(diào)用一次拷貝構(gòu)造函數(shù),創(chuàng)建了一個(gè)臨時(shí)變量。
中間臨時(shí)對象的創(chuàng)建和銷毀是隱式的,因此如果中間臨時(shí)對象的創(chuàng)建和銷毀在循環(huán)內(nèi)或是對象構(gòu)造需要分配很多資源,會造成資源在短時(shí)間內(nèi)被頻繁的分配和釋放,甚至可能造成內(nèi)存泄露。
上述代碼getA函數(shù)的問題可以通過傳遞引用的方式解決,即getA(A& a),不用構(gòu)造參數(shù)的臨時(shí)對象。
實(shí)際的C++工程實(shí)踐中,會有大量其它類型的隱式臨時(shí)對象存在,如重載+和重載++等操作符,對對象進(jìn)行算術(shù)運(yùn)算時(shí)也會有臨時(shí)對象,操作符重載本質(zhì)上也是函數(shù),因此要盡量避免臨時(shí)對象的出現(xiàn)。
當(dāng)一個(gè)派生類實(shí)例化一個(gè)對象時(shí),會先構(gòu)造一個(gè)父類對象,同樣,在銷毀一個(gè)派生類對象時(shí)也會銷毀其父類對象。派生類對象的父類對象是隱含的對象,其生命周期和派生類對象綁定在一起。如果構(gòu)造父類對象的開銷很大,則所有子類的構(gòu)造都會開銷很大。
#include
#include
class A
{
public:
A()
{
printf("A Created\n");
}
~A()
{
printf("A Destroyed\n");
}
};
class B : public A
{
public:
B(): A()
{
printf("B Created\n");
}
~B()
{
printf("B Destroyed\n");
}
};
int main()
{
B b;
return 0;
}
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行結(jié)果如下:
A Created
B Created
B Destroyed
A Destroyed
根據(jù)運(yùn)行結(jié)果,創(chuàng)建派生類對象時(shí)會先創(chuàng)建隱含的父類對象,銷毀派生類對象時(shí)會在調(diào)用派生類析構(gòu)函數(shù)后調(diào)用父類的析構(gòu)函數(shù)。
C++對象的內(nèi)部結(jié)構(gòu)及實(shí)現(xiàn)和C++編譯器緊密相關(guān),不同的編譯器可能會有不同的實(shí)現(xiàn)方式。
在一個(gè)C++對象中包含成員數(shù)據(jù)和成員函數(shù),成員數(shù)據(jù)分為靜態(tài)成員數(shù)據(jù)和非靜態(tài)成員數(shù)據(jù);成員函數(shù)分為靜態(tài)成員函數(shù)、非靜態(tài)成員函數(shù)和虛函數(shù)。
#include
#include
class SimpleObject
{
public:
static int nCounter;
double value;
char flag;
SimpleObject()
{
printf("SimpleObject Created\n");
}
virtual ~SimpleObject()
{
printf("SimpleObject Destroyed\n");
}
double getValue()
{
return value;
}
static int getCount()
{
return nCounter;
}
virtual void test()
{
printf("virtual void test()\n");
}
};
int main()
{
SimpleObject object;
printf("Obejct start address: 0x%X\n", &object);
printf("Value address: 0x%X\n", &object.value);
printf("flag address: 0x%X\n", &object.flag);
printf("Object size: %d\n", sizeof(object));
return 0;
}
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行結(jié)果如下:
SimpleObject Created
Obejct start address: 0x5728F3F0
Value address: 0x5728F3F8
flag address: 0x5728F400
Object size: 24
SimpleObject Destroyed
上述代碼,靜態(tài)成員數(shù)據(jù)nCounter存儲在全局/靜態(tài)數(shù)據(jù)區(qū),由類的所有對象共享,并不作為對象占據(jù)的內(nèi)存的一部分,因此sizeof返回的SimpleObject大小并不包括nCounter所占據(jù)的內(nèi)存大小。非靜態(tài)成員數(shù)據(jù)value和flag存儲在對象占用的內(nèi)存中,不論時(shí)全局/靜態(tài)數(shù)據(jù)區(qū),還是堆上、棧上。Value是double類型,占據(jù)8個(gè)字節(jié)(64位),flag是char類型,占據(jù)1個(gè)字節(jié),但由于內(nèi)存對齊,也會占用8字節(jié)。
SimpleObject類對象的數(shù)據(jù)成員占用了16個(gè)字節(jié),剩下的8字節(jié)是與虛函數(shù)相關(guān)的。如果將兩個(gè)虛函數(shù)的virtual關(guān)鍵字去掉,則sizeof(SimpleObject)將得到16。
虛函數(shù)用于實(shí)現(xiàn)C++語言的動態(tài)綁定特性,為了實(shí)現(xiàn)動態(tài)綁定特性,C++編譯器遇到含有虛函數(shù)的類時(shí),會分配一個(gè)指針指向一個(gè)函數(shù)地址表,即虛函數(shù)表(virtual table),虛函數(shù)表指針占據(jù)了8個(gè)字節(jié),并且占據(jù)的是類實(shí)例內(nèi)存布局開始的8個(gè)字節(jié)。
C++簡單對象占用的內(nèi)存空間如下:
(1)非靜態(tài)成員數(shù)據(jù)是影響對象占用內(nèi)存大小的主要因素,隨著對象數(shù)目的增加,非靜態(tài)成員數(shù)據(jù)占用的內(nèi)存空間會相應(yīng)增加。
(2)所有的對象共享一份靜態(tài)成員數(shù)據(jù),因此靜態(tài)成員數(shù)據(jù)占用的內(nèi)存空間大小不會隨著對象數(shù)目的增加而增加。
(3)靜態(tài)成員函數(shù)和非靜態(tài)成員函數(shù)不會影響對象內(nèi)存的大小,雖然其實(shí)現(xiàn)會占用相應(yīng)的內(nèi)存空間,同樣不會隨著對象數(shù)目的增加而增加。
(4)如果類中包含虛函數(shù),類對象會包含一個(gè)指向虛函數(shù)表的指針,虛函數(shù)的地址會放在虛函數(shù)表中。
在虛函數(shù)表中,不一定完全是指向虛函數(shù)實(shí)現(xiàn)的指針。當(dāng)指定編譯器打開RTTI開關(guān)時(shí),虛函數(shù)表中的第一個(gè)指針指向的是一個(gè)typeinfo的結(jié)構(gòu),每個(gè)類只產(chǎn)生一個(gè)typeinfo結(jié)構(gòu)的實(shí)例,當(dāng)程序調(diào)用typeid()來獲取類的信息時(shí),實(shí)際是通過虛函數(shù)表中的第一個(gè)指針獲取typeinfo結(jié)構(gòu)體實(shí)例。
C++語言中,繼承分為單繼承和多繼承。
#include
#include
class SimpleObject
{
public:
static int nCounter;
double value;
char flag;
SimpleObject()
{
printf("SimpleObject Created\n");
}
virtual ~SimpleObject()
{
printf("SimpleObject Destroyed\n");
}
double getValue()
{
return value;
}
static int getCount()
{
return nCounter;
}
virtual void test()
{
printf("virtual void SimpleObject::test()\n");
}
};
int SimpleObject::nCounter = 0;
class DerivedObject : public SimpleObject
{
public:
double subValue;
DerivedObject()
{
printf("DerivedObject Created\n");
}
virtual ~DerivedObject()
{
printf("DerivedObject Destroyed\n");
}
virtual void test()
{
printf("virtual void DerivedObject::test()\n");
}
};
int main()
{
DerivedObject object;
printf("Obejct start address: 0x%X\n", &object);
printf("Value address: 0x%X\n", &object.value);
printf("flag address: 0x%X\n", &object.flag);
printf("subValue address: 0x%X\n", &object.subValue);
printf("SimpleObject size: %d\n"
"DerivedObject size: %d\n",
sizeof(SimpleObject),
sizeof(DerivedObject));
return 0;
}
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行結(jié)果如下:
SimpleObject Created
DerivedObject Created
Obejct start address: 0x96EA42D0
Value address: 0x96EA42D8
flag address: 0x96EA42E0
subValue address: 0x96EA42E8
SimpleObject size: 24
DerivedObject size: 32
DerivedObject Destroyed
SimpleObject Destroyed
根據(jù)上述輸出結(jié)果,構(gòu)造一個(gè)派生類實(shí)例時(shí)首先需要構(gòu)造一個(gè)基類的實(shí)例,基類實(shí)例在派生類實(shí)例銷毀后被銷毀。
SimpleObject類大小是24個(gè)字節(jié),DerivedObject類的大小是32個(gè)字節(jié),DerivedObject增加了一個(gè)double類型的成員數(shù)據(jù)subValue,需要占用8個(gè)字節(jié)。由于DerivedObject類也需要一個(gè)虛函數(shù)表,因此DerivedObject派生類與SimpleObject基類使用同一個(gè)虛函數(shù)表,DerivedObject派生類在構(gòu)造時(shí)不會再創(chuàng)建一個(gè)新的虛函數(shù)表,而是在SimpleObject基類的虛函數(shù)表中增加或修改,DerivedObject實(shí)例的虛函數(shù)表中會存儲DerivedObject相應(yīng)的虛函數(shù)實(shí)現(xiàn),如果DerivedObject沒有提供某個(gè)虛函數(shù)實(shí)現(xiàn),則存儲基類SimpleObject的虛函數(shù)實(shí)現(xiàn)。
C++語言提供多繼承的支持,多繼承中派生類可以有一個(gè)以上的基類。多繼承是C++語言中頗受爭議的一項(xiàng)特性,多繼承在提供強(qiáng)大功能的同時(shí)也帶來了容易造成錯(cuò)誤的諸多不便。因此,后續(xù)很多面向?qū)ο蟪绦蛟O(shè)計(jì)語言取消了多繼承支持,而是提供了更清晰的接口概念。
C++語言中仍然通過繼承實(shí)現(xiàn)接口,在面向接口的編程模型,如COM,都采用多繼承實(shí)現(xiàn)。如果需要開發(fā)一個(gè)文字處理軟件,要求有些文檔即可以打印有可以存儲,有些文檔只可以打印或存儲??紤]到程序的可擴(kuò)展性,比較好的設(shè)計(jì)是將打印和存儲分別定義為兩個(gè)接口,在接口中定義相應(yīng)的方法。當(dāng)一個(gè)類實(shí)現(xiàn)了打印和存儲接口時(shí),其對象即可以打印也可以存儲。如果只實(shí)現(xiàn)了打印或存儲,則只具備相應(yīng)的功能。
#include
#include
using namespace std;
class BaseA
{
public:
BaseA(int a)
{
m_a = a;
}
virtual void funcA()
{
cout << "BaseA::funcA()" <a <b <c <vptrA <vptrB <
上述代碼中,Derived類繼承自BaseA和BaseB類,funcA和funcB為虛函數(shù)。
Derived派生類對象的內(nèi)存模型如下:
創(chuàng)建派生類時(shí),首先需要?jiǎng)?chuàng)建基類的對象。由于多繼承一個(gè)派生類中有多個(gè)基類,因此,創(chuàng)建基類的對象時(shí)要遵循一定的順序,其順序由派生類聲明時(shí)決定,如果將Derived類的聲明修改為:class Derived : public BaseB, public BaseA
基類對象BaseB會被首先創(chuàng)建,BaseA對象其次被創(chuàng)建?;悓ο箐N毀的順序與創(chuàng)建的順序相反。
多繼承會引入很多復(fù)雜問題,菱形繼承時(shí)很典型的一種。菱形繼承示例代碼如下:
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "name: " << m_name
<< " age: " << m_age <name1 << endl;
cout << pTest->age1 << endl;
cout << pTest->research << endl;
cout << pTest->name2 << endl;
cout << pTest->age2 << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
return 0;
}
// output:
// Doctor size: 28
// Bauer_1
// 31
// Computer
// Bauer_2
// 32
// Computer Engneering
// HPC
上述代碼中,底層子類對象的內(nèi)存局部如下:
底層子類對象中,分別繼承了中間層父類從頂層父類繼承而來的成員變量,因此內(nèi)存模型中含有兩份底層父類的成員變量。
如果頂層父類含有虛函數(shù),中間層父類會分別繼承頂層父類的虛函數(shù)表指針,因此,底層子類對象內(nèi)存布局如下:
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
virtual void print()
{
cout << "name: " << m_name
<< " age: " << m_age <vptr1 << endl;
cout << pTest->name1 << endl;
cout << pTest->age1 << endl;
cout << pTest->research << endl;
cout << pTest->vptr2 << endl;
cout << pTest->name2 << endl;
cout << pTest->age2 << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
return 0;
}
// output:
// Doctor size: 28
// 0x405370
// Bauer_1
// 31
// Computer
// 0x40537c
// Bauer_2
// 32
// Computer Engneering
// HPC
虛繼承是解決C++多重繼承問題的一種手段,虛繼承的底層實(shí)現(xiàn)原理與C++編譯器相關(guān),一般通過虛基類指針和虛基類表實(shí)現(xiàn),每個(gè)虛繼承的子類都有一個(gè)虛基類指針(占用一個(gè)指針的存儲空間,4(8)字節(jié))和虛基類表(不占用類對象的存儲空間)(虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份);當(dāng)虛繼承的子類被當(dāng)做父類繼承時(shí),虛基類指針也會被繼承。
在虛繼承情況下,底層子類對象的布局不同于普通繼承,需要多出一個(gè)指向中間層父類對象的虛基類表指針vbptr。
vbptr是虛基類表指針(virtual base table pointer),vbptr指針指向一個(gè)虛基類表(virtual table),虛基類表存儲了虛基類相對直接繼承類的偏移地址;通過偏移地址可以找到虛基類成員,虛繼承不用像普通多繼承維持著公共基類(虛基類)的兩份同樣的拷貝,節(jié)省了存儲空間。
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "this: " << this <vbptr_left << endl;
cout << *(int*)pTest->vbptr_left << endl;
cout << pTest->research << endl;
cout << pTest->vbptr_right << endl;
cout << *(int*)pTest->vbptr_right << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
cout << pTest->name << endl;
cout << pTest->age << endl;
return 0;
}
// output:
// Doctor size: 28
// 0x40539c
// 12
// Computer
// 0x4053a8
// 0
// Computer Engneering
// HPC
// Bauer
// 30
上述代碼沒有虛函數(shù),在G++編譯器打印結(jié)果如上,底層子類對象的內(nèi)存布局如下:
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
virtual void print()
{
cout << "this: " << this <vbptr_left << endl;
cout << std::hex << *(int*)pTest->vbptr_left << endl;
cout << std::dec << *((int*)pTest->vbptr_left+8) << endl;
cout << std::dec << *((int*)pTest->vbptr_left+16) << endl;
cout << std::dec << *((int*)pTest->vbptr_left+24) << endl;
cout << pTest->research << endl;
cout << pTest->vbptr_right << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
cout << pTest->vptr_base << endl;
cout << pTest->name << endl;
cout << pTest->age << endl;
return 0;
}
上述代碼中,使用了虛繼承,因此不同的C++編譯器實(shí)現(xiàn)原理不同。
對于GCC編譯器,People對象大小為char* + int + 虛函數(shù)表指針
,Teacher對象大小為char*+虛基類表指針+A類型的大小
,Student對象大小為char*+虛基類表指針+A類型的大小
,Doctor對象大小為char* + int +虛函數(shù)表指針+char*+虛基類表指針+char*+虛基類表指針+char*
。中間層父類共享頂層父類的虛函數(shù)表指針,沒有自己的虛函數(shù)表指針,虛基類指針不共享,因此都有自己獨(dú)立的虛基類表指針。
VC++、GCC和Clang編譯器的實(shí)現(xiàn)中,不管是否是虛繼承還是有虛函數(shù),其虛基類指針都不共享,都是單獨(dú)的。對于虛函數(shù)表指針,VC++編譯器根據(jù)是否為虛繼承來判斷是否在繼承關(guān)系中共享虛表指針。如果子類是虛繼承擁有虛函數(shù)父類,且子類有新加的虛函數(shù)時(shí),子類中則會新加一個(gè)虛函數(shù)表指針;GCC編譯器和Clang編譯器的虛函數(shù)表指針在整個(gè)繼承關(guān)系中共享的。
G++編譯器對于類的內(nèi)存分布和虛函數(shù)表信息命令如下:
g++ -fdump-class-hierarchy main.cpp
cat main.cpp.002t.class
VC++編譯器對于類的內(nèi)存分布和虛函數(shù)表信息命令如下:cl main.cpp /d1reportSingleClassLayoutX
Clang編譯器對于類的內(nèi)存分布和虛函數(shù)表信息命令如下:clang -Xclang -fdump-record-layouts
C++標(biāo)準(zhǔn)規(guī)定,每個(gè)類都必須有構(gòu)造函數(shù),如果開發(fā)人員沒有定義,則C++編譯器會提供一個(gè)默認(rèn)的構(gòu)造函數(shù),默認(rèn)構(gòu)造函數(shù)不帶任何參數(shù),也不會對成員數(shù)據(jù)進(jìn)行初始化。如果類中定義了任何一種形式的構(gòu)造函數(shù),C++編譯器將不再生成默認(rèn)構(gòu)造函數(shù)。
除了構(gòu)造函數(shù),C++標(biāo)準(zhǔn)規(guī)定,每個(gè)類都必須有拷貝構(gòu)造函數(shù),如果開發(fā)人員沒有定義,則C++編譯器會提供一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù),默認(rèn)拷貝構(gòu)造函數(shù)是淺拷貝,即按照對象的內(nèi)存空間逐個(gè)字節(jié)進(jìn)行拷貝,因此默認(rèn)拷貝構(gòu)造函數(shù)會帶來隱含的內(nèi)存問題。
#include
#include
class SimpleObject
{
public:
int n;
SimpleObject(int n)
{
this->n = n;
buffer = new char[n];
printf("SimpleObject Created\n");
}
virtual ~SimpleObject()
{
if(buffer != NULL)
{
delete buffer;
printf("SimpleObject Destroyed\n");
}
}
private:
//SimpleObject(const SimpleObject& another);
private:
char* buffer;
};
int main()
{
SimpleObject a(10);
SimpleObject b = a;
printf("Object size: %d\n", a.n);
return 0;
}
在RHEL 7.3系統(tǒng)使用GCC編譯器編譯運(yùn)行時(shí)會異常退出。
SimpleObject在構(gòu)造時(shí)分配了n個(gè)字節(jié)的緩沖區(qū),在析構(gòu)時(shí)釋放緩沖區(qū)。但由于沒有定義拷貝構(gòu)造函數(shù),C++編譯器會提供一個(gè)淺拷貝的默認(rèn)拷貝構(gòu)造函數(shù),SimpleObject b = a語句會通過淺拷貝構(gòu)造一個(gè)SimpleObject對象b,對象b的buffer和對象a的buffer指向同一塊內(nèi)存空間,在對象a和對象b析構(gòu)時(shí),這塊內(nèi)存空間被釋放了兩次,造成程序崩潰。如果不想通過賦值或拷貝構(gòu)造函數(shù)構(gòu)造對象,可以將拷貝構(gòu)造函數(shù)定義為private,此時(shí)SimpleObject b = a;會在編譯時(shí)報(bào)錯(cuò)。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。