2014-09-11 52 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

相关问题