extern "C" 内联函数

8
这段代码会导致未定义行为吗? header.h
#ifdef __cplusplus
extern "C"
{
#endif

inline int foo(int a)
{
    return a * 2;
}

#ifdef __cplusplus
}
#endif

def.c

#include "header.h"

extern inline int foo(int a);

use.c

#include "header.h"

int bar(int a)
{
    return foo(a + 3);
}

main.cpp

#include <stdio.h>
#include "header.h"

extern "C"
{
    int bar(int a);
}

int main(int argc, char** argv)
{
    printf("%d\n", foo(argc));
    printf("%d\n", bar(argc));
}

这是一个示例程序,在C和C++中都需要使用inline函数。如果在C中删除def.c并且不使用foo,它会起作用吗? (假设C编译器是C99)

该代码可在以下编译器上编译成功:

gcc -std=c99 -pedantic -Wall -Wextra -c -o def.o def.c
g++ -std=c++11 -pedantic -Wall -Wextra -c -o main.o main.cpp
gcc -std=c99 -pedantic -Wall -Wextra -c -o use.o use.c
g++ -std=c++11 -pedantic -Wall -Wextra -o extern_C_inline def.o main.o use.o

foo 只在 extern_C_inline 中出现一次,因为编译器在不同的目标文件中输出的不同版本会被合并,但我想知道这种行为是否由标准指定。如果我删除 fooextern 定义,并将其定义为 static,那么 foo 将在每个编译单元中都出现在 extern_C_inline 中多次。


5
是的,但它的语义略有不同。 - Deduplicator
4
在C语言中,你需要在某处使用extern inline定义,以确保非内联版本出现在目标文件中。请参见https://dev59.com/RXVC5IYBdhLWcg3wrDNd。 - qbt937
1
如果我将foo声明为静态,那么它将在可执行文件中出现多次,这将增加可执行文件的大小。 - qbt937
1
@jxh "将其设为静态"通常不是一个好的解决方案,它会产生其他后果并改变程序的语义(例如,如果在foo()中有一个静态局部变量,则将foo设置为静态意味着您在每个翻译单元中都会得到一个不同的静态局部变量,而不是整个程序的单个静态局部变量)。只有在有意义的情况下使用static,而不仅仅是为了解决链接器错误。 - Jonathan Wakely
1
有趣的是,这正是我不喜欢它的原因。宏很糟糕。 - Jonathan Wakely
显示剩余15条评论
1个回答

8
该程序作为书写的是合法的,但需要def.c以确保代码始终能在所有编译器和不同文件的任意优化级别下运行。
由于其中一个声明带有extern,因此def.c提供了foo()函数的外部定义,您可以使用nm进行确认。
$ nm def.o
0000000000000000 T foo

无论如何,def.o 中的定义总是存在的。

use.c 中有一个内联定义foo(),但根据C标准6.7.4的规定,调用 foo() 是使用该内联定义还是使用外部定义都是不确定的(实际上它是否使用内联定义取决于文件是否被优化)。如果编译器选择使用内联定义,则可以正常工作。 如果它选择不使用内联定义(例如因为没有进行优化编译),则需要在其他文件中使用外部定义。

没有优化时,use.o 中有一个未定义的引用:

$ gcc -std=c99 -pedantic -Wall -Wextra -c -o use.o use.c
$ nm use.o
0000000000000000 T bar
                 U foo

但是通过优化,它就不会发生这种情况:
$ gcc -std=c99 -pedantic -Wall -Wextra -c -o use.o use.c -O3
$ nm use.o
0000000000000000 T bar

main.cpp中将定义foo(),但通常会生成一个弱符号,因此如果在另一个对象中找到了另一个定义,则可能不会被链接器保留。如果存在弱符号,则可以满足use.o中对外部定义的任何可能引用,但是如果编译器在main.o中内联foo(),则可能不会在main.o中发出任何foo()的定义,因此仍需要在def.o中找到定义以满足use.o 在没有优化的情况下,main.o包含一个弱符号:
$ g++ -std=c++11 -pedantic -Wall -Wextra -c -o main.o main.cpp
$ nm main.o
                 U bar
0000000000000000 W foo
0000000000000000 T main
                 U printf

然而,使用-O3编译main.cpp会内联调用foo,编译器不会为其发出任何符号:

$ g++ -std=c++11 -pedantic -Wall -Wextra -c -o main.o main.cpp -O3
$ nm main.o
                 U bar
0000000000000000 T main
                 U printf

因此,如果在use.o中没有将foo()内联,但在main.o中进行了内联,则需要在def.o中使用外部定义。

如果从C中移除def.c并且未在C中使用foo,它会起作用吗?

是的。如果只在C++文件中使用foo,则不需要在def.o中使用foo的外部定义,因为main.o要么包含自己的(弱)定义,要么会内联函数。仅当其他C代码中存在非内联调用foo时,才需要foo.o中的定义。
此外:当优化main.o时,C++编译器允许跳过为foo生成任何符号,因为C++标准规定,在一个翻译单元中声明为inline的函数必须在所有翻译单元中声明为inline,并且调用声明为inline的函数时,定义必须在与调用相同的文件中可用。这意味着编译器知道如果其他文件想调用foo(),那么该其他文件必须包含函数的定义,因此当编译其他文件时,编译器将能够生成另一个函数的弱符号定义(或将其内联),如有需要。因此,如果在main.o中的所有调用都已经内联,那么无需在main.o中输出foo。
这与C的语义不同,其中在use.c中的内联定义可能会被忽略,而即使def.c中没有调用它,也必须存在外部定义def.o中。

为什么回答“这里没有涉及C编译器”是不够的呢?因为重点在于C链接,而不是C编译器,即使使用extern "C"编写的代码也会被C++编译器编译。 - user2485710
1
@user2485710,这并不是真的。def.cuse.c都是由C编译器编译的。 - Jonathan Wakely
@JonathanWakely 当然可以,但是在一个以 extern "C" 开头的问题中,这是一个仅适用于 C++ 的构造,这个东西会被一个普通的 C 编译器编译多少次?我认为这个问题受到 OP 假设即使在 C++ 代码库中使用该代码也会被 C 编译器编译的影响。 - user2485710
1
@user2485710,你认为#ifdef __cplusplus检查是用来干什么的?我觉得你有些困惑了。OP显然是在使用C编译器编译def.cuse.c,因为问题中已经表明了这一点,可以看到末尾的gcc -std=c99命令。 - Jonathan Wakely
感谢您的出色回答!这绝对回答了我的问题。 - qbt937

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