對(duì)象模型是面向?qū)ο蟪绦蛟O(shè)計(jì)語(yǔ)言的重要方面,會(huì)直接影響面向?qū)ο笳Z(yǔ)言編寫(xiě)程序的運(yùn)行機(jī)制以及對(duì)內(nèi)存的使用機(jī)制,因此了解對(duì)象模型是進(jìn)行程序性能優(yōu)化的基礎(chǔ)。只有深入理解C++對(duì)象模型,才能避免程序開(kāi)發(fā)過(guò)程中一些不易發(fā)現(xiàn)的內(nèi)存錯(cuò)誤,從而改善程序性能,提高程序質(zhì)量。
創(chuàng)新互聯(lián)公司是一家專(zhuān)業(yè)提供漠河企業(yè)網(wǎng)站建設(shè),專(zhuān)注與做網(wǎng)站、成都網(wǎng)站制作、H5響應(yīng)式網(wǎng)站、小程序制作等業(yè)務(wù)。10年已為漠河眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專(zhuān)業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進(jìn)行中。
通常,計(jì)算機(jī)程序由代碼和數(shù)據(jù)組成,因此代碼和數(shù)據(jù)也是影響程序所需內(nèi)存的主要因素。代碼是程序運(yùn)行的指令,比如數(shù)學(xué)運(yùn)算、比較、跳轉(zhuǎn)以及函數(shù)調(diào)用,其大小通常由程序的功能和復(fù)雜度決定,正確地使用程序編寫(xiě)技巧以及編程語(yǔ)言的特性可以?xún)?yōu)化所生成的代碼的大??;數(shù)據(jù)是代碼要處理的對(duì)象。
程序占用的內(nèi)存區(qū)通常分為五種:全局/靜態(tài)數(shù)據(jù)區(qū)、常量數(shù)據(jù)區(qū)、代碼區(qū)、棧、堆。
程序的代碼存儲(chǔ)在代碼區(qū)中,而程序的數(shù)據(jù)則根據(jù)數(shù)據(jù)種類(lèi)的不同存儲(chǔ)在不同的內(nèi)存區(qū)中。C++語(yǔ)言中,數(shù)據(jù)有不同的分類(lèi)方法,例如常量和變量,全局?jǐn)?shù)據(jù)和局部數(shù)據(jù),靜態(tài)數(shù)據(jù)和非靜態(tài)數(shù)據(jù)。此外,程序運(yùn)行過(guò)程中動(dòng)態(tài)產(chǎn)生和釋放的數(shù)據(jù)也要存放在不同的內(nèi)存區(qū)。
不同內(nèi)存區(qū)存儲(chǔ)的數(shù)據(jù)如下:
(1)全局/靜態(tài)數(shù)據(jù)區(qū)存儲(chǔ)全局變量以及靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)。
(2)常量數(shù)據(jù)區(qū)存儲(chǔ)程序中的常量字符串等。
(3)棧中存儲(chǔ)自動(dòng)變量或者局部變量,以及傳遞函數(shù)參數(shù)等,而堆是用戶(hù)程序控制的存儲(chǔ)區(qū),存儲(chǔ)動(dòng)態(tài)產(chǎn)生的數(shù)據(jù)。
不同類(lèi)型的數(shù)據(jù)在內(nèi)存存儲(chǔ)位置的示例如下:
#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é)對(duì)齊
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)變量存儲(chǔ)在全局/靜態(tài)數(shù)據(jù)區(qū)。
字符串常量存儲(chǔ)在常量數(shù)據(jù)區(qū),pLocalString1指向的字符串"pLocalString1"的長(zhǎng)度是13字節(jié),加上結(jié)束符’\0’,共計(jì)14個(gè)字節(jié),存儲(chǔ)在0x4009a0開(kāi)始的14個(gè)字節(jié)內(nèi)存空間;存儲(chǔ)pLocalString2的字符串"pLocalString2"時(shí),從0x4009ae地址開(kāi)始,因此,沒(méi)有進(jìn)行內(nèi)存對(duì)齊處理。程序中的其它字符串常量,如printf中的格式化串通常也存儲(chǔ)在常量數(shù)據(jù)區(qū)。
通過(guò)new、malloc獲得的內(nèi)存是堆的內(nèi)存。通過(guò)new申請(qǐng)5個(gè)int所需的內(nèi)存,但由于內(nèi)存邊界需要字節(jié)對(duì)齊(堆上分配內(nèi)存時(shí)按16字節(jié)對(duì)齊),因此申請(qǐng)5個(gè)int共計(jì)20個(gè)字節(jié),但占據(jù)32字節(jié)的內(nèi)存。通過(guò)malloc申請(qǐng)1個(gè)字節(jié)的內(nèi)存,申請(qǐng)1個(gè)字節(jié)時(shí)會(huì)從32字節(jié)后開(kāi)始分配。
內(nèi)存對(duì)齊雖然會(huì)浪費(fèi)部分內(nèi)存,但由于CPU在對(duì)齊方式下運(yùn)行較快,因此內(nèi)存對(duì)齊對(duì)于程序性能是有益的。C++語(yǔ)言中struct、union、class在編譯時(shí)也會(huì)對(duì)成員變量進(jìn)行內(nèi)存對(duì)齊處理,開(kāi)發(fā)人員可以使用#progma pack()或者編譯器的編譯選項(xiàng)來(lái)控制對(duì)struct、union、class的成員變量按多少字節(jié)對(duì)齊,或者關(guān)閉對(duì)齊。
全局/靜態(tài)存儲(chǔ)區(qū)、常量數(shù)據(jù)區(qū)在程序編譯階段已經(jīng)分配好,在整個(gè)程序運(yùn)行過(guò)程中始終存在,用于存儲(chǔ)全局變量、靜態(tài)變量,以及字符串常量等。其中字符串常量存儲(chǔ)的區(qū)域是不可修改的內(nèi)存區(qū)域,試圖修改字符串常量會(huì)導(dǎo)致程序異常退出。
char* pLocalString1 = "hello world";
pLocalString1[0] = 'H';// 試圖修改不可修改的內(nèi)存區(qū)
全局/靜態(tài)數(shù)據(jù)區(qū)除了全局變量,還有靜態(tài)變量。C語(yǔ)言中可以定義靜態(tài)變量,靜態(tài)變量在第一次進(jìn)入作用域時(shí)被初始化,后續(xù)再次進(jìn)入作用域時(shí)不必初始化。C++語(yǔ)言中,可以定義靜態(tài)變量,也可以定義類(lèi)的靜態(tài)成員變量,類(lèi)的靜態(tài)成員變量用來(lái)在類(lèi)的多個(gè)對(duì)象間共享數(shù)據(jù)。類(lèi)的靜態(tài)成員變量存儲(chǔ)在全局/靜態(tài)數(shù)據(jù)區(qū),并且只有一份拷貝,由類(lèi)的所有對(duì)象共享。如果通過(guò)全局變量在類(lèi)的多個(gè)對(duì)象間共享數(shù)據(jù)則會(huì)破壞類(lèi)的封裝性。
#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;
}
上述代碼,類(lèi)A定義了一個(gè)靜態(tài)成員變量nCounter用于對(duì)類(lèi)A的對(duì)象進(jìn)行計(jì)數(shù),類(lèi)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
對(duì)象a和對(duì)象b中的value成員變量的地址不同,而靜態(tài)成員變量nCounter的地址相同。類(lèi)A的每一個(gè)對(duì)象會(huì)有自己的value存儲(chǔ)空間,在棧上分配;類(lèi)A的所有對(duì)象共享一個(gè)nCounter的存儲(chǔ)空間,在全局/靜態(tài)數(shù)據(jù)區(qū)分配。
在C/C++語(yǔ)言中,當(dāng)開(kāi)發(fā)人員在函數(shù)內(nèi)部定義一個(gè)變量,或者向某個(gè)函數(shù)傳遞參數(shù)時(shí),變量和參數(shù)存儲(chǔ)在棧中。當(dāng)退出變量的作用域時(shí),棧上的存儲(chǔ)單元會(huì)被自動(dòng)釋放。當(dāng)開(kāi)發(fā)人員通過(guò)malloc申請(qǐng)一塊內(nèi)存或使用new創(chuàng)建一個(gè)對(duì)象時(shí),申請(qǐng)的內(nèi)存或?qū)ο笏嫉膬?nèi)存在堆上分配。開(kā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é)對(duì)齊
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;
}
上述代碼中,通過(guò)new在堆上申請(qǐng)5個(gè)int的所需的內(nèi)存空間,將獲得的地址記錄在棧上的變量pNew中;通過(guò)malloc在堆上申請(qǐng)1字節(jié)的內(nèi)存空間,將獲得的地址記錄在棧上的變量pMalloc中。
int* pNew = new int[5]; // 16字節(jié)對(duì)齊
char* pMalloc = (char*)malloc(1);
在main函數(shù)結(jié)束時(shí),pNew和pMalloc自身是棧上的內(nèi)存單元,會(huì)被自動(dòng)釋放,但pNew和pMalloc所指向的內(nèi)存是堆上的,雖然指向堆空間的pNew和pMalloc指針變量已經(jīng)不存在,但相應(yīng)的堆空間內(nèi)存不會(huì)被自動(dòng)釋放,造成內(nèi)存泄露。通過(guò)new申請(qǐng)的堆內(nèi)存空間需要使用delete進(jìn)行釋放,使用malloc獲得的堆空間內(nèi)存需要使用free進(jìn)行釋放。
既然棧上的內(nèi)存空間不存內(nèi)存泄露的問(wèn)題,而堆上的內(nèi)存容易引起內(nèi)存泄露,為什么要使用堆上的內(nèi)存呢?因?yàn)楹芏鄳?yīng)用程序需要?jiǎng)討B(tài)管理地管理數(shù)據(jù)。此外,棧的大小有限制,占用內(nèi)存較多的對(duì)象或數(shù)據(jù)只能分配在堆空間。
棧和堆的區(qū)別如下:
(1)大小
通常,程序使用棧的大小是固定的,由編譯器決定,開(kāi)發(fā)人員可以通過(guò)編譯器選項(xiàng)指定棧的大小,但通常棧都不會(huì)太大。
#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)行,程序會(huì)運(yùn)行時(shí)出錯(cuò),原因是棧溢出。
堆的大小通常只受限于系統(tǒng)有效的虛擬內(nèi)存的大小,因此可以用來(lái)分配創(chuàng)建一些占用內(nèi)存較大的對(duì)象或數(shù)據(jù)。
(2)效率
棧上的內(nèi)存是系統(tǒng)自動(dòng)分配的,壓棧和出棧都有相應(yīng)的指令進(jìn)行操作,因此效率較高,并且分配的內(nèi)存空間是連續(xù)的,不會(huì)產(chǎn)生內(nèi)存碎片;堆上的內(nèi)存是由開(kāi)發(fā)人員來(lái)動(dòng)態(tài)分配和回收的。當(dāng)開(kāi)發(fā)人員通過(guò)new或malloc申請(qǐng)堆上的內(nèi)存空間時(shí),系統(tǒng)需要按照一定的算法在堆空間中尋找合適大小的空閑堆,并修改相應(yīng)的維護(hù)堆空閑空間的鏈表,然后返回地址給程序。因此,效率幣棧要低,此外還容易產(chǎn)生內(nèi)存碎片。
如果程序在堆上申請(qǐng)5個(gè)100字節(jié)大小的內(nèi)存塊,然后釋放其中不連續(xù)的兩個(gè)內(nèi)存塊,此時(shí)當(dāng)需要在堆上申請(qǐng)一個(gè)150字節(jié)大小的內(nèi)存塊時(shí),則無(wú)法充分利用剛剛釋放的兩個(gè)小內(nèi)存塊。由此可見(jiàn),連續(xù)創(chuàng)建和刪除占用內(nèi)存較小的對(duì)象或數(shù)據(jù)時(shí),很容易在堆上造成內(nèi)存碎片,使得內(nèi)存的使用效率降低。
從C++對(duì)象模型角度看,對(duì)象就是內(nèi)存中的一塊區(qū)域。根據(jù)C++標(biāo)準(zhǔn),一個(gè)對(duì)象可以通過(guò)定義變量創(chuàng)建,或者通過(guò)new操作符創(chuàng)建,或者通過(guò)實(shí)現(xiàn)來(lái)創(chuàng)建。如果一個(gè)對(duì)象通過(guò)定義在某個(gè)函數(shù)內(nèi)的變量或者需要的臨時(shí)變量來(lái)創(chuàng)建,是棧上的一個(gè)對(duì)象;如果一個(gè)對(duì)象是定義在全局范圍內(nèi)的變量,則對(duì)象存儲(chǔ)在全局/靜態(tài)數(shù)據(jù)區(qū);如果一個(gè)對(duì)象通過(guò)new操作符創(chuàng)建,存儲(chǔ)在堆空間。
對(duì)面向?qū)ο蟮腃++程序設(shè)計(jì),程序運(yùn)行過(guò)程中的大部分?jǐn)?shù)據(jù)應(yīng)該封裝在對(duì)象中,而程序的行為也由對(duì)象的行為決定。因此,深入理解C++對(duì)象的內(nèi)部結(jié)構(gòu),從而正確地設(shè)計(jì)和使用對(duì)象,對(duì)于設(shè)計(jì)開(kāi)發(fā)高性能的C++程序很重要。
對(duì)象的生命周期是指對(duì)象從創(chuàng)建到銷(xiāo)毀的過(guò)程,創(chuàng)建對(duì)象時(shí)要占用一定的內(nèi)存空間,而對(duì)象要銷(xiāo)毀后要釋放對(duì)應(yīng)的內(nèi)存空間,因此整個(gè)程序占用的內(nèi)存空間也會(huì)隨著對(duì)象的創(chuàng)建和銷(xiāo)毀而動(dòng)態(tài)的發(fā)生變化。深入理解對(duì)象的生命周期會(huì)幫助分析程序?qū)?nèi)存的消耗情況,從而找到改進(jìn)方法。
對(duì)象的創(chuàng)建有三種方式,不同方式所創(chuàng)建對(duì)象的生命周期各有不同,創(chuàng)建對(duì)象的三種方式如下:
(1)通過(guò)定義變量創(chuàng)建對(duì)象
(2)通過(guò)new操作符創(chuàng)建對(duì)象
(3)通過(guò)實(shí)現(xiàn)創(chuàng)建對(duì)象
通過(guò)定義變量創(chuàng)建對(duì)象時(shí),變量的作用域決定了對(duì)象的生命周期。當(dāng)進(jìn)入變量的作用域時(shí),對(duì)象被創(chuàng)建;退出變量的作用域時(shí),對(duì)象被銷(xiāo)毀。全局變量的作用域時(shí)整個(gè)程序,被聲明為全局對(duì)象的變量在程序調(diào)用main函數(shù)前被創(chuàng)建,當(dāng)程序退出main函數(shù)后,全局對(duì)象才會(huì)被銷(xiāo)毀。靜態(tài)變量作用域不是整個(gè)程序,但靜態(tài)變量存儲(chǔ)在全局/靜態(tài)數(shù)據(jù)區(qū),在程序開(kāi)始時(shí)已經(jīng)分配好,因此聲明為靜態(tài)變量的對(duì)象在第一次進(jìn)入作用域時(shí)會(huì)被創(chuàng)建,直到程序退出時(shí)被銷(xiāo)毀。
#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的全局對(duì)象globalA,一個(gè)A的局部對(duì)象localA,一個(gè)B的靜態(tài)局部對(duì)象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é)果,全局對(duì)象globalA在main函數(shù)開(kāi)始前被創(chuàng)建,在main函數(shù)退出后被銷(xiāo)毀;靜態(tài)對(duì)象localB在第一次進(jìn)入作用域時(shí)被創(chuàng)建,在main函數(shù)退出后被銷(xiāo)毀,如果程序從來(lái)沒(méi)有進(jìn)入到其作用域,則靜態(tài)對(duì)象不會(huì)被創(chuàng)建;局部對(duì)象在進(jìn)入作用域時(shí)被創(chuàng)建,在退出作用域時(shí)被銷(xiāo)毀。
通過(guò)new創(chuàng)建的對(duì)象會(huì)一直存在,直到被delete銷(xiāo)毀。即使指向?qū)ο蟮闹羔槺讳N(xiāo)毀,但還沒(méi)有調(diào)用delete,對(duì)象仍然會(huì)一直存在,占據(jù)這堆空間,直到程序退出,因此會(huì)造成內(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對(duì)象,并將返回地址記錄在pA指針變量中;然后再次使用createA函數(shù)創(chuàng)建了一個(gè)A對(duì)象,將返回地址記錄在pA指針變量中,此時(shí)pA指針將指向第二次創(chuàng)建的A對(duì)象,第一次創(chuàng)建的A對(duì)象已經(jīng)沒(méi)有指針指向。使用deleteA銷(xiāo)毀對(duì)象時(shí),銷(xiāo)毀的是第二次創(chuàng)建的A對(duì)象,第一次創(chuàng)建的A對(duì)象會(huì)一直存在,直到程序退出,并且即使在程序退出時(shí),第一次創(chuàng)建的A對(duì)象的析構(gòu)函數(shù)仍然不會(huì)被調(diào)用,最終造成內(nèi)存泄露。
通過(guò)實(shí)現(xiàn)創(chuàng)建對(duì)象通常是指一些隱藏的中間臨時(shí)變量的創(chuàng)建和銷(xiāo)毀。中間臨時(shí)變量的生命周期很短,不易被開(kāi)發(fā)人員察覺(jué),通常是造成性能下降的瓶頸,特別是占用內(nèi)存多、創(chuàng)建速度慢的對(duì)象。
中間臨時(shí)對(duì)象通常是通過(guò)拷貝構(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ù)和返回值都是通過(guò)值傳遞的,在調(diào)用getA是需要把實(shí)參復(fù)制一份,壓入getA函數(shù)的棧中(對(duì)于某些C++編譯器,getA函數(shù)的返回值也要拷貝一份放在棧中,在getA函數(shù)調(diào)用結(jié)束時(shí),參數(shù)出棧就會(huì)返回給調(diào)用者)。因此,在調(diào)用getA函數(shù)時(shí),需要構(gòu)造一個(gè)a的副本,調(diào)用一次拷貝構(gòu)造函數(shù),創(chuàng)建了一個(gè)臨時(shí)變量。
中間臨時(shí)對(duì)象的創(chuàng)建和銷(xiāo)毀是隱式的,因此如果中間臨時(shí)對(duì)象的創(chuàng)建和銷(xiāo)毀在循環(huán)內(nèi)或是對(duì)象構(gòu)造需要分配很多資源,會(huì)造成資源在短時(shí)間內(nèi)被頻繁的分配和釋放,甚至可能造成內(nèi)存泄露。
上述代碼getA函數(shù)的問(wèn)題可以通過(guò)傳遞引用的方式解決,即getA(A& a),不用構(gòu)造參數(shù)的臨時(shí)對(duì)象。
實(shí)際的C++工程實(shí)踐中,會(huì)有大量其它類(lèi)型的隱式臨時(shí)對(duì)象存在,如重載+和重載++等操作符,對(duì)對(duì)象進(jìn)行算術(shù)運(yùn)算時(shí)也會(huì)有臨時(shí)對(duì)象,操作符重載本質(zhì)上也是函數(shù),因此要盡量避免臨時(shí)對(duì)象的出現(xiàn)。
當(dāng)一個(gè)派生類(lèi)實(shí)例化一個(gè)對(duì)象時(shí),會(huì)先構(gòu)造一個(gè)父類(lèi)對(duì)象,同樣,在銷(xiāo)毀一個(gè)派生類(lèi)對(duì)象時(shí)也會(huì)銷(xiāo)毀其父類(lèi)對(duì)象。派生類(lèi)對(duì)象的父類(lèi)對(duì)象是隱含的對(duì)象,其生命周期和派生類(lèi)對(duì)象綁定在一起。如果構(gòu)造父類(lèi)對(duì)象的開(kāi)銷(xiāo)很大,則所有子類(lèi)的構(gòu)造都會(huì)開(kāi)銷(xiāo)很大。
#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)建派生類(lèi)對(duì)象時(shí)會(huì)先創(chuàng)建隱含的父類(lèi)對(duì)象,銷(xiāo)毀派生類(lèi)對(duì)象時(shí)會(huì)在調(diào)用派生類(lèi)析構(gòu)函數(shù)后調(diào)用父類(lèi)的析構(gòu)函數(shù)。
C++對(duì)象的內(nèi)部結(jié)構(gòu)及實(shí)現(xiàn)和C++編譯器緊密相關(guān),不同的編譯器可能會(huì)有不同的實(shí)現(xiàn)方式。
在一個(gè)C++對(duì)象中包含成員數(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存儲(chǔ)在全局/靜態(tài)數(shù)據(jù)區(qū),由類(lèi)的所有對(duì)象共享,并不作為對(duì)象占據(jù)的內(nèi)存的一部分,因此sizeof返回的SimpleObject大小并不包括nCounter所占據(jù)的內(nèi)存大小。非靜態(tài)成員數(shù)據(jù)value和flag存儲(chǔ)在對(duì)象占用的內(nèi)存中,不論時(shí)全局/靜態(tài)數(shù)據(jù)區(qū),還是堆上、棧上。Value是double類(lèi)型,占據(jù)8個(gè)字節(jié)(64位),flag是char類(lèi)型,占據(jù)1個(gè)字節(jié),但由于內(nèi)存對(duì)齊,也會(huì)占用8字節(jié)。
SimpleObject類(lèi)對(duì)象的數(shù)據(jù)成員占用了16個(gè)字節(jié),剩下的8字節(jié)是與虛函數(shù)相關(guān)的。如果將兩個(gè)虛函數(shù)的virtual關(guān)鍵字去掉,則sizeof(SimpleObject)將得到16。
虛函數(shù)用于實(shí)現(xiàn)C++語(yǔ)言的動(dòng)態(tài)綁定特性,為了實(shí)現(xiàn)動(dòng)態(tài)綁定特性,C++編譯器遇到含有虛函數(shù)的類(lèi)時(shí),會(huì)分配一個(gè)指針指向一個(gè)函數(shù)地址表,即虛函數(shù)表(virtual table),虛函數(shù)表指針占據(jù)了8個(gè)字節(jié),并且占據(jù)的是類(lèi)實(shí)例內(nèi)存布局開(kāi)始的8個(gè)字節(jié)。
C++簡(jiǎn)單對(duì)象占用的內(nèi)存空間如下:
(1)非靜態(tài)成員數(shù)據(jù)是影響對(duì)象占用內(nèi)存大小的主要因素,隨著對(duì)象數(shù)目的增加,非靜態(tài)成員數(shù)據(jù)占用的內(nèi)存空間會(huì)相應(yīng)增加。
(2)所有的對(duì)象共享一份靜態(tài)成員數(shù)據(jù),因此靜態(tài)成員數(shù)據(jù)占用的內(nèi)存空間大小不會(huì)隨著對(duì)象數(shù)目的增加而增加。
(3)靜態(tài)成員函數(shù)和非靜態(tài)成員函數(shù)不會(huì)影響對(duì)象內(nèi)存的大小,雖然其實(shí)現(xiàn)會(huì)占用相應(yīng)的內(nèi)存空間,同樣不會(huì)隨著對(duì)象數(shù)目的增加而增加。
(4)如果類(lèi)中包含虛函數(shù),類(lèi)對(duì)象會(huì)包含一個(gè)指向虛函數(shù)表的指針,虛函數(shù)的地址會(huì)放在虛函數(shù)表中。
在虛函數(shù)表中,不一定完全是指向虛函數(shù)實(shí)現(xiàn)的指針。當(dāng)指定編譯器打開(kāi)RTTI開(kāi)關(guān)時(shí),虛函數(shù)表中的第一個(gè)指針指向的是一個(gè)typeinfo的結(jié)構(gòu),每個(gè)類(lèi)只產(chǎn)生一個(gè)typeinfo結(jié)構(gòu)的實(shí)例,當(dāng)程序調(diào)用typeid()來(lái)獲取類(lèi)的信息時(shí),實(shí)際是通過(guò)虛函數(shù)表中的第一個(gè)指針獲取typeinfo結(jié)構(gòu)體實(shí)例。
C++語(yǔ)言中,繼承分為單繼承和多繼承。
#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è)派生類(lèi)實(shí)例時(shí)首先需要構(gòu)造一個(gè)基類(lèi)的實(shí)例,基類(lèi)實(shí)例在派生類(lèi)實(shí)例銷(xiāo)毀后被銷(xiāo)毀。
SimpleObject類(lèi)大小是24個(gè)字節(jié),DerivedObject類(lèi)的大小是32個(gè)字節(jié),DerivedObject增加了一個(gè)double類(lèi)型的成員數(shù)據(jù)subValue,需要占用8個(gè)字節(jié)。由于DerivedObject類(lèi)也需要一個(gè)虛函數(shù)表,因此DerivedObject派生類(lèi)與SimpleObject基類(lèi)使用同一個(gè)虛函數(shù)表,DerivedObject派生類(lèi)在構(gòu)造時(shí)不會(huì)再創(chuàng)建一個(gè)新的虛函數(shù)表,而是在SimpleObject基類(lèi)的虛函數(shù)表中增加或修改,DerivedObject實(shí)例的虛函數(shù)表中會(huì)存儲(chǔ)DerivedObject相應(yīng)的虛函數(shù)實(shí)現(xiàn),如果DerivedObject沒(méi)有提供某個(gè)虛函數(shù)實(shí)現(xiàn),則存儲(chǔ)基類(lèi)SimpleObject的虛函數(shù)實(shí)現(xiàn)。
C++語(yǔ)言提供多繼承的支持,多繼承中派生類(lèi)可以有一個(gè)以上的基類(lèi)。多繼承是C++語(yǔ)言中頗受爭(zhēng)議的一項(xiàng)特性,多繼承在提供強(qiáng)大功能的同時(shí)也帶來(lái)了容易造成錯(cuò)誤的諸多不便。因此,后續(xù)很多面向?qū)ο蟪绦蛟O(shè)計(jì)語(yǔ)言取消了多繼承支持,而是提供了更清晰的接口概念。
C++語(yǔ)言中仍然通過(guò)繼承實(shí)現(xiàn)接口,在面向接口的編程模型,如COM,都采用多繼承實(shí)現(xiàn)。如果需要開(kāi)發(fā)一個(gè)文字處理軟件,要求有些文檔即可以打印有可以存儲(chǔ),有些文檔只可以打印或存儲(chǔ)??紤]到程序的可擴(kuò)展性,比較好的設(shè)計(jì)是將打印和存儲(chǔ)分別定義為兩個(gè)接口,在接口中定義相應(yīng)的方法。當(dāng)一個(gè)類(lèi)實(shí)現(xiàn)了打印和存儲(chǔ)接口時(shí),其對(duì)象即可以打印也可以存儲(chǔ)。如果只實(shí)現(xiàn)了打印或存儲(chǔ),則只具備相應(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類(lèi)繼承自BaseA和BaseB類(lèi),funcA和funcB為虛函數(shù)。
Derived派生類(lèi)對(duì)象的內(nèi)存模型如下:
創(chuàng)建派生類(lèi)時(shí),首先需要?jiǎng)?chuàng)建基類(lèi)的對(duì)象。由于多繼承一個(gè)派生類(lèi)中有多個(gè)基類(lèi),因此,創(chuàng)建基類(lèi)的對(duì)象時(shí)要遵循一定的順序,其順序由派生類(lèi)聲明時(shí)決定,如果將Derived類(lèi)的聲明修改為:class Derived : public BaseB, public BaseA
基類(lèi)對(duì)象BaseB會(huì)被首先創(chuàng)建,BaseA對(duì)象其次被創(chuàng)建。基類(lèi)對(duì)象銷(xiāo)毀的順序與創(chuàng)建的順序相反。
多繼承會(huì)引入很多復(fù)雜問(wèn)題,菱形繼承時(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
上述代碼中,底層子類(lèi)對(duì)象的內(nèi)存局部如下:
底層子類(lèi)對(duì)象中,分別繼承了中間層父類(lèi)從頂層父類(lèi)繼承而來(lái)的成員變量,因此內(nèi)存模型中含有兩份底層父類(lèi)的成員變量。
如果頂層父類(lèi)含有虛函數(shù),中間層父類(lèi)會(huì)分別繼承頂層父類(lèi)的虛函數(shù)表指針,因此,底層子類(lèi)對(duì)象內(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++多重繼承問(wèn)題的一種手段,虛繼承的底層實(shí)現(xiàn)原理與C++編譯器相關(guān),一般通過(guò)虛基類(lèi)指針和虛基類(lèi)表實(shí)現(xiàn),每個(gè)虛繼承的子類(lèi)都有一個(gè)虛基類(lèi)指針(占用一個(gè)指針的存儲(chǔ)空間,4(8)字節(jié))和虛基類(lèi)表(不占用類(lèi)對(duì)象的存儲(chǔ)空間)(虛基類(lèi)依舊會(huì)在子類(lèi)里面存在拷貝,只是僅僅最多存在一份);當(dāng)虛繼承的子類(lèi)被當(dāng)做父類(lèi)繼承時(shí),虛基類(lèi)指針也會(huì)被繼承。
在虛繼承情況下,底層子類(lèi)對(duì)象的布局不同于普通繼承,需要多出一個(gè)指向中間層父類(lèi)對(duì)象的虛基類(lèi)表指針vbptr。
vbptr是虛基類(lèi)表指針(virtual base table pointer),vbptr指針指向一個(gè)虛基類(lèi)表(virtual table),虛基類(lèi)表存儲(chǔ)了虛基類(lèi)相對(duì)直接繼承類(lèi)的偏移地址;通過(guò)偏移地址可以找到虛基類(lèi)成員,虛繼承不用像普通多繼承維持著公共基類(lèi)(虛基類(lèi))的兩份同樣的拷貝,節(jié)省了存儲(chǔ)空間。
#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
上述代碼沒(méi)有虛函數(shù),在G++編譯器打印結(jié)果如上,底層子類(lèi)對(duì)象的內(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)原理不同。
對(duì)于GCC編譯器,People對(duì)象大小為char* + int + 虛函數(shù)表指針
,Teacher對(duì)象大小為char*+虛基類(lèi)表指針+A類(lèi)型的大小
,Student對(duì)象大小為char*+虛基類(lèi)表指針+A類(lèi)型的大小
,Doctor對(duì)象大小為char* + int +虛函數(shù)表指針+char*+虛基類(lèi)表指針+char*+虛基類(lèi)表指針+char*
。中間層父類(lèi)共享頂層父類(lèi)的虛函數(shù)表指針,沒(méi)有自己的虛函數(shù)表指針,虛基類(lèi)指針不共享,因此都有自己獨(dú)立的虛基類(lèi)表指針。
VC++、GCC和Clang編譯器的實(shí)現(xiàn)中,不管是否是虛繼承還是有虛函數(shù),其虛基類(lèi)指針都不共享,都是單獨(dú)的。對(duì)于虛函數(shù)表指針,VC++編譯器根據(jù)是否為虛繼承來(lái)判斷是否在繼承關(guān)系中共享虛表指針。如果子類(lèi)是虛繼承擁有虛函數(shù)父類(lèi),且子類(lèi)有新加的虛函數(shù)時(shí),子類(lèi)中則會(huì)新加一個(gè)虛函數(shù)表指針;GCC編譯器和Clang編譯器的虛函數(shù)表指針在整個(gè)繼承關(guān)系中共享的。
G++編譯器對(duì)于類(lèi)的內(nèi)存分布和虛函數(shù)表信息命令如下:
g++ -fdump-class-hierarchy main.cpp
cat main.cpp.002t.class
VC++編譯器對(duì)于類(lèi)的內(nèi)存分布和虛函數(shù)表信息命令如下:cl main.cpp /d1reportSingleClassLayoutX
Clang編譯器對(duì)于類(lèi)的內(nèi)存分布和虛函數(shù)表信息命令如下:clang -Xclang -fdump-record-layouts
C++標(biāo)準(zhǔn)規(guī)定,每個(gè)類(lèi)都必須有構(gòu)造函數(shù),如果開(kāi)發(fā)人員沒(méi)有定義,則C++編譯器會(huì)提供一個(gè)默認(rèn)的構(gòu)造函數(shù),默認(rèn)構(gòu)造函數(shù)不帶任何參數(shù),也不會(huì)對(duì)成員數(shù)據(jù)進(jìn)行初始化。如果類(lèi)中定義了任何一種形式的構(gòu)造函數(shù),C++編譯器將不再生成默認(rèn)構(gòu)造函數(shù)。
除了構(gòu)造函數(shù),C++標(biāo)準(zhǔn)規(guī)定,每個(gè)類(lèi)都必須有拷貝構(gòu)造函數(shù),如果開(kāi)發(fā)人員沒(méi)有定義,則C++編譯器會(huì)提供一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù),默認(rèn)拷貝構(gòu)造函數(shù)是淺拷貝,即按照對(duì)象的內(nèi)存空間逐個(gè)字節(jié)進(jìn)行拷貝,因此默認(rèn)拷貝構(gòu)造函數(shù)會(huì)帶來(lái)隱含的內(nèi)存問(wèn)題。
#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í)會(huì)異常退出。
SimpleObject在構(gòu)造時(shí)分配了n個(gè)字節(jié)的緩沖區(qū),在析構(gòu)時(shí)釋放緩沖區(qū)。但由于沒(méi)有定義拷貝構(gòu)造函數(shù),C++編譯器會(huì)提供一個(gè)淺拷貝的默認(rèn)拷貝構(gòu)造函數(shù),SimpleObject b = a語(yǔ)句會(huì)通過(guò)淺拷貝構(gòu)造一個(gè)SimpleObject對(duì)象b,對(duì)象b的buffer和對(duì)象a的buffer指向同一塊內(nèi)存空間,在對(duì)象a和對(duì)象b析構(gòu)時(shí),這塊內(nèi)存空間被釋放了兩次,造成程序崩潰。如果不想通過(guò)賦值或拷貝構(gòu)造函數(shù)構(gòu)造對(duì)象,可以將拷貝構(gòu)造函數(shù)定義為private,此時(shí)SimpleObject b = a;會(huì)在編譯時(shí)報(bào)錯(cuò)。