分配太少的空间是否安全(如果你知道你不需要它)?

时间:2011-09-09 02:02:04

标签: c malloc flexible-array-member

因此,C99祝福常用的“灵活阵列成员”黑客,允许我们制作可以根据我们的尺寸要求进行过度调整的struct。我怀疑在大多数理智的实现中这样做是完全安全的,但是如果我们知道在某些情况下我们不需要struct的某些成员,那么在C中是否合法“unrallocate”?

抽象示例

说我有一个类型:

struct a {
  bool   data_is_x;
  void * data;
  size_t pos;
};

如果data_is_x,则data的类型是需要使用pos成员的类型。否则,使用此struct的函数将不需要pos成员struct的此特定副本。从本质上讲,struct带有关于它是否有pos成员的信息,并且这些信息在struct的生命周期内不会被更改(除了邪恶的恶作剧,这将是无论如何都要打破任何东西)。可以说:

struct a *a = malloc(data_is_x ? sizeof(struct a) : offsetof(struct a, pos));

只有在需要时才为pos成员分配空间?或者它是否违反使用对struct指针而言太小的投射空间的约束,即使您从未使用过相关成员?

具体例子

我的实际用例有点涉及;它主要是为了让你理解为什么我想这样做:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

mylist_create的代码指定,对于size > 0data是一个连续数据的数组,size项长(无论项目是什么),但是对于size == 0,它是包含项目的双向链表的当前节点。与mylist一起使用的所有函数都将检查size == 0是否为data。如果是这样,他们将把数据作为链表进行处理,其中“当前”索引是指向pos的节点所指向的。如果没有,他们将把数据作为一个数组处理,其中“当前”索引存储在size == 0中。

现在,如果pos我们确实不需要size > 0成员,但如果mylist *list = malloc(size ? sizeof(mylist) : offsetof(mylist, pos)); 我们会。所以我的问题是,这样做是否合法:

size == 0

如果我们保证(对未定义行为的惩罚),在pos期间,我们永远不会尝试(或需要)访问{{1}}成员?或者它是否在标准的某个地方说UB甚至想到这样做?

5 个答案:

答案 0 :(得分:4)

malloc本身并不关心为结构分配多少内存,它是未定义的块外部的解除引用。来自C99 6.5.3.2 Address and indirection operators

  

如果为指针指定了无效值,则unary *运算符的行为未定义。

而且,从7.20.3 Memory management functions,我们发现(我的斜体):

  

如果分配成功,则返回指针,以便可以将其分配给指向任何类型对象的指针,然后用于在分配的空间中访问此类对象或此类对象的数组(直到空格被明确释放)。

因此,您可以执行以下操作:

typedef struct { char ch[100]; } ch100;
ch100 *c = malloc (1);

并且,如果您只尝试使用c->ch[0]做任何事情,那么这是完全可以接受的。


对于您具体的具体示例,我不太确定我会担心,假设您关注的是存储空间。如果您担心其他原因,请随意忽略这一点,特别是因为标准中没有规定其中包含的假设。

根据我的理解,你有一个结构:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

您只想使用data size为0的data以及possize data大于0的地方。使用posmalloc加入联盟。

大量的size_t实现会将您请求的空间四舍五入为16个字节的倍数(或两个更高的幂),以减轻内存碎片问题。当然,这不是标准所要求的,但它很常见。

假设(例如)32位指针和pos,您的十二个字节的结构很可能占用一个16字节的竞技场标头和一个16字节的数据块。即使你只要求8(即没有size_t),这个块仍然是16个字节。

如果你有64位指针和pos类型,它可能会有所不同 - 带有{{1}}的24个字节和16个没有。

但即便如此,除非您分配这些结构的批次,否则可能不是问题。

答案 1 :(得分:2)

这是完全合法的,但你可能应该通过使用两个结构,并且当你阅读它时不那么模糊:

struct leaf_node {
    size_t size;
    void *data;
    size_t pos;
};
struct linked_node {
    size_t size;
    void *next;
};

void *in = ...;

if (*(size_t*)(in) == 0) {
    struct leaf_node *node = in;
    ...
} else {
    struct linked_node *node = in;
    ....
}

这与paxdiablo引用的标准更加相似,您可以将指针强制转换为任何数据指针。如果你这样做,你也总是要确保把它投射到一个适合分配缓冲区的结构(一个不必要但很方便的专长)。

paxdiablo说32位系统上最小16字节的大小通常是正确的,但你仍然可以分配大块来解决这个问题。

在32位系统上,linked_node将使用8个字节。您必须使用池才能从您想要做的事情中受益。

struct leaf_node *leaf_pool = malloc(N*sizeof(struct leaf_node));
struct linked_node *linked_pool = malloc(N*sizeof(struct linked_node));

当然,您永远不应该重新分配池,而是根据需要分配新池并重用节点。在这种情况下,单个leaf_node将使用12个字节。

同样适用于linked_node,如果您在池中分配它们,它将使用8个字节而不是16个字节。

只要您的结构在GCC中没有使用__attribute__ ((packed)),就不存在性能瓶颈,在这种情况下,您的结构可能会非常严重地对齐。特别是如果你的结构中有一个额外的char,它的大小为13个字节。

现在,如果我们回到您的初始问题,只要您确保不访问缓冲区之外的数据,用于指向分配数据的指针就无关紧要了。你的struct基本上就像一个char字符串,你检查第一个size_t是否是一个“空字节”,如果它是假设缓冲区更小。如果它不为null,则假定“string”更长并且您读取更多数据。涉及完全相同的风险,编译后的唯一区别是每个元素读取的大小。与转换为结构指针和读取元素相比,使用[el]字符串并没有什么神奇之处,因为您可以使用[el]轻松地进行验证。

答案 2 :(得分:1)

据我所知,任何成员访问也是对聚合的访问,因此声明了一个有效的类型,即我们得到的分配对象太小而实际上不包含其类型的值。

这闻起来像未定义的行为,但我实际上无法从标准中回复,并且还有合理的论据支持其他解释。

答案 3 :(得分:1)

您可能认为保存了4或8个字节,但您的内存分配可以对齐。如果您正在使用gcc及其16字节对齐,则可以得到类似的内容。

for (int i = 0; i <= 64; i++) {
    char *p = (char *) malloc(i);
    char *q = (char *) malloc(i);
    long long t = q - p;
    cout << "malloc(" << i << ") used " << t << " bytes " << endl;
}

打印

malloc(0) used 32 bytes 
malloc(1) used 32 bytes 
malloc(2) used 32 bytes 
malloc(3) used 32 bytes 
malloc(4) used 32 bytes 
malloc(5) used 32 bytes 
malloc(6) used 32 bytes 
malloc(7) used 32 bytes 
malloc(8) used 32 bytes 
malloc(9) used 32 bytes 
malloc(10) used 32 bytes 
malloc(11) used 32 bytes 
malloc(12) used 32 bytes 
malloc(13) used 32 bytes 
malloc(14) used 32 bytes 
malloc(15) used 32 bytes 
malloc(16) used 32 bytes 
malloc(17) used 32 bytes 
malloc(18) used 32 bytes 
malloc(19) used 32 bytes 
malloc(20) used 32 bytes 
malloc(21) used 32 bytes 
malloc(22) used 32 bytes 
malloc(23) used 32 bytes 
malloc(24) used 32 bytes 
malloc(25) used 48 bytes 
malloc(26) used 48 bytes 
malloc(27) used 48 bytes 
malloc(28) used 48 bytes 
malloc(29) used 48 bytes 
malloc(30) used 48 bytes 
malloc(31) used 48 bytes 
malloc(32) used 48 bytes 
malloc(33) used 48 bytes 
malloc(34) used 48 bytes 
malloc(35) used 48 bytes 
malloc(36) used 48 bytes 
malloc(37) used 48 bytes 
malloc(38) used 48 bytes 
malloc(39) used 48 bytes 
malloc(40) used 48 bytes 
malloc(41) used 64 bytes 
malloc(42) used 64 bytes 
malloc(43) used 64 bytes 
malloc(44) used 64 bytes 
malloc(45) used 64 bytes 
malloc(46) used 64 bytes 
malloc(47) used 64 bytes 
malloc(48) used 64 bytes 
malloc(49) used 64 bytes 
malloc(50) used 64 bytes 
malloc(51) used 64 bytes 
malloc(52) used 64 bytes 
malloc(53) used 64 bytes 
malloc(54) used 64 bytes 
malloc(55) used 64 bytes 
malloc(56) used 64 bytes 
malloc(57) used 80 bytes 
malloc(58) used 80 bytes 
malloc(59) used 80 bytes 
malloc(60) used 80 bytes 
malloc(61) used 80 bytes 
malloc(62) used 80 bytes 
malloc(63) used 80 bytes 
malloc(64) used 80 bytes 

根据您的系统,可能会使用malloc(0)或malloc(24)来使用相同数量的内存。

答案 4 :(得分:0)

在分配中保存4个字节几乎没有意义,除非你谈论它们的成千上万个,并且在这种情况下你可能想要暂时使用带有“释放”结构的池分配方案但是在“可用”列表(“池”),而不是不断释放和重新分配它们。我保证会更快。但是为了干净地使用这样的方案,所有可重复使用的部分都需要易于互换 - 即拥有“size_t pos”成员。

所以,是的,你所想做的事情是完全合法的;我不确定它是否值得并发症和它所带来的灵活性缺乏。