溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型

發(fā)布時間:2020-06-24 21:08:59 來源:網(wǎng)絡(luò) 閱讀:897 作者:天山老妖S 欄目:編程語言

C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型

一、C++對象模型與性能優(yōu)化

對象模型是面向?qū)ο蟪绦蛟O(shè)計語言的重要方面,會直接影響面向?qū)ο笳Z言編寫程序的運(yùn)行機(jī)制以及對內(nèi)存的使用機(jī)制,因此了解對象模型是進(jìn)行程序性能優(yōu)化的基礎(chǔ)。只有深入理解C++對象模型,才能避免程序開發(fā)過程中一些不易發(fā)現(xiàn)的內(nèi)存錯誤,從而改善程序性能,提高程序質(zhì)量。

二、C++程序的內(nèi)存分布

1、程序內(nèi)存分布簡介

通常,計算機(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 <stdio.h>
#include <stdlib.h>

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個變量,一個全局變量GolbalVariable,一個靜態(tài)局部變量staticLocalVariable,六個局部變量。在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’,共計14個字節(jié),存儲在0x4009a0開始的14個字節(jié)內(nèi)存空間;存儲pLocalString2的字符串"pLocalString2"時,從0x4009ae地址開始,因此,沒有進(jìn)行內(nèi)存對齊處理。程序中的其它字符串常量,如printf中的格式化串通常也存儲在常量數(shù)據(jù)區(qū)。
通過new、malloc獲得的內(nèi)存是堆的內(nèi)存。通過new申請5個int所需的內(nèi)存,但由于內(nèi)存邊界需要字節(jié)對齊(堆上分配內(nèi)存時按16字節(jié)對齊),因此申請5個int共計20個字節(jié),但占據(jù)32字節(jié)的內(nèi)存。通過malloc申請1個字節(jié)的內(nèi)存,申請1個字節(jié)時會從32字節(jié)后開始分配。
內(nèi)存對齊雖然會浪費(fèi)部分內(nèi)存,但由于CPU在對齊方式下運(yùn)行較快,因此內(nèi)存對齊對于程序性能是有益的。C++語言中struct、union、class在編譯時也會對成員變量進(jìn)行內(nèi)存對齊處理,開發(fā)人員可以使用#progma pack()或者編譯器的編譯選項來控制對struct、union、class的成員變量按多少字節(jié)對齊,或者關(guān)閉對齊。

2、全局/靜態(tài)數(shù)據(jù)區(qū)、常量數(shù)據(jù)區(qū)

全局/靜態(tài)存儲區(qū)、常量數(shù)據(jù)區(qū)在程序編譯階段已經(jīng)分配好,在整個程序運(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)入作用域時被初始化,后續(xù)再次進(jìn)入作用域時不必初始化。C++語言中,可以定義靜態(tài)變量,也可以定義類的靜態(tài)成員變量,類的靜態(tài)成員變量用來在類的多個對象間共享數(shù)據(jù)。類的靜態(tài)成員變量存儲在全局/靜態(tài)數(shù)據(jù)區(qū),并且只有一份拷貝,由類的所有對象共享。如果通過全局變量在類的多個對象間共享數(shù)據(jù)則會破壞類的封裝性。

#include <stdio.h>
#include <stdlib.h>

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定義了一個靜態(tài)成員變量nCounter用于對類A的對象進(jìn)行計數(shù),類A也定義了一個成員變量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的每一個對象會有自己的value存儲空間,在棧上分配;類A的所有對象共享一個nCounter的存儲空間,在全局/靜態(tài)數(shù)據(jù)區(qū)分配。

3、堆和棧

在C/C++語言中,當(dāng)開發(fā)人員在函數(shù)內(nèi)部定義一個變量,或者向某個函數(shù)傳遞參數(shù)時,變量和參數(shù)存儲在棧中。當(dāng)退出變量的作用域時,棧上的存儲單元會被自動釋放。當(dāng)開發(fā)人員通過malloc申請一塊內(nèi)存或使用new創(chuàng)建一個對象時,申請的內(nèi)存或?qū)ο笏嫉膬?nèi)存在堆上分配。開發(fā)人員需要記錄得到的地址,并在不再需要時負(fù)責(zé)釋放內(nèi)存空間。

#include <stdio.h>
#include <stdlib.h>

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個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é)束時,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īng)用程序需要動態(tài)管理地管理數(shù)據(jù)。此外,棧的大小有限制,占用內(nèi)存較多的對象或數(shù)據(jù)只能分配在堆空間。
棧和堆的區(qū)別如下:
(1)大小
通常,程序使用棧的大小是固定的,由編譯器決定,開發(fā)人員可以通過編譯器選項指定棧的大小,但通常棧都不會太大。

#include <stdio.h>
#include <stdlib.h>

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)行時出錯,原因是棧溢出。
堆的大小通常只受限于系統(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)存空間時,系統(tǒng)需要按照一定的算法在堆空間中尋找合適大小的空閑堆,并修改相應(yīng)的維護(hù)堆空閑空間的鏈表,然后返回地址給程序。因此,效率幣棧要低,此外還容易產(chǎn)生內(nèi)存碎片。
如果程序在堆上申請5個100字節(jié)大小的內(nèi)存塊,然后釋放其中不連續(xù)的兩個內(nèi)存塊,此時當(dāng)需要在堆上申請一個150字節(jié)大小的內(nèi)存塊時,則無法充分利用剛剛釋放的兩個小內(nèi)存塊。由此可見,連續(xù)創(chuàng)建和刪除占用內(nèi)存較小的對象或數(shù)據(jù)時,很容易在堆上造成內(nèi)存碎片,使得內(nèi)存的使用效率降低。

4、C++對象創(chuàng)建方式

從C++對象模型角度看,對象就是內(nèi)存中的一塊區(qū)域。根據(jù)C++標(biāo)準(zhǔn),一個對象可以通過定義變量創(chuàng)建,或者通過new操作符創(chuàng)建,或者通過實現(xiàn)來創(chuàng)建。如果一個對象通過定義在某個函數(shù)內(nèi)的變量或者需要的臨時變量來創(chuàng)建,是棧上的一個對象;如果一個對象是定義在全局范圍內(nèi)的變量,則對象存儲在全局/靜態(tài)數(shù)據(jù)區(qū);如果一個對象通過new操作符創(chuàng)建,存儲在堆空間。
對面向?qū)ο蟮腃++程序設(shè)計,程序運(yùn)行過程中的大部分?jǐn)?shù)據(jù)應(yīng)該封裝在對象中,而程序的行為也由對象的行為決定。因此,深入理解C++對象的內(nèi)部結(jié)構(gòu),從而正確地設(shè)計和使用對象,對于設(shè)計開發(fā)高性能的C++程序很重要。

三、C++對象的生命周期

1、C++對象生命周期簡介

對象的生命周期是指對象從創(chuàng)建到銷毀的過程,創(chuàng)建對象時要占用一定的內(nèi)存空間,而對象要銷毀后要釋放對應(yīng)的內(nèi)存空間,因此整個程序占用的內(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)通過實現(xiàn)創(chuàng)建對象

2、通過定義變量創(chuàng)建對象

通過定義變量創(chuàng)建對象時,變量的作用域決定了對象的生命周期。當(dāng)進(jìn)入變量的作用域時,對象被創(chuàng)建;退出變量的作用域時,對象被銷毀。全局變量的作用域時整個程序,被聲明為全局對象的變量在程序調(diào)用main函數(shù)前被創(chuàng)建,當(dāng)程序退出main函數(shù)后,全局對象才會被銷毀。靜態(tài)變量作用域不是整個程序,但靜態(tài)變量存儲在全局/靜態(tài)數(shù)據(jù)區(qū),在程序開始時已經(jīng)分配好,因此聲明為靜態(tài)變量的對象在第一次進(jìn)入作用域時會被創(chuàng)建,直到程序退出時被銷毀。

#include <stdio.h>
#include <stdlib.h>

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;
}

上述代碼中定義了一個A的全局對象globalA,一個A的局部對象localA,一個B的靜態(tài)局部對象localB,localA和localB的作用域為test函數(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)入作用域時被創(chuàng)建,在main函數(shù)退出后被銷毀,如果程序從來沒有進(jìn)入到其作用域,則靜態(tài)對象不會被創(chuàng)建;局部對象在進(jìn)入作用域時被創(chuàng)建,在退出作用域時被銷毀。

3、通過new操作符創(chuàng)建對象

通過new創(chuàng)建的對象會一直存在,直到被delete銷毀。即使指向?qū)ο蟮闹羔槺讳N毀,但還沒有調(diào)用delete,對象仍然會一直存在,占據(jù)這堆空間,直到程序退出,因此會造成內(nèi)存泄露。

#include <stdio.h>
#include <stdlib.h>

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)建了一個A對象,并將返回地址記錄在pA指針變量中;然后再次使用createA函數(shù)創(chuàng)建了一個A對象,將返回地址記錄在pA指針變量中,此時pA指針將指向第二次創(chuàng)建的A對象,第一次創(chuàng)建的A對象已經(jīng)沒有指針指向。使用deleteA銷毀對象時,銷毀的是第二次創(chuàng)建的A對象,第一次創(chuàng)建的A對象會一直存在,直到程序退出,并且即使在程序退出時,第一次創(chuàng)建的A對象的析構(gòu)函數(shù)仍然不會被調(diào)用,最終造成內(nèi)存泄露。

4、通過實現(xiàn)創(chuàng)建對象

通過實現(xiàn)創(chuàng)建對象通常是指一些隱藏的中間臨時變量的創(chuàng)建和銷毀。中間臨時變量的生命周期很短,不易被開發(fā)人員察覺,通常是造成性能下降的瓶頸,特別是占用內(nèi)存多、創(chuàng)建速度慢的對象。
中間臨時對象通常是通過拷貝構(gòu)造函數(shù)創(chuàng)建的。

#include <stdio.h>
#include <stdlib.h>

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是需要把實參復(fù)制一份,壓入getA函數(shù)的棧中(對于某些C++編譯器,getA函數(shù)的返回值也要拷貝一份放在棧中,在getA函數(shù)調(diào)用結(jié)束時,參數(shù)出棧就會返回給調(diào)用者)。因此,在調(diào)用getA函數(shù)時,需要構(gòu)造一個a的副本,調(diào)用一次拷貝構(gòu)造函數(shù),創(chuàng)建了一個臨時變量。
中間臨時對象的創(chuàng)建和銷毀是隱式的,因此如果中間臨時對象的創(chuàng)建和銷毀在循環(huán)內(nèi)或是對象構(gòu)造需要分配很多資源,會造成資源在短時間內(nèi)被頻繁的分配和釋放,甚至可能造成內(nèi)存泄露。
上述代碼getA函數(shù)的問題可以通過傳遞引用的方式解決,即getA(A& a),不用構(gòu)造參數(shù)的臨時對象。
實際的C++工程實踐中,會有大量其它類型的隱式臨時對象存在,如重載+和重載++等操作符,對對象進(jìn)行算術(shù)運(yùn)算時也會有臨時對象,操作符重載本質(zhì)上也是函數(shù),因此要盡量避免臨時對象的出現(xiàn)。
當(dāng)一個派生類實例化一個對象時,會先構(gòu)造一個父類對象,同樣,在銷毀一個派生類對象時也會銷毀其父類對象。派生類對象的父類對象是隱含的對象,其生命周期和派生類對象綁定在一起。如果構(gòu)造父類對象的開銷很大,則所有子類的構(gòu)造都會開銷很大。

#include <stdio.h>
#include <stdlib.h>

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)建派生類對象時會先創(chuàng)建隱含的父類對象,銷毀派生類對象時會在調(diào)用派生類析構(gòu)函數(shù)后調(diào)用父類的析構(gòu)函數(shù)。

四、C++對象的內(nèi)存布局

1、C++對象內(nèi)部結(jié)構(gòu)簡介

C++對象的內(nèi)部結(jié)構(gòu)及實現(xiàn)和C++編譯器緊密相關(guān),不同的編譯器可能會有不同的實現(xiàn)方式。

2、C++簡單對象

在一個C++對象中包含成員數(shù)據(jù)和成員函數(shù),成員數(shù)據(jù)分為靜態(tài)成員數(shù)據(jù)和非靜態(tài)成員數(shù)據(jù);成員函數(shù)分為靜態(tài)成員函數(shù)、非靜態(tài)成員函數(shù)和虛函數(shù)。

#include <stdio.h>
#include <stdlib.h>

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)存中,不論時全局/靜態(tài)數(shù)據(jù)區(qū),還是堆上、棧上。Value是double類型,占據(jù)8個字節(jié)(64位),flag是char類型,占據(jù)1個字節(jié),但由于內(nèi)存對齊,也會占用8字節(jié)。
SimpleObject類對象的數(shù)據(jù)成員占用了16個字節(jié),剩下的8字節(jié)是與虛函數(shù)相關(guān)的。如果將兩個虛函數(shù)的virtual關(guān)鍵字去掉,則sizeof(SimpleObject)將得到16。
虛函數(shù)用于實現(xiàn)C++語言的動態(tài)綁定特性,為了實現(xiàn)動態(tài)綁定特性,C++編譯器遇到含有虛函數(shù)的類時,會分配一個指針指向一個函數(shù)地址表,即虛函數(shù)表(virtual table),虛函數(shù)表指針占據(jù)了8個字節(jié),并且占據(jù)的是類實例內(nèi)存布局開始的8個字節(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)存的大小,雖然其實現(xiàn)會占用相應(yīng)的內(nèi)存空間,同樣不會隨著對象數(shù)目的增加而增加。
(4)如果類中包含虛函數(shù),類對象會包含一個指向虛函數(shù)表的指針,虛函數(shù)的地址會放在虛函數(shù)表中。
在虛函數(shù)表中,不一定完全是指向虛函數(shù)實現(xiàn)的指針。當(dāng)指定編譯器打開RTTI開關(guān)時,虛函數(shù)表中的第一個指針指向的是一個typeinfo的結(jié)構(gòu),每個類只產(chǎn)生一個typeinfo結(jié)構(gòu)的實例,當(dāng)程序調(diào)用typeid()來獲取類的信息時,實際是通過虛函數(shù)表中的第一個指針獲取typeinfo結(jié)構(gòu)體實例。

3、單繼承

C++語言中,繼承分為單繼承和多繼承。

#include <stdio.h>
#include <stdlib.h>

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òu)造一個基類的實例,基類實例在派生類實例銷毀后被銷毀。
SimpleObject類大小是24個字節(jié),DerivedObject類的大小是32個字節(jié),DerivedObject增加了一個double類型的成員數(shù)據(jù)subValue,需要占用8個字節(jié)。由于DerivedObject類也需要一個虛函數(shù)表,因此DerivedObject派生類與SimpleObject基類使用同一個虛函數(shù)表,DerivedObject派生類在構(gòu)造時不會再創(chuàng)建一個新的虛函數(shù)表,而是在SimpleObject基類的虛函數(shù)表中增加或修改,DerivedObject實例的虛函數(shù)表中會存儲DerivedObject相應(yīng)的虛函數(shù)實現(xiàn),如果DerivedObject沒有提供某個虛函數(shù)實現(xiàn),則存儲基類SimpleObject的虛函數(shù)實現(xiàn)。

4、多繼承

C++語言提供多繼承的支持,多繼承中派生類可以有一個以上的基類。多繼承是C++語言中頗受爭議的一項特性,多繼承在提供強(qiáng)大功能的同時也帶來了容易造成錯誤的諸多不便。因此,后續(xù)很多面向?qū)ο蟪绦蛟O(shè)計語言取消了多繼承支持,而是提供了更清晰的接口概念。
C++語言中仍然通過繼承實現(xiàn)接口,在面向接口的編程模型,如COM,都采用多繼承實現(xiàn)。如果需要開發(fā)一個文字處理軟件,要求有些文檔即可以打印有可以存儲,有些文檔只可以打印或存儲。考慮到程序的可擴(kuò)展性,比較好的設(shè)計是將打印和存儲分別定義為兩個接口,在接口中定義相應(yīng)的方法。當(dāng)一個類實現(xiàn)了打印和存儲接口時,其對象即可以打印也可以存儲。如果只實現(xiàn)了打印或存儲,則只具備相應(yīng)的功能。

#include <iostream>
#include <string>

using namespace std;

class BaseA
{
public:
    BaseA(int a)
    {
        m_a = a;
    }
    virtual void funcA()
    {
        cout << "BaseA::funcA()" <<endl;
    }
private:
    int m_a;
};

class BaseB
{
public:
    BaseB(int b)
    {
        m_b = b;
    }
    virtual void funcB()
    {
        cout << "BaseB::funcB()" <<endl;
    }
private:
    int m_b;
};

class Derived : public BaseA, public BaseB
{
public:
    Derived(int a, int b, int c):BaseA(a),BaseB(b)
    {
        m_c = c;
    }
private:
    int m_c;
};

struct Test
{
    void* vptrA;
    int a;
    void* vptrB;
    int b;
    int c;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Derived) << endl;
    Derived d(1,2,3);
    Test* pTest = (Test*)&d;
    cout << pTest->a <<endl;//1
    cout << pTest->b <<endl;//2
    cout << pTest->c <<endl;//3
    cout << pTest->vptrA <<endl;//
    cout << pTest->vptrB <<endl;//

    return 0;
}

上述代碼中,Derived類繼承自BaseA和BaseB類,funcA和funcB為虛函數(shù)。
C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型
Derived派生類對象的內(nèi)存模型如下:
C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型
創(chuàng)建派生類時,首先需要創(chuàng)建基類的對象。由于多繼承一個派生類中有多個基類,因此,創(chuàng)建基類的對象時要遵循一定的順序,其順序由派生類聲明時決定,如果將Derived類的聲明修改為:
class Derived : public BaseB, public BaseA
基類對象BaseB會被首先創(chuàng)建,BaseA對象其次被創(chuàng)建?;悓ο箐N毀的順序與創(chuàng)建的順序相反。
多繼承會引入很多復(fù)雜問題,菱形繼承時很典型的一種。菱形繼承示例代碼如下:

#include <iostream>
#include <string>

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 <<endl;
    }
private:
    string m_name;
    int m_age;
};

class Teacher : public People
{
    string m_research;
public:
    Teacher(string name, int age, string research):People(name + "_1", age + 1)
    {
        m_research = research;
    }
};

class Student : public People
{
    string m_major;
public:
    Student(string name, int age,string major):People(name + "_2", age + 2)
    {
        m_major = major;
    }
};

class Doctor : public Teacher, public Student
{
    string m_subject;
public:
    Doctor(string name, int age,string research, string major, string subject):
        Teacher(name, age,research),Student(name, age, major)
    {
        m_subject = subject;
    }
};

struct Test
{
    string name1;
    int age1;
    string research;
    string name2;
    int age2;
    string major;
    string subject;
};

int main(int argc, char *argv[])
{
    Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");
    cout << "Doctor size: " << sizeof(doc) << endl;
    Test* pTest = (Test*)&doc;
    cout << pTest->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)存局部如下:
C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型
底層子類對象中,分別繼承了中間層父類從頂層父類繼承而來的成員變量,因此內(nèi)存模型中含有兩份底層父類的成員變量。
如果頂層父類含有虛函數(shù),中間層父類會分別繼承頂層父類的虛函數(shù)表指針,因此,底層子類對象內(nèi)存布局如下:
C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型

#include <iostream>
#include <string>

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 <<endl;
    }
private:
    string m_name;
    int m_age;
};

class Teacher : public People
{
    string m_research;
public:
    Teacher(string name, int age, string research):People(name + "_1", age + 1)
    {
        m_research = research;
    }
};

class Student : public People
{
    string m_major;
public:
    Student(string name, int age,string major):People(name + "_2", age + 2)
    {
        m_major = major;
    }
};

class Doctor : public Teacher, public Student
{
    string m_subject;
public:
    Doctor(string name, int age,string research, string major, string subject):
        Teacher(name, age,research),Student(name, age, major)
    {
        m_subject = subject;
    }
    virtual void print()
    {

    }
};

struct Test
{
    void* vptr1;
    string name1;
    int age1;
    string research;
    void* vptr2;
    string name2;
    int age2;
    string major;
    string subject;
};

int main(int argc, char *argv[])
{
    Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");
    cout << "Doctor size: " << sizeof(doc) << endl;
    Test* pTest = (Test*)&doc;
    cout << pTest->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++多重繼承問題的一種手段,虛繼承的底層實現(xiàn)原理與C++編譯器相關(guān),一般通過虛基類指針和虛基類表實現(xiàn),每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4(8)字節(jié))和虛基類表(不占用類對象的存儲空間)(虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份);當(dāng)虛繼承的子類被當(dāng)做父類繼承時,虛基類指針也會被繼承。
在虛繼承情況下,底層子類對象的布局不同于普通繼承,需要多出一個指向中間層父類對象的虛基類表指針vbptr。
vbptr是虛基類表指針(virtual base table pointer),vbptr指針指向一個虛基類表(virtual table),虛基類表存儲了虛基類相對直接繼承類的偏移地址;通過偏移地址可以找到虛基類成員,虛繼承不用像普通多繼承維持著公共基類(虛基類)的兩份同樣的拷貝,節(jié)省了存儲空間。

#include <iostream>
#include <string>

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
private:
    string m_name;
    int m_age;
};

class Teacher : virtual public People
{
    string m_research;
public:
    Teacher(string name, int age, string research):People(name + "_1", age + 1)
    {
        m_research = research;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
};

class Student : virtual public People
{
    string m_major;
public:
    Student(string name, int age,string major):People(name + "_2", age + 2)
    {
        m_major = major;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
};

class Doctor : public Teacher, public Student
{
    string m_subject;
public:
    Doctor(string name, int age,string research, string major, string subject):
        People(name, age),Teacher(name, age,research),Student(name, age, major)
    {
        m_subject = subject;
    }
};

struct Test
{
    void* vbptr_left;
    string research;
    void* vbptr_right;
    string major;
    string subject;
    string name;
    int age;
};

int main(int argc, char *argv[])
{
    Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");
    cout << "Doctor size: " << sizeof(doc) << endl;
    Test* pTest = (Test*)&doc;
    cout << pTest->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)存布局如下:
C++應(yīng)用程序性能優(yōu)化(二)——C++對象模型

#include <iostream>
#include <string>

using namespace std;

class People
{
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    virtual void print()
    {
        cout << "this: " << this <<endl;
    }
private:
    string m_name;
    int m_age;
};

class Teacher : virtual public People
{
    string m_research;
public:
    Teacher(string name, int age, string research):People(name + "_1", age + 1)
    {
        m_research = research;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
    virtual void func1()
    {}
};

class Student : virtual public People
{
    string m_major;
public:
    Student(string name, int age,string major):People(name + "_2", age + 2)
    {
        m_major = major;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
    virtual void func2()
    {}
};

class Doctor : public Teacher, public Student
{
    string m_subject;
public:
    Doctor(string name, int age,string research, string major, string subject):
        People(name, age),Teacher(name, age,research),Student(name, age, major)
    {
        m_subject = subject;
    }
    void print()
    {
        cout << "this: " << this <<endl;
    }
    virtual void func3()
    {}
};

struct Test
{
    void* vbptr_left;
    char* research;
    void* vbptr_right;
    char* major;
    char* subject;
    void* vptr_base;
    char* name;
    long age;
};

int main(int argc, char *argv[])
{
    Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");
    cout << "Doctor size: " << sizeof(doc) << endl;
    Test* pTest = (Test*)&doc;
    cout << pTest->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++編譯器實現(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編譯器的實現(xiàn)中,不管是否是虛繼承還是有虛函數(shù),其虛基類指針都不共享,都是單獨(dú)的。對于虛函數(shù)表指針,VC++編譯器根據(jù)是否為虛繼承來判斷是否在繼承關(guān)系中共享虛表指針。如果子類是虛繼承擁有虛函數(shù)父類,且子類有新加的虛函數(shù)時,子類中則會新加一個虛函數(shù)表指針;GCC編譯器和Clang編譯器的虛函數(shù)表指針在整個繼承關(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

5、構(gòu)造與析構(gòu)

C++標(biāo)準(zhǔn)規(guī)定,每個類都必須有構(gòu)造函數(shù),如果開發(fā)人員沒有定義,則C++編譯器會提供一個默認(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òu)造函數(shù),如果開發(fā)人員沒有定義,則C++編譯器會提供一個默認(rèn)的拷貝構(gòu)造函數(shù),默認(rèn)拷貝構(gòu)造函數(shù)是淺拷貝,即按照對象的內(nèi)存空間逐個字節(jié)進(jìn)行拷貝,因此默認(rèn)拷貝構(gòu)造函數(shù)會帶來隱含的內(nèi)存問題。

#include <stdio.h>
#include <stdlib.h>

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)行時會異常退出。

SimpleObject在構(gòu)造時分配了n個字節(jié)的緩沖區(qū),在析構(gòu)時釋放緩沖區(qū)。但由于沒有定義拷貝構(gòu)造函數(shù),C++編譯器會提供一個淺拷貝的默認(rèn)拷貝構(gòu)造函數(shù),SimpleObject b = a語句會通過淺拷貝構(gòu)造一個SimpleObject對象b,對象b的buffer和對象a的buffer指向同一塊內(nèi)存空間,在對象a和對象b析構(gòu)時,這塊內(nèi)存空間被釋放了兩次,造成程序崩潰。如果不想通過賦值或拷貝構(gòu)造函數(shù)構(gòu)造對象,可以將拷貝構(gòu)造函數(shù)定義為private,此時SimpleObject b = a;會在編譯時報錯。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI