C和C++之间链接的区别是什么?

10

我已经阅读了SO上关于外部链接和内部链接的现有问题。我的问题不同——如果我在不同的翻译单元中使用具有外部链接的相同变量的多个定义,会发生什么事情,在C和C++下是如何处理的?

例如:

/*file1.c*/

typedef struct foo {
    int a;
    int b;
    int c;
} foo;

foo xyz;


/*file2.c*/

typedef struct abc {
    double x;
} foo;

foo xyz;

使用Dev-C++作为C程序,上述程序可以完美编译和链接;但如果将其编译为C++程序,则会出现多次重新定义的错误。为什么在C下可以工作,在C++下有何差别?这种行为是否未定义且取决于编译器?这段代码有多“糟糕”,如果我想重构它(我已经遇到了许多像这样旧代码),我应该怎么做?

5个回答

4

在任何程序中,C和C ++都有“一个定义规则”,即每个对象只能定义一次。违反此规则将导致“未定义的行为”,这意味着编译时可能会或可能不会看到诊断消息。

以下声明在文件范围内存在语言差异,但它与您的示例问题无关。

int a;

在C语言中,这是一个试探性的定义。它可以与同一翻译单元中的其他试探性定义合并成一个单一的定义。在C++中,它总是一个定义(如果要声明一个未定义的对象,必须使用extern),同一翻译单元中后续对同一对象的定义都是错误的。

在您的示例中,两个翻译单元都有一个(冲突的)从其试探性定义中定义的xyz


2
这是由于C++的名称重载引起的。根据维基百科的描述:

最初的C++编译器被实现为将代码翻译成C语言源代码,然后由C编译器将其编译成目标代码;因此,符号名称必须符合C标识符规则。即使在后来出现了直接生成机器代码或汇编代码的编译器,系统的链接器通常也不支持C++符号,因此仍需要进行名称重载。

关于兼容性
为了给编译器厂商更大的自由度,C++标准委员会决定不规定名称重整、异常处理和其他实现特定的功能的实现。这个决定的缺点是由不同编译器生成的目标代码预计是不兼容的。然而,对于特定的机器或操作系统,有第三方标准试图在这些平台上标准化编译器(例如C++ ABI[18]);一些编译器采用这些项目的次要标准。从http://www.cs.indiana.edu/~welu/notes/node36.html中给出了以下例子:
例如,对于下面的C代码
int foo(double*);
double bar(int, double*);

int foo (double* d) 
{
    return 1;
}

double bar (int i, double* d) 
{
    return 0.9;
}

它的符号表将会是(通过 dump -t):
[4]  0x18        44       2     1   0   0x2 bar
[5]  0x0         24       2     1   0   0x2 foo

对于同一个文件,如果使用g++编译,则符号表将会是:
[4]  0x0         24       2     1   0   0x2 _Z3fooPd
[5]  0x18        44       2     1   0   0x2 _Z3bariPd

"_Z3bariPd" 表示一个函数,其名称为 bar,第一个参数是整数,第二个参数是指向双精度浮点数的指针。

1

C++ 不允许一个符号被定义超过一次。不确定 C 链接器在做什么,但很可能的猜测是它只是将两个定义映射到同一个符号,这当然会导致严重的错误。

为了进行移植,我建议将单个 C 文件的内容放入匿名命名空间中,这基本上使得符号不同,并且局部于文件,因此它们不会与其他地方具有相同名称的符号产生冲突。


当然可以定义不止一次。但是这些定义必须是相同的。 - Potatoswatter
1
@Potatoswatter:对象必须仅被定义一次,但可以在多个地方进行声明。inline函数是特殊的,因为它们可以在每个翻译单元中定义一次,但其他函数必须在每个程序中仅被定义一次。 - CB Bailey

0

C程序允许这样做,并将内存视为联合体。它可以运行,但可能不会给您期望的结果。

C++程序(类型更强)正确检测到问题并要求您修复它。如果您真的想要一个联合体,请将其声明为一个联合体。如果您想要两个不同的对象,请限制它们的范围。


1
C语言的行为在你的实现上可能是正确的,但这并不被语言保证。 - CB Bailey
变量名只是内存地址的标签。如果您提供了两个解释该标签的定义,那并不会使该标签神奇地引用两个不同的对象。您见过会有不同行为的链接器吗? - Michael J
我不否认这是常见的链接器行为,其他语言和许多C实现也使用了这种行为。然而,从你的回答中可以看出,它是一种明确定义的行为。根据C标准Annex J,允许程序中有多个外部定义是一种常见的扩展,但即使使用了这种扩展,如果定义不一致,结果仍然是未定义的行为。 - CB Bailey

0

你发现了一个定义规则。很明显,你的程序有一个错误,因为

  • 程序链接后只能有一个名为foo的对象。
  • 如果一些源文件包括所有的头文件,它会看到两个foo的定义。

C++编译器可以通过“名称重整”来解决#1的问题:在链接程序中,您变量的名称可能与您选择的名称不同。在这种情况下,虽然不是必需的,但这可能是编译器检测问题的方式。但是#2仍然存在,所以您不能那样做。

如果你真的想打败安全机制,你可以像这样禁用名称重整:

extern "C" struct abc foo;

...其他文件...

extern "C" struct foo foo;

extern "C" 指示链接器使用 C ABI 约定。


当然,正如其他人提到的那样,你应该使用union - Potatoswatter

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