确保 char 存在于结构的末尾

Posted

技术标签:

【中文标题】确保 char 存在于结构的末尾【英文标题】:Ensuring a char exists at the end of a struct 【发布时间】:2022-01-09 12:54:03 【问题描述】:

在 C 语言中,我想在结构的最末端放置一个char id,以便我可以从指向末端的指针中辨别结构类型结构的(动态分配的)。显然,最后填充的可能性使这变得困难。我想到了两种方法。

第一种方法是放置一个一直延伸到结构末尾的字符数组,以便(char*)ptr_to_end - 1 始终指向有效字符。我认为如果编译器没有做任何有趣的事情,这应该可以工作。否则,它应该无法编译:

typedef struct

    int foo;
    int bar;
    char type;
 MyStructDummy;

typedef struct

    int foo;
    int bar;
    char type[ sizeof( MyStructDummy ) - offsetof( MyStructDummy, type ) ];
 MyStruct;

_Static_assert(
    sizeof( MyStruct ) == sizeof( MyStructDummy ),
    "Could not ensure char at end of MyStruct"
);

第二种方法是使用offsetof始终将 malloc-ed bloc 作为单个(成员)变量访问,而不是作为一个完整的结构访问。这样,我们就避免了将结构的类型作为有效类型传递给整个块或意外更改填充值:

typedef struct

    int foo;
    int bar;
    char type;
 MyStruct;

int *MyStruct_foo( void *end_ptr )

    return (int*)( (char*)end_ptr - sizeof( MyStruct ) + offsetof( MyStruct, foo ) );


int *MyStruct_bar( void *end_ptr )

    return (int*)( (char*)end_ptr - sizeof( MyStruct ) + offsetof( MyStruct, bar ) );


char *MyStruct_type( void *end_ptr )

    return (char*)end_ptr - 1;

这两种方法中的任何一种都比另一种更可取吗?是否有现有的 C 习惯用法可以实现我想要实现的目标(我不能使用灵活的数组成员,因为我想保持 C++ 兼容性)?

谢谢!

编辑:

Karl 询问在结构的末尾放置一个 id 会有什么用处。考虑一下动态数组/向量的这种节省内存的实现:

//VecHdr is for vectors without an automatic element destructor function
//and whose capacity is < UINT_MAX
typedef struct

    alignas( max_align_t )
    unsigned int size;
    unsigned int cap;
    char type_and_flags; //At very end
 VecHdr; //Probable size: 16 bytes

//VecHdr is for vectors with an element destructor or whose capacity is >= UINT_MAX
typedef struct

    alignas( max_align_t )
    size_t size;
    size_t cap;
    void (*element_destructor_func)( void* );
    char type_and_flags; //At very end
 VecHdrEx; //Probable size: 32 bytes

//...

int *foo = vec_create( int );
//This macro returns a pointer to a malloced block of ints, preceded by a VecHdr

int *bar = vec_create_ex( int, my_element_destructor );
//This macro returns a pointer to malloced block of ints, preceded by a VecHdrEx

vec_push( foo, 12345 );
//vec_push knows that foo is preceded by a VecHdr by checking (char*)foo - 1
//foo's VecHdr may eventually be replaced with a VecHdrEx if we add enough elements

vec_push( bar, 12345 );
//vec_push knows that bar is preceded by a VecHdrEx by checking (char*)foo - 1

【问题讨论】:

"我想在结构的最后放置一个char id" - 为什么? “这样我就可以通过指向结构末尾的指针来识别结构类型” - 为什么要从末尾而不是从前面访问结构? @JDormer 我认为两种方法的内存使用量应该相同。该内存要么落在char 数组中,要么被填充消耗。 我仍然无法理解您试图用这种方法解决什么问题。请展示一个代码示例,否则您会因为没有关于指针的必要信息而遇到问题,并说明您为什么会出现这种情况。 我会在用户可见数组之前放置一个固定大小的主标题块,并在主标题块之前放置另一个带有附加数据的可选标题块。它的存在和大小由主块中的类型字段决定。 offsetof 似乎足够了。那时,将字符放在哪里都无关紧要。您也可以使用一个 int(或两个)来虚拟地保证对齐,尽管这不能保证 【参考方案1】:

如果最后一个成员没有对齐,则只有在末尾才会有填充,例如小整数类型。

但是,如果将最后一个成员设为字符类型的灵活数组成员,它将始终放在此类填充字节的顶部,因为在确定大小和填充时,该结构不会考虑灵活数组成员。

例子:

typedef struct

    int foo;
    int bar;
    char type[];
 MyStructDummy;

MyStructDummy* dummy = malloc (sizeof *dummy + 1);
printf("Size: %zu\n", sizeof(MyStructDummy));
printf("Address of struct:%p\n", dummy);
printf("Address of type:%p\n", dummy->type);

这给出了类似的东西:

Size: 8
Address of struct:0x4072a0
Address of type:0x4072a8

如果我们添加一个额外的成员以确保末尾有填充:

typedef struct

    int foo;
    int bar;
    char causing_padding;
    char type[];
 MyStructDummy;

然后打印与上面完全相同的代码:

Size: 12
Address of struct:0x16f22a0
Address of type:0x16f22a9

所以这里编译器确实添加了填充,但它允许我们使用字节 9 来存储数据。我们最终分配了超出灵活数组成员的内存。现在,我们可以分配灵活的数组成员来覆盖所有的填充:

size_t trailing_padding = sizeof(MyStructDummy) - offsetof(MyStructDummy, type);
MyStructDummy* dummy = malloc (sizeof *dummy + trailing_padding);

这仍然会在地址 9 处留下 type,但它现在占用 3 个字节。我们可以使用您希望放置的任何代码对它们进行 memset。这是定义明确且可移植的。完整示例:

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>

typedef struct

    int foo;
    int bar;
    char causing_padding;
    char type[];
 MyStructDummy;

int main (void)

  size_t trailing_padding = sizeof(MyStructDummy) - offsetof(MyStructDummy, type);
  MyStructDummy* dummy = malloc (sizeof *dummy + trailing_padding);
  memset(dummy->type, 42, trailing_padding); // write code 42 to all bytes
  
  printf("Size: %zu\n", sizeof(MyStructDummy));
  printf("Address of struct:%p\n", dummy);
  printf("Address of type:%p\n", dummy->type);

  unsigned char* endptr = (unsigned char*)dummy + sizeof(*dummy) - 1;
  printf("Value of last byte: %d", *endptr);

输出:

Size: 12
Address of struct:0xa842a0
Address of type:0xa842a9
Value of last byte: 42

【讨论】:

以上是关于确保 char 存在于结构的末尾的主要内容,如果未能解决你的问题,请参考以下文章

无法安装 playwright:找不到使用 Playwright 的项目。确保项目或解决方案存在于

如果在文件末尾省略PHP块的结束标记,是否会删除任何换行符或空格(如果存在于相应的文件中)?

Typescript - 确保泛型属性存在于具有描述性错误的泛型类型上

SQL - 确保在一组关键密钥对中表示的两个实体都存在于最终数据集中的有效方法

如果它存在于vim中,则删除行开头的特定char

如何确保在 systemd 中启动服务之前存在延迟?