14

因此,C99 祝福了常用的“灵活数组成员”hack,允许我们制作struct可以过度分配的 s 以满足我们的大小要求。我怀疑在大多数理智的实现中这样做是完全安全的,但是如果我们知道在某些情况下我们不需要 a 的某些成员,那么在 C 中“分配不足”是否合法struct

抽象例子

假设我有一个类型:

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

如果data_is_x,那么 的类型data是需要使用pos成员的类型。否则,使用 this 的函数将struct不需要. 本质上,携带有关它是否有成员的信息,并且此信息在 的生命周期内不会更改(在邪恶的恶作剧之外,无论如何都会破坏任何东西)。可以安全地说:posstructstructposstruct

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;

for 的代码mylist_create指定 forsize > 0data一个连续数据数组,它是size项长(无论项可能是什么),但size == 0它是包含项的双向链表的当前节点。与 s 一起工作的所有功能mylist都会检查size == 0. 如果是这样,他们会将数据作为链表处理,其中“当前”索引是节点data指向的任何一个。如果没有,他们会将数据作为数组处理,其中“当前”索引存储在pos.

现在,如果size == 0我们真的不需要该pos成员,但如果size > 0我们需要的话。所以我的问题是,这样做是否合法:

mylist *list = malloc(size ? sizeof(mylist) : offsetof(mylist, pos));

如果我们保证(对未定义行为的惩罚), while size == 0,我们将永远不会尝试(或需要)访问该pos成员?或者它是否在标准中的某个地方说它是 UB 甚至考虑这样做?

4

5 回答 5

4

malloc它本身根本不关心你为一个结构分配了多少内存,它是未定义的块外内存的取消引用。从 C99 开始6.5.3.2 Address and indirection operators

如果已为指针分配了无效值,则一元 * 运算符的行为未定义。

而且,从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;

where 你只想使用datawheresize是 0,并且都dataposwheresize都大于 0。这排除了在 union 中使用 putdata和。pos

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

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

如果你有 64 位指针和size_t类型,它可能会有所不同——有 24 个字节,pos没有 16 个字节。

但即便如此,除非你分配了很多这样的结构,否则它可能不是问题。

于 2011-09-09T02:12:46.887 回答
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)),在这种情况下,您的结构可能会非常糟糕地对齐。特别是如果你的结构中有一个额外的字符,例如,它的大小为 13 个字节。

现在,如果我们回到您最初的问题,只要您确保不访问缓冲区之外的数据,您用来指向已分配数据的指针并不重要。您的结构本质上就像一个 char 字符串,并且您检查第一个 size_t 是否是“空字节”,如果是,则假设缓冲区较小。如果它不为空,则假定“字符串”更长并且您读取更多数据。涉及完全相同的风险,编译后的唯一区别是每个元素读取的大小。与转换为结构指针和读取元素相比,使用 for 字符串没有什么神奇之处[el],因为您可以通过使用[el].

于 2011-09-09T08:10:03.160 回答
1

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

这闻起来像是未定义的行为,但我实际上不能从标准中支持这一点,而且也有合理的论据支持另一种解释。

于 2011-09-09T15:13:07.623 回答
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),都可能使用相同数量的内存。

于 2011-09-09T20:59:27.010 回答
0

在分配中节省 4 个字节几乎没有意义,除非您谈论的是成千上万个字节,在这种情况下,您可能希望暂时使用具有“已释放”结构但在“可用”上的池分配方案列表(“池”),而不是不断释放和重新分配它们。我保证会更快。但是要干净利落地使用这样的方案,所有可重复使用的部分都需要易于互换——也就是说,要有“size_t pos”成员。

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

于 2011-09-09T16:06:01.050 回答