【问题标题】:Where could I use struct compatibility?我在哪里可以使用结构兼容性?
【发布时间】:2014-09-26 00:03:30
【问题描述】:

我在this 页面上阅读了有关 C 中的类型提升的信息,并发现了有关 struct/union/enum 的更多信息:

6.12.2 单独编译兼容性

由于每次编译可能会查看不同的源文件,因此大多数 跨不同编译的兼容类型的规则是 结构性:

匹配的标量(整数、浮点和指针)类型必须是 兼容,就好像它们在同一个源文件中一样。

匹配的结构、联合和枚举必须具有相同数量的 成员。每个匹配的成员必须有一个兼容的类型(在 单独的编译意义),包括位域宽度。

匹配的结构必须具有相同顺序的成员。命令 union 和 enum 成员的数量无关紧要。

匹配的枚举成员必须具有相同的值。

另一个要求是成员的姓名,包括 未命名成员缺少名称,结构匹配,联合,以及 枚举,但不一定是它们各自的标签。

我的问题:结构“等于”在什么意义上?在哪里以及为什么可以使用它?谁能给个代码示例?

显然,如果没有强制转换,下面的代码不会在没有警告的情况下编译。而且由于 C 不是一种强类型语言,它可以与任何 种类 的结构一起使用。所以我真的不知道它在哪里有用;

struct foo_t
{
    int a;
    int b;
};

struct baa_t
{
    int a;
    int b;
};

void print(struct foo_t*);

int main(void)
{
    struct foo_t a = {1,2};
    struct baa_t b = {3,4};

    print(&a);
    print(&b); //cast needed
    return 0;   
}


void print(struct foo_t *f)
{
    printf("a = %d\r\n", f->a);
    printf("b = %d\r\n", f->b);
}

【问题讨论】:

  • C 不是强类型的?
  • @mafso 强类型可以 mean many different things 但它通常与 static 类型不同,您现在可能正在考虑这种类型。
  • @mafso:是的。任何指针都会转换为其他指针类型,只需强制转换即可。
  • 好吧,指针转换是一个显式请求忽略静态类型(并且需要因为 C是强类型),至少,我如何理解这个词。但是,是的,我看到了定义问题……我只是在结合代码时有点困惑(这是一个由于类型不匹配而无法工作的示例,所以至少它不是 C 被弱类型化的示例)。

标签: c types struct enums


【解决方案1】:

这主要涉及让多个 .cpp 文件引用同一个头文件并使用它的结构(或者更糟的是,两者都独立定义它)。

每个 .cpp 文件都将在一个完全独立的进程中编译,并且彼此都不知道(忽略 LTCG),但它们都将以相同的方式处理结构,因此在一个 .cpp 文件中使用的结构将具有与另一个完全相同的布局。

同样适用于结构内部的结构 - 一个 .cpp 文件可能有一个包含另一个结构的结构,而另一个 .cpp 可能只暴露给指向内部结构的指针,而不知道它周围的内容。不过,其他 .cpp 需要能够对其进行操作,因此数据成员的实际布局需要完全匹配。

【讨论】:

  • 因此,如果我有两个 .c 文件(两个翻译单元)具有相同的标签名称但该结构中的不同成员,编译器将比较它们发现差异并给出错误。如果没有区别,它就会起作用,对吗?
  • 编译器不一定会比较它们(并且通常不能在单独编译为目标文件时进行比较,这就是为什么标题通常用于声明在单独的翻译单元中使用的类型,以确保它们相等在所有这些中)。它们必须匹配,否则行为未定义。
【解决方案2】:

C 语言是在目标文件链接系统非常原始的时候创建的。不幸的是,在许多情况下,它们仍然是。

假设下面的.C文件

struct sheep1 {int16_t a, b;}
struct sheep2 {int16_t a; unsigned char b[2];}
int foo1(struct sheep1 bah) { return a+(b & 255)+(b >> 8); }
int foo2(struct sheep2 bah) { return a+b[0]+b[1]; }
int foo2(struct sheep2 bah) { return a+b[0]+b[1]; }

从不同的 .C 文件调用

struct sheep1 {int16_t a, b;}
struct sheep2 {int16_t a; unsigned char b[2];}
extern int foo1(struct1 sheep bah);
extern int foo2(struct2 sheep bah);
extern int foo3(struct1 sheep bah);
...
struct sheep1 marysLamb;
struct sheep2 marysLamb;
...
foo1(marysLamb1);
foo2(marysLamb2);
foo3(marysLamb1);

请注意,sheep1sheep2 都是四个字节长,并且都具有相同的对齐要求。尽管如此,对foo3 的调用可能会以惊人的方式失败。

foo1 生成的代码可能期望调用者将bah 的字段放入两个CPU 寄存器,因为这比将它们放入内存要快。当调用代码调用foo1时,它会按预期将值放入寄存器中,一切都会正常运行。

因为结构 sheep2 包含一个数组,调用约定可能不允许它在寄存器中传递(即使它足够小,可以容纳,编译器通常不具备寻址寄存器中的单个字节的能力)。因此,调用者需要将其放入堆栈上的四个字节的物理内存中。当期望这个方法被代码调用时(就像调用foo2的情况一样),调用者将把四个字节放在堆栈上,被调用的方法将使用并(在某些环境中)删除。即使foo2 的调用方式必须与foo1 不同,编译器也会知道这一点并相应地生成代码。

然而,调用foo3 可能会导致失败。编译器会将marysLamb1 的字段加载到寄存器中并调用foo3。然而,该函数将期望调用者将四个字节的数据压入堆栈。因此,它将从堆栈中读取 4 个字节的 who-knows-what,对它们执行指定的算术运算。然后,在某些环境中,它可能会在返回时从堆栈中弹出这四个字节。如果调用者在这四个字节的堆栈中有一些重要的东西(通常是这种情况),那么不知道会发生什么。

在最初设计 C 的系统上,链接器对 foo1、foo2 和 foo3 方法一无所知,除了它们的名称和代码所在的位置。然而,一些较新的系统为了避免上述问题,定义了编译器可以提供更多关于参数方法期望从调用者那里得到什么或调用者将要提供的参数的信息的方法;然后,链接器可以利用此信息在调用者未提供正确内容的情况下提供警告或错误。 C 标准的维护者不希望阻止这种努力,但他们希望避免链接器对代码发出尖叫的情况——如果不是这样的尖叫——工作得很好。在存在潜在差异的情况下,标准有必要指定允许链接器“挑剔”的情况以及链接器即使看起来“可疑”也应该接受的情况。

【讨论】:

    猜你喜欢
    • 2013-05-01
    • 2010-12-08
    • 2011-07-21
    • 1970-01-01
    • 2019-04-12
    • 2012-11-01
    • 2020-03-13
    • 2011-05-22
    • 1970-01-01
    相关资源
    最近更新 更多