2014-09-11 53 views
51

我發現MSVC和GCC編譯器爲每個類實例分配至少一個字節,即使這個類是一個沒有成員變量(或者只有靜態成員變量)的謂詞。以下代碼說明了這一點。爲什麼沒有成員變量的C++類佔用空間?

#include <iostream> 

class A 
{ 
public: 
    bool operator()(int x) const 
    { 
     return x>0; 
    } 
}; 

class B 
{ 
public: 
    static int v; 
    static bool check(int x) 
    { 
     return x>0; 
    } 
}; 

int B::v = 0; 

void test() 
{ 
    A a; 
    B b; 
    std::cout << "sizeof(A)=" << sizeof(A) << "\n" 
      << "sizeof(a)=" << sizeof(a) << "\n" 
      << "sizeof(B)=" << sizeof(B) << "\n" 
      << "sizeof(b)=" << sizeof(b) << "\n"; 
} 

int main() 
{ 
    test(); 
    return 0; 
} 

輸出:

sizeof(A)=1 
sizeof(a)=1 
sizeof(B)=1 
sizeof(b)=1 

我的問題是爲什麼編譯器需要它?我能想出的唯一原因是確保所有成員var指針都不相同,因此我們可以通過比較指向它們的指針來區分A或B類型的兩個成員。但是在處理小尺寸容器時,這樣做的代價非常嚴重。考慮到可能的數據對齊,我們可以得到每個類沒有變量(?!)的16個字節。假設我們有一個自定義容器,通常會容納幾個int值。然後考慮一個這樣的容器數組(大約有1000000個成員)。開銷將是16 * 1000000!它可能發生的典型情況是一個帶有存儲在成員變量中的比較謂詞的容器類。另外,考慮到類實例應該總是佔用一定的空間,調用A()(value)時應該期望什麼類型的開銷?

+7

只是爲了確認您的懷疑:*除非是位域(9.6),否則大多數派生對象的大小應爲非零,並佔用一個或多個字節的存儲空間。基類子對象可能具有零大小。* – chris 2014-09-11 12:00:37

+3

供參考:大小爲*的子對象被允許。所以,如果你從這樣一個空的類派生出來並添加另一個x的成員,那麼你的派生類型的大小也是x。這就是所謂的「空基類優化」 – sellibitze 2014-09-11 12:04:26

+8

我相信你有幾個問題重疊在那裏。沒有必要在容器中「存儲」大量沒有成員的類。畢竟,由於沒有數據,它們之間沒有區別。但是,沒有成員的類在C++中的大小不爲零並不意味着有成員的類將會產生不必要的開銷。然而,內存對齊問題是一個獨立的問題,並不限於C++。 – AlefSin 2014-09-11 12:05:19

回答

74

需要滿足來自C++標準的不變量:每個相同類型的C++對象都需要有一個唯一的地址才能被識別。

如果對象不佔用空間,則數組中的項目將共享相同的地址。

+1

順便說一句,我猜如果你有一個帶有這樣一個空的'struct'(或'class')的局部變量,它可能經常被優化爲在調用堆棧上根本沒有空間。 – 2014-09-11 12:00:51

+1

我想知道:由於沒有可訪問的數據,一個相符的實現可以在一個非常高的地址「分配」一個空類的數組,而這個地址永遠不可用?例如x86_64上的那些「不可能」指針值之一? – 2014-09-11 12:01:25

+0

沒有值的數組恰好佔用零字節,而C++在處理這些數據時沒有問題。爲什麼通過指針來區分實例很重要,如果它們根據定義是相同的並且不能在任何值的表達式中使用? – bkxp 2014-09-11 12:02:44

13

所有完整的對象都必須有唯一的地址;所以它們必須佔用至少一個字節的存儲空間 - 地址中的字節。

它可能發生的一個典型情況是一個帶有存儲在成員變量中的比較謂詞的容器類。

在這種情況下,你可以使用空基類優化:一個基子被允許具有相同的地址作爲完整的對象,它的組成部分,所以可以不佔用存儲空間。因此,您可以將謂詞作爲(可能是私有的)基類而不是成員附加到類。與成員相比,處理起來更加煩瑣,但應該消除開銷。

調用A()(value)時應該期望什麼類型的開銷?

與調用非成員函數相比,唯一的開銷是傳遞額外的this參數。如果函數被內聯,那麼這應該被消除(通常情況下,當調用不訪問任何成員變量的成員函數時)。

+1

值得注意的是,傳遞'this'的開銷通常很小,但如果參數的數量超過了由於不必要的'this'而導致堆棧傳遞的ABI限制,則會變得更大。如果成員函數是「內聯」的,則可以優化。 – 2014-09-11 12:08:35

+0

感謝您的解釋。繼承技巧非常有趣,儘管它需要一個適配器類來避免在容器類中輸入多餘的運算符()。 – bkxp 2014-09-11 12:14:51

+0

@bkxp:如果你使用私有繼承,那麼你_technically_仍然有'operator()',但是它是私有的,所以沒有其他人可以使用它。我預計幾乎每個容器都會在分配器中執行此操作。 – 2014-09-11 20:10:52

26

基本上,它是兩個需求之間的相互作用:相同類型的

  • 兩個不同的對象必須處於不同的地址。
  • 在數組中,對象之間可能沒有任何填充。

注意第一個條件本身並不需要非零大小:鑑於

struct empty {}; 
struct foo { empty a, b; }; 

的第一個要求很容易被具有零大小a後跟一個填充字節,以滿足強制執行一個不同的地址,然後是一個零大小的b。然而,鑑於

empty array[2]; 

不再工作,因爲不同的對象empty[0]empty[1]之間的襯墊將不被允許。

+0

我仍然試圖弄清楚這是絕對必要的情況。其中一種情況是在計算N/sizeof(A)時避免被零除。 – bkxp 2014-09-11 16:20:09

+0

@bkxp:'std :: sort(&array [0],&array [不管它的大小是多少]);' – PlasmaHH 2014-09-12 11:06:44

+0

@PlasmaHH:假定對一個相同對象的數組進行排序是無操作,並且begin = end,調用'std :: sort'也是沒有操作的,這個特定的行肯定不會中斷。 – celtschk 2014-09-17 13:56:51

1

已經有優秀的答案回答了主要問題。我想解決您表達的關注:

但是,處理小尺寸容器時,這種成本相當嚴重。考慮到可能的數據對齊,我們可以得到每個類沒有變量(?!)的16個字節。假設我們有一個自定義容器,通常會容納幾個int值。然後考慮一個這樣的容器數組(大約有1000000個成員)。開銷將是16 * 1000000!它可能發生的典型情況是一個帶有存儲在成員變量中的比較謂詞的容器類。

避免持有A

如果容器的所有實例取決於A類型的成本,那麼就沒有必要召開的A情況下,在容器中。只要在需要時在堆棧上創建A的實例,就可以避免與非零大小的A相關的開銷。

不能夠避免的控股A

你可能會被迫在容器中的每個實例的指針堅持A如果A是由多態有望成本。對於這樣的容器,每個容器的成本會隨着指針的大小而增加。基類A中是否有成員變量對容器的大小沒有影響。

sizeof A

影響在兩種情況下,一個空類的尺寸應當具有在容器的存儲需求無關。

+0

容器的內存需求較少vs創建臨時對象的時間較少。我想你必須決定哪種折衷對你的用例更好。 – 2014-09-19 06:47:19

相關問題