作用域中的结构体和函数定义

30

据我所知,这在C语言中是合法的:

foo.c

struct foo {
   int a;
};

bar.c

struct foo {
    char a;
};

但是对于函数来说,这样做是不合法的:

foo.c

int foo() {
    return 1;
}

bar.c

int foo() {
    return 0;
}

这会导致链接错误(函数foo的多重定义)。

为什么会这样?C语言无法处理结构体名称和函数名称之间的差异。是否扩展到C ++?


10
您混淆了声明和定义。struct是一个声明,它声明了一个新的数据类型,但并不创建任何新的对象。相反,函数是被定义的(您创建了一个新的函数实体)。 - DYZ
可能是 https://dev59.com/JZDea4cB1Zd3GeqPXiau 的重复问题。 - DYZ
@DyZ 你是在说这里声明了struct foo但没有定义它吗? - curiousguy
6个回答

25

为什么会这样呢?

struct foo {
   int a;
};

定义了一个创建对象的模板,不会创建任何对象或函数。除非struct foo 在你的代码中使用过,否则对于编译器/链接器而言,这些代码行就好像不存在一样。

请注意,C和C++在处理不兼容的struct定义方面有所区别。

在C程序中,你发布的代码中struct foo的不同定义是可以的,只要不混合它们的使用即可。

但是,在C++中是不合法的。在C++中,它们具有外部链接并且必须被相同地定义。有关详细信息,请参见3.2 One definition rule/5


但是如果我在foo.c中使用struct foo,它将使用该定义,如果我在bar.c中使用它,它将使用来自bar的定义,不是吗? - V0ldek
2
@V0ldek,是的,那是正确的。如果你不混合它们,也就是不把来自foo.c的foo传递给期望来自bar.c的foo的函数,那么你应该没问题。 - R Sahu
那么行为是什么?编译器会使用本地定义而不是外部定义吗?因为如果我在同一个文件中声明两次 struct foo,肯定会出错。 - V0ldek
@V0ldek,struct 没有外部定义这种东西。它只是编译 .c 文件时编译器所看到的内容。 - R Sahu
2
在C++中,违反了ODR - SergeyA

19
在这种情况下,区分概念称为“链接(linkage)”。
在C语言中,结构体、联合体或枚举类型的标签没有链接。它们实际上是局部于其作用域的。根据6.2.2标准中的定义,除了被声明为对象或函数之外,其他任何声明的标识符都没有链接;被声明为函数参数的标识符;用未带有存储类说明符extern的对象声明的块作用域标识符。它们不能在同一作用域内重新声明(除了所谓的前向声明)。但是,它们可以在不同的作用域中自由地重新声明,包括不同的翻译单元。在不同的作用域中,它们可能声明完全独立的类型。这就是你在例子中看到的:在两个不同的翻译单元(即两个不同的文件作用域)中,你声明了两个不同、不相关的struct foo类型,这是完全合法的。
与此同时,在C语言中,函数具有链接性。在你的例子中,这两个定义定义了同一个具有外部链接的foo函数。你不允许在整个程序中提供任何具有外部链接的函数的多个定义。根据6.9标准中的定义,“如果用external关键字声明了一个标识符,那么在程序的其他文件中该标识符也要被声明一次,并且只能定义一次。”如果在表达式中使用了 linkage (除了作为 sizeof_Alignof 运算符的操作数之一且其结果是整数常量),那么整个程序中必须有一个标识符的外部定义;否则,不应有超过一个。


C ++中扩展了链接的概念:它将特定的链接分配给更广泛的实体,包括类型。在C ++中,类类型具有链接性。在命名空间范围内声明的类具有外部链接。C++的"一个定义规则"明确规定,如果具有外部链接的类在多个翻译单元中有几个定义,则应在所有这些翻译单元中等效地定义它们( http://eel.is/c++draft/basic.def.odr#12 )。因此,在C ++中,您的struct定义将是非法的。

由于C++ ODR规则(但本质上与C中相同的原因),您的函数定义在C++中仍然是非法的。


第6.9.5节是否意味着sizeof_Alignof运算符的操作数可以是具有超过一个定义的外部链接标识符?还是它暗示这些运算符的操作数不需要是明确定义的标识符?也就是说,已声明但未定义的标识符可以传递给这些运算符。 - Nicholas Cousar
1
@Nicholas Cousar:这意味着不需要定义。 - AnT stands with Russia

13

你的函数定义都声明了一个名为foo的具有外部链接性质的实体,C标准规定,具有外部链接性质的实体不得有超过一个的定义。你定义的结构体类型不是具有外部链接性质的实体,因此可以有多个struct foo的定义。

如果你使用相同的名称声明具有外部链接性质的对象,那么将会出现错误:

foo.c

struct foo {
   int a;
};
struct foo obj;

bar.c

struct foo {
    char a;
};
struct foo obj;

现在你有两个名为obj的对象,它们都具有外部链接,这是不允许的。

即使其中一个对象仅被声明而不被定义,这仍然是错误的:

foo.c

struct foo {
   int a;
};
struct foo obj;

bar.c

struct foo {
    char a;
};
extern struct foo obj;

由于obj的两个声明引用同一对象,但它们的类型不兼容(因为struct foo在每个文件中的定义不同),所以这是未定义的。

C++有类似的规则,但更加复杂,以考虑inline函数和inline变量、模板和其他C++特性。在C++中,相关要求被称为单一定义规则(ODR)。一个值得注意的区别是,即使两个不同的struct定义从未用于声明具有外部链接或在翻译单元之间“共享”的对象,C++也不允许这样做。


4

struct foo的两个声明在类型不同的成员之间不兼容。只要你不做任何事情来混淆它们,在每个翻译单元中都可以同时使用它们。

例如,如果你这样做:

foo.c:

struct foo {
   char a;
};

void bar_func(struct foo *f);

void foo_func()
{
    struct foo f;
    bar_func(&f);
}

bar.c:

struct foo {
   int a;
};

void bar_func(struct foo *f)
{
    f.a = 1000;
}

您将会调用 bar_func 期望的结构体 struct foo,但是 foo_func 提供的 struct foo 不兼容,这将导致 未定义行为
结构体的兼容性详见 C 标准第 6.2.7 节:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

1 如果两个类型的类型相同,则它们具有兼容的类型。判断两个类型是否兼容的其他规则在类型说明符的 6.7.2 节,类型修饰符的 6.7.3 节和声明符的 6.7.6 节中描述。此外,如果在不同的翻译单元中声明了两个结构体、联合体或枚举类型,则它们的标记和成员满足以下要求时它们是兼容的:如果其中一个带标记,则另一个也必须带有相同的标记。如果两个都在其各自的翻译单位内任何地方完成,则适用以下附加要求:应当存在一对一的对应关系,使得每一对相应的成员都用兼容类型声明;如果一对成员中的一个带有对齐说明符,则另一个带有等效的对齐说明符;如果一对成员中的一个带有名称,则另一个使用相同的名称声明。对于两个结构体,相应成员应在相同的顺序中声明。对于两个结构体或联合体,相应的位域应具有相同的宽度。对于两个枚举类型,相应的成员应具有相同的值。

2 所有引用同一对象或函数的声明必须具有兼容类型;否则,行为未定义。

总而言之,两个 struct foo 实例的成员必须相同且按相同顺序,才能互相兼容。
这样的规则是为了使得结构体可以在头文件中定义一次,并在多个源文件中引用该头文件。这将导致结构体在多个源文件中定义,但每个实例是兼容的。

3
这里的区别并不在于名称,而是存在性;结构体定义不存储在任何地方,它的名称仅在编译期间存在。
(程序员有责任确保相同名称的结构体没有冲突。否则,我们亲爱的老朋友“未定义行为”就会出现。)
另一方面,函数需要被存储在某个地方,如果它具有外部链接性,则链接器需要其名称。
如果将您的函数声明为静态函数,这样它们在各自的编译单元之外是“不可见的”,链接错误就会消失。

我相信在C语言中,如果两个不兼容的类型定义使用相同的名称,只要没有使用该名称的两个声明引用同一实体,则不会出现未定义的情况。例如,没有使用不同类型定义的对象或具有外部链接的函数的声明。 - Jonathan Wakely
@JonathanWakely 我的意思是在“使用中的冲突”方面,但我想我表达得不够清楚。 - molbdnilo
现在我再读一遍,明白了,抱歉对已经正确的事情挑剔! - Jonathan Wakely

2
为了让链接器忽略函数定义,请使用关键字static。
foo.c
    static int foo() {
        return 1;
    }

bar.c

    static int foo() {
        return 0;
    }

它是如何回答这个问题的? - SergeyA
1
它解决了问题。是的,我看到这里有一个问题而不是一个问题。 - armagedescu
好的,待添加。 - IS4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接