在 C 中可靠且可移植地存储和检索结构类型的对象

Reliably and portably store and retrieve objects of structure type in C

@bdonlan,在Copying structure in C with assignment instead of memcpy()中,列出了使用memcpy复制结构类型对象的几个原因。我还有一个原因:我想使用相同内存区域来存储和检索任意对象——可能不同结构类型——在不同的时间(比如存储在预分配的堆上)。

我想知道:

这是一个 MRE(排序:“M”[最小] 没有那么多,我基本上是在询问“R”[可重现]):

编辑:我希望在此之后放置一个更好的例子。我将此留在这里,以便为迄今为止的答案和评论提供参考。

// FILE: memcpy_struct.c

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

// EDIT: @john-bollinger POINTS OUT THAT THE FOLLOWING LINE
//  IS NOT PORTABLE.
// typedef struct { } structure ;
// INSTEAD:
typedef struct { char dummy ; } structure ;

typedef struct {
    unsigned long long u ; unsigned long long v ;
} unsignedLongLong2; // TWICE AS MANY BITS AS long long

typedef struct
{
    unsigned long long u ; unsigned long long v ;
    unsigned long long w ; unsigned long long x ;
} unsignedLongLong4; // FOUR TIMES AS MANY BITS AS long long

typedef unsigned char byte ;

void store ( byte * target , const structure * source , size_t size ) {
    memcpy ( target , source , size ) ;
}

void fetch ( structure * target , const byte * source , size_t size ) {
    memcpy ( target , source , size ) ;
}

const size_t enough =
    sizeof ( unsignedLongLong2 ) < sizeof ( unsignedLongLong4 )
    ? sizeof ( unsignedLongLong4 ) : sizeof ( unsignedLongLong2 ) ;

int main ( void )
{
    byte * memory = malloc ( enough ) ;
    unsignedLongLong2 v0 = { 0xabacadabaabacada , 0xbaabacadabaabaca } ;
    unsignedLongLong4 w0= {
        0xabacadabaabacada , 0xbaabacadabaabaca ,
        0xdabaabacadabaaba , 0xcadabaabacadabaa } ;
    unsignedLongLong2 v1 ;
    unsignedLongLong4 w1 ;
    store ( memory ,   ( structure * ) & v0 ,   sizeof v0 ) ;
    fetch ( ( structure * ) & v1 ,   memory ,   sizeof v1 ) ;
    store ( memory ,   ( structure * ) & w0 ,   sizeof w0 ) ;
    fetch ( ( structure * ) & w1 ,   memory ,   sizeof w1 ) ;
    char s [ 1 + sizeof w0 * CHAR_BIT ] ; // ENOUGH FOR TERMINATING NULL CHAR-
    char t [ 1 + sizeof w0 * CHAR_BIT ] ; // ACTERS + BASE-2 REPRESENTATION.
    sprintf ( s, "%llx-%llx",  v0 . u,  v0 . v ) ;
    sprintf ( t, "%llx-%llx",  v1 . u,  v1 . v ) ;
    puts ( s ) ;   puts ( t ) ;
    puts ( strcmp ( s , t ) ? "UNEQUAL" : "EQUAL" ) ;
    sprintf ( s, "%llx-%llx-%llx-%llx",  w0 . u,  w0 . v,  w0 . w,  w0 . x ) ;
    sprintf ( t, "%llx-%llx-%llx-%llx",  w1 . u,  w1 . v,  w1 . w,  w1 . x ) ;
    puts ( s ) ;   puts ( t ) ;
    puts ( strcmp ( s , t ) ? "UNEQUAL" : "EQUAL" ) ;
    free ( memory ) ;
}

编译为

gcc -std=c11 memcpy_struct.c # can do C99 or C17, too

相应可执行文件的输出

abacadabaabacada-baabacadabaabaca
abacadabaabacada-baabacadabaabaca
EQUAL
abacadabaabacada-baabacadabaabaca-dabaabacadabaaba-cadabaabacadabaa
abacadabaabacada-baabacadabaabaca-dabaabacadabaaba-cadabaabacadabaa
EQUAL

但是,如果遵守标准,是什么保证输出对 始终EQUAL我认为以下内容有帮助(N2176 类型 6.2.5-28):

All pointers to structure types shall have the same representation and alignment requirements as each other.

编辑:在考虑了答案和评论后,我认为以下是更好的MRE:

// FILE: memcpy_struct-1.c

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

typedef struct {
    size_t length ;
} array_header ;

typedef struct
{
    size_t capacity ;
    size_t length ;
} buffer_header ;

const size_t hsize_max =
    sizeof ( array_header ) < sizeof ( buffer_header )
    ? sizeof ( buffer_header ) : sizeof ( array_header ) ;

const size_t block = 512u ;

const size_t pageSize = block * ( 1 +
    ( hsize_max / block + ! ! hsize_max % block ) ) ;

int main ( void )
{
    void * memory = malloc ( pageSize ) ;
    array_header a0 = { 42u } ;
    buffer_header b0 = { 42u , 0u } ;
    array_header a1 ;
    buffer_header b1 ;
    memcpy ( memory ,     & a0 , sizeof a0 ) ;
    memcpy (   & a1 ,   memory , sizeof a1 ) ;
    memcpy ( memory ,     & b0 , sizeof b0 ) ;
    memcpy (   & b1 ,   memory , sizeof b1 ) ;
    fputs ( "array_header-s are " , stdout ) ;
    puts ( a0.length == a1.length ? "EQUAL" : "UNEQUAL" ) ;
    fputs ( "buffer_header-s are " , stdout ) ;
    puts ( b0.capacity == b1.capacity && b0.length == b1.length
        ? "EQUAL" : "UNEQUAL" ) ;
    free ( memory ) ;
}

既然你问的是可移植性和标准的规定,首先想到的是没有任何成员的结构类型,比如这个...

typedef struct { } structure ;

... 是 non-portable 扩展。您的 objective 似乎可以使用 structure * 作为通用 pointer-to-structure 类型,但是当您有 void * 作为通用 pointer-to-任何类型。使用 void *,您甚至可以自动获得指针转换,而无需显式强制转换。另请注意,当您调用 memcpy().

时,您最终还是会转换为 void *

I want to use the same area of memory to store and retrieve arbitrary objects—of possibly different structure type—at different times (like storage on a pre-allocated heap).

好的。这不是一个特别大的问题。

I want to know:

  • how this can be done portably (in the sense that the behavior defined by the Standard) and

你的例子很好。或者,如果您事先知道可能要存储的所有不同结构类型,则可以使用 union.

  • what parts of the Standard allow me to reasonably assume that it can be done portably.

以你的动态分配/memcpy()为例,有

  • C17 7.22.3.4/2:“malloc 函数为大小由 [ 指定的对象分配 space =19=]"

  • C17 6.2.4/2:“对象存在,具有恒定地址,并在其整个生命周期中保留其 last-stored 值。”

  • C17 7.22.3/1:“已分配对象的生命周期从 分配直到解除分配。"

  • C17 7.24.2.1/3:“memcpy 函数从 [= 指向的对象复制 n 个字符22=] 进入指向的对象 通过 s1."

因此,在仅表现出已定义行为的程序中,memcpy() 忠实地将所有指定字节从源对象的表示复制到目标对象的表示。该对象保持它们不变,直到并且除非它们被覆盖或生命周期结束。这使它们可供第二个 memcpy() 将它们从那里复制到其他对象。 memcpy 都不会改变字节序列,分配的对象忠实地保持它们之间,所以最后,所有三个对象——原始的、分配的和最终的目的地,必须包含相同的字节序列,向上复制的字节数。

如果您正在询问某种方法来“存储”一个结构,然后将相同的结构恢复到相同类型的对象中,那么仅复制字节就足够了。这可以通过 memcpy 来完成,并且不需要使用由不同数量的 unsigned long long 元素定义的结构进行任何拼凑。1 这是由 C 2018 保证的6.2.6.1 第 2 至 4 段:

2 Except for bit-fields, objects are composed of contiguous sequences of one or more bytes, the number, order, and encoding of which are either explicitly specified or implementation-defined.

3 Values stored in unsigned bit-fields and objects of type unsigned char shall be represented using a pure binary notation.

4 Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT bits, where n is the size of an object of that type, in bytes. The value may be copied into an object of type unsigned char [n] (e.g., by memcpy); the resulting set of bytes is called the object representation of the value…

因此,要存储除 bit-field 之外的任何结构或任何对象,请为其预留足够的内存2 并将对象的字节复制到该内存中。要恢复结构,请将字节复制回来。

关于:

I think the following helps (N2176 Types 6.2.5-28):

All pointers to structure types shall have the same representation and alignment requirements as each other.

那是无关紧要的。问题的代码中没有使用任何指针的表示,因此它们的表示(哪些字节构成了指针的记录值)是无关紧要的。

脚注

1 为什么要使用不同名称的多个成员?要定义一个包含 <i>N</i> unsigned long 元素的结构,您只需要 struct { unsigned long long x [<i>N</i>]; }.

2 对于一个对象 X,这可以用 void * Memory = malloc(sizeof X) 或者,如果你的编译器支持可变长度数组,用 unsigned char Memory[sizeof X];,或者,如果你想把它放在一个结构中,struct { unsigned char x[sizeof X]; } Memory;.