【C++】C++用new和不用new建立類物件區別

漫步雲端ly發表於2017-12-20

起初剛學C++時,很不習慣用new,後來看老外的程式,發現幾乎都是使用new,想一想區別也不是太大,但是在大一點的專案設計中,有時候不使用new的確會帶來很多問題。

當然這都是跟new的用法有關的。new建立類物件,使用完後需使用delete刪除,跟申請記憶體類似。所以,new有時候又不太適合,比如在頻繁呼叫場合,使用區域性new類物件就不是個好選擇,使用全域性類物件或一個經過初始化的全域性類指標似乎更加高效。

一、new建立類物件與不new區別

下面是自己總結的一些關於new建立類物件特點:

  • new建立類物件需要指標接收,一處初始化,多處使用
  • new建立類物件使用完需delete銷燬
  • new建立物件直接使用堆空間,而區域性不用new定義類物件則使用棧空間
  • new物件指標用途廣泛,比如作為函式返回值、函式引數等
  • 頻繁呼叫場合並不適合new,就像new申請和釋放記憶體一樣

二、new建立類物件例項

1、new建立類物件例子:

CTest* pTest = new CTest();

delete pTest;

pTest用來接收類物件指標。

不用new,直接使用類定義申明:

CTest mTest;

此種建立方式,使用完後不需要手動釋放,該類解構函式會自動執行。而new申請的物件,則只有呼叫到delete時再會執行解構函式,如果程式退出而沒有執行delete則會造成記憶體洩漏。

2、只定義類指標

這跟不用new申明物件有很大區別,類指標可以先行定義,但類指標只是個通用指標,在new之前併為該類物件分配任何記憶體空間。比如:

CTest* pTest = NULL;

但使用普通方式建立的類物件,在建立之初就已經分配了記憶體空間。而類指標,如果未經過物件初始化,則不需要delete釋放。

3、new物件指標作為函式引數和返回值

下面是天緣隨手寫一個例子,不太嚴謹。主要示意一下類指標物件作為返回值和引數使用。

 

示例:

類的記憶體分配簡單總結,程式碼片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
usingnamespace std;
 
/**
* yanggang
* http://blog.ithomer.net
* 2014-08-11
**/
 
 
classClassA {
    private:
        intA;
        intB;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
 
    virtualvoid prin3() {
    }
};
 
classClassB : publicClassA {
    public:
        intC;
        intD;
 
    voidprin4() {
    }
 
    voidprin5() {
    }
 
    virtual void  prin6() {
    }
};
 
intmain(intargc, char* argv[]) {
    cout<<sizeof(ClassA)<<endl;       // 16
    cout<<sizeof(ClassB)<<endl;       // 24
 
    return0;
}

測試環境:

Ubuntu 12.04.5 LTS   x86_64 GNU/Linux

g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 編譯引數 -m64 

結果是

homer@homer-pc:~/Desktop$ g++ testMem.c -o testMem && ./testMem
16
24

結果為什麼是這樣?

32位系統int4個位元組,64位系統int也佔4個位元組(不是8個位元組),而一個類中所有的虛擬函式通過一個虛擬函式指標管理,類物件的大小隻包含這個vptr指標(在64位機器上指標sizeof為8個位元組),其他虛擬函式是放在別的記憶體空間中管理,vptr指標在64位機器上是8個位元組大小(32位機器上是4個位元組)。注意到普通成員函式並不佔類物件的大小空間,因為普通成員函式通過this指標管理,一個物件的this指標並不是物件本身的一部分,不會影響sizeof(物件)的結果。

sizeof(ClassA)

1)int A 和 int B 各佔4個位元組,考慮64位機器編譯器對其規則,合併為8個位元組

2)virtual void prin3() 虛擬函式的vptr指標,在64位機器編譯器上佔8個位元組

3)合計 sizeof(ClassA)為 8 + 8 = 16個位元組

this作用域是在類內部,當在類的非靜態成員函式中訪問類的非靜態成員的時候,編譯器會自動將物件本身的地址作為一個隱含引數傳遞給函式。這個this指標會因編譯器不同而有不同的放置位置可能是棧,也可能是暫存器,甚至全域性變數。

子類其實不管如何繼承,用sizeof()算該類的大小都會把父類中的私有成員變數所佔的空間算進去,也就是說,私有變數也在子類中分配了記憶體,但你卻不可以直接訪問,這起到一個保護作用,這如同一個珠寶,共有繼承就是開放性的展覽,而私有繼承是把珠寶鎖起來,你卻不能動,要動珠寶如果有管家(基類的public中定義了一些對其私有變數操作的成員函式,)只能讓管家幫你代勞。

sizeof(ClassB)

1)int A 和 int B 各佔4個位元組,是父類ClassA中的私有變數,合併佔用8個位元組

2)virtual void prin3() 虛擬函式的vptr指標,父類ClassA在子類中不會分配空間

3)int C 和 int D 各佔4個位元組,在子類中會分配空間,合併佔用8個位元組

4)virtual void prin6() 虛擬函式的vptr指標,在64位機器編譯器上佔8個位元組

5)合計 sizeof(ClassB)為 8 + 8 + 8 = 24個位元組

註明: 上述示例在32位編譯器上測試,sizeof(ClassA)和sizeof(ClassB)分別為12和20位元組(虛擬函式指標佔用空間少了4位元組)


只有虛擬函式會佔用一個指標大小的記憶體,原因是系統用一個指標維護這個類的虛擬函式表,並且注意這個虛擬函式無論含有多少項(類中含有多個虛擬函式)都不會影響類的大小。

 

知識延伸:

1) 空 ClassA(驗證空Class佔用空間大小

1
2
3
4
5
6
7
8
9
10
11
classClassA {
    private:
 
    voidprin1() {
     }
 
    voidprin2() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 1

 

2) ClassA 只有一個char(驗證編譯器對其規則

1
2
3
4
5
6
7
8
9
10
11
12
classClassA {
    private:
        charC;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 1

 

3)ClassA 有一個char和一個虛擬函式(或一個long型)(驗證編譯器對其規則二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classClassA {
    private:
        charC;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
 
    virtualvoid prin3() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 16

 

4)ClassA 有一個char和一個虛擬函式(或一個long型),且對調char和虛擬函式的先後順序(驗證編譯器對其規則三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
classClassA {
 
    voidprin1() {
     }
 
    voidprin2() {
    }
 
    virtualvoid prin3() {
    }
 
    private:
        charC;
};
 
cout<<sizeof(ClassA)<<endl;       // 16

 

5)ClassA 有一個char,一個int,一個虛擬函式(或一個long型)(驗證編譯器對其規則四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
classClassA {
    private:
        charC;
        intA;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
 
    virtualvoid prin3() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 16

 

6)ClassA 有一個char,一個int(驗證編譯器對其規則五

1
2
3
4
5
6
7
8
9
10
11
12
13
classClassA {
    private:
        charC;
        intA;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 8

 

7)ClassA 只有一個int(驗證編譯器對其規則六

1
2
3
4
5
6
7
8
9
10
11
12
classClassA {
    private:
        intA;
 
    voidprin1() {
     }
 
    voidprin2() {
    }
};
 
cout<<sizeof(ClassA)<<endl;       // 4

 

總結如下:

1) 空類的 sizeof 為1個位元組

2) 只有一個char的類,sizeof為一個位元組

3) 類中含有char和虛擬函式,將以最大的變數或指標為編譯器對齊規則,例如:虛擬函式指標佔8個位元組(64位編譯器),則char雖然只佔1個位元組,但對齊後空餘了7個位元組,合併類佔8(指標) + 1(char) + 7(對齊的空位元組) = 16個位元組

4) 對齊規則,跟變數或虛擬函式的先後順序無關,只跟最大變數型別或函式指標有關,函式指標跟編譯器最大對齊位數有關(不太好理解,請繼續往下看)

5) char和int合佔8個位元組,虛擬函式指標佔8個位元組,且以最大的虛擬函式指標的8位元組對其,其中char佔一個空餘3個位元組合併佔4個,int佔4個位元組,按8位規則對齊,合計16位元組

6) 一個char和int合併佔8個位元組,無虛擬函式,此時以最大變數型別int對齊,因此char佔1位元組空3位元組佔4位元組

7)一個int佔4位,自己便是最大的對齊規則; 2)一個char同理也佔1個位元組,此處跟編譯器最大64位對齊規則無關(即一個int或一個char,不會擴充到佔用8位元組)

 

這裡,又延伸出了一個很有意思的問題,空類sizeof為什麼不為0,而為1?

回答這個問題,需要回到編譯器和類的例項化問題上來,在學校看過《深度探索C++物件模型》,類分抽象類和普通類,空類屬於普通類,是可以被例項化的。

每個類的例項,在記憶體中都有一個獨一無二的地址,為了達到這個目的,編譯器往往會給一個空類隱含的加一個位元組,這樣空類在例項化後在記憶體得到了獨一無二的地址,因此1)中的空類ClassA預設會佔用1個位元組。


相關文章