C 中的指针转换

Pointer Cast in C

我有以下三个struct

struct A {
  int a;
  int bOffset;
  int cOffset;
};

struct B {
  long long b;
  int other[];
};

struct C {
  long long c;
  int other[];
};

main 函数如下:

int main(void) {
  int otherSize = 0;
  scanf("%d", &otherSize);

  int aSize = sizeof(struct A);
  int bSize = sizeof(struct B) + sizeof(int) * otherSize;
  int cSize = sizeof(struct C) + sizeof(int) * otherSize;
  int totalSize = aSize + bSize + cSize;

  struct A *a = malloc(totalSize);
  a->bOffset = aSize;
  a->cOffset = aSize + bSize;

  struct B *b = (struct B*)((char*)a + a->bOffset);
  struct C *c = (struct C*)((char*)a + a->cOffset);

  ......
}

struct Astruct Bstruct C 的 space 一起分配,以显示更好的缓存行为。我的问题是,根据之前关于 SO 的帖子,演员

  struct B *b = (struct B*)((char*)a + a->bOffset);
  struct C *c = (struct C*)((char*)a + a->cOffset);

是 C 中的未定义行为,因为 struct Bstruct Cstruct A 具有更严格的对齐要求。 那我该怎么做才能在 C 中明确定义转换?

我现在能想到的是在 struct A 中添加一个 long long 变量,如下所示。

struct A {
  int a;
  int bOffset;
  int cOffset;
  long long unused;
};

另一个问题是如果我取消引用bc,它也是一个UB。有什么办法可以解决这个问题吗?

那我该怎么做才能在 C 中转换为 well-defined?

要正确计算 struct Bstruct C 的放置位置,您应该将之前的尺寸填充到必要的对齐位置。 C 提供了 _Alignof 运算符来提供类型的对齐要求。所以这段代码将完成这项工作:

/*  Calculate how many bytes are required to add to size s to make it be a
    multiple of alignment a.  If s is a multiple of a, this is zero.
    Otherwise, we need to add a-r bytes, where r is the remainder of s divided
    by a.

    Omitting the parentheses used for macro parameters, the following code is
    a - (s-1)%a - 1.  To see it works, consider two cases:


        s is a multiple of a.  Then s-1 is a-1 modulo a, and the expression
        evaluates to a - (a-1) - 1 = 0.

        s has some non-zero remainder r modulo a.  Then (s-1)%a evaluates to
        r-1, and the expression evaluates to a - (r-1) - 1 = a-r.
*/
#define PadToAlignment(s, a)    ((a) - ((s)-1) % (a) - 1)

…

    //  Add padding needed to align struct B and struct C correctly.
    aSize += PadToAlignment(aSize,         _Alignof (struct B));
    bSize += PadToAlignment(aSize + bSize, _Alignof (struct C));

备注

对于尺寸,您通常应该使用 size_t 而不是 int。此外,当使用带有 sizeof_Alignof 的类型时,我不喜欢像 sizeof(int) 那样将它们写成函数调用,因为它们不是函数调用。相反,它们是带有操作数的运算符,出于语法原因,该操作数是用圆括号括起来的类型名,因此 sizeof (int) 有助于提醒读者 C 代码的含义。

这是包含这些的完整程序:

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


/*  Calculate how many bytes are required to add to size s to make it be a
    multiple of alignment a.  If s is a multiple of a, this is zero.
    Otherwise, we need to add a-r bytes, where r is the remainder of s divided
    by a.

    Omitting the parentheses used for macro parameters, the following code is
    a - (s-1)%a - 1.  To see it works, consider two cases:


        s is a multiple of a.  Then s-1 is a-1 modulo a, and the expression
        evaluates to a - (a-1) - 1 = 0.

        s has some non-zero remainder r modulo a.  Then (s-1)%a evaluates to
        r-1, and the expression evaluates to a - (r-1) - 1 = a-r.
*/
#define PadToAlignment(s, a)    ((a) - ((s)-1) % (a) - 1)


struct A {
  int a;
  int bOffset;
  int cOffset;
};

struct B {
  long long b;
  int other[];
};

struct C {
  long long c;
  int other[];
};


int main(void)
{
    int otherSize = 0;
    if (1 != scanf("%d", &otherSize))
    {
        fprintf(stderr, "Error, scanf failed.\n");
        exit(EXIT_FAILURE);
    }

    size_t aSize = sizeof (struct A);
    size_t bSize = sizeof (struct B) + sizeof (int) * otherSize;
    size_t cSize = sizeof (struct C) + sizeof (int) * otherSize;

    //  Add padding needed to align struct B and struct C correctly.
    aSize += PadToAlignment(aSize,         _Alignof (struct B));
    bSize += PadToAlignment(aSize + bSize, _Alignof (struct C));

    size_t totalSize = aSize + bSize + cSize;

    unsigned char *RawMemory = malloc(totalSize);
    if (!RawMemory)
    {
        fprintf(stderr, "Error, unable to allocate memory.\n");
        exit(EXIT_FAILURE);
    }

    struct A *a = (struct A *) RawMemory;
    a->bOffset = aSize;
    a->cOffset = aSize + bSize;

    struct B *b = (struct B *) (RawMemory + a->bOffset);
    struct C *c = (struct C *) (RawMemory + a->cOffset);

    printf("a is at %p.\n", (void *) a);
    printf("b is at %p.\n", (void *) b);
    printf("c is at %p.\n", (void *) c);

    free(RawMemory);
}

另一个问题是,如果我取消引用 b 或 c,它也是一个 UB。

malloc分配的内存没有有效类型。通过该类型的左值存储数据,它可以用作任何类型。

C 标准关于涉及结构的动态分配内存中有效类型的规则不完整;自然语言措辞不足以写出正式的语义描述。当然很明显,如果 struct S 有成员 abc 而没有其他成员,我们做:

struct S *p = malloc(sizeof *p);
p->a = 3;
p->b = 4;
p->c = 5;

然后,出于别名的考虑,在内存地址 p 处应该有一个 struct S,即使内存只是部分写入,从来没有完整的 struct S 左值.但是标准关于有效类型的规则并没有明确说明这一点;他们根本不够。

在内存中放入多个结构会使这更复杂。但是,该标准的别名规则的目的是指定对象何时可以或不可以被别名(以及编译器可以针对这些进行哪些优化)。出于实际目的,只要您以正常方式使用这些结构(您为 struct A 指定的内存为 struct A,为 struct B 指定的内存为 struct B ,并且您为 struct C 指定的内存为 struct C),则您不会将内存与其他类型混为一谈,并且编译器不会执行意外的优化来破坏它。我希望以这种方式使用分配的内存是安全的。

标准的大部分内容是在考虑 type-based 别名之前编写的,并且使用的术语不足以明确指定以任何合理方式定义或未定义的内容(任何一致的阅读都将归类为未定义显然应该工作的构造,或者定义 clang 和 gcc 拒绝支持的构造,除非 type-based 别名被禁用)。

例如,给定定义:

struct s1 { int dat[4]; } *x1;
struct s2 { int dat[4]; } *x2;
int i;

标准明确指出左值 x1->dat[i]*(x1->dat+i) 是等价的(事实上,前者 定义 表示后者),并且建议期望 int* 的函数不能使用它来访问 x1->dat+ix2->dat+i 可互换的对象是荒谬的,但是 gcc 和 clang 都不会可靠地识别左值表达式 x1->dat[i] 可能访问类型 struct s2.

的对象

如果标准指定数组在与 [] 运算符一起使用时不会衰减,而是定义 [] 运算符对结构或联合的 array-type 成员的行为作为对包含结构或联合的一部分的访问,这将有可能解决标准中的许多歧义,但标准将所有此类构造视为未定义行为,并依赖于实现来判断哪些应该支持和不支持。