extern "C" - 应该放在库头文件之前还是之后?

5

我正在编写一个C语言库,可能对于编写C++程序的人很有用。这个库有一个头文件,长这样:

#ifndef FOO_H_
#define FOO_H_

#include <bar.h>

#include <stdarg.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

void foo_func();

#ifdef __cplusplus
}
#endif
#endif 

我在思考——是否应该在包含头文件之前移动 extern "C" 位?特别是在实践中,一些头文件本身可能有 extern "C"


3
标准库头文件中已经在必要时包含了这个内容,请为您自己的声明担心。 - user207421
@user207421:我稍微扩大了问题范围,这样答案可能可以涵盖标准库和其他头文件。此外,标准库头文件在必要时具有extern C的保证吗?这是语言标准的要求吗? - einpoklum
1
一些头文件被明确地写成在最外层的“作用域”具有C++语言链接的假设下。它们可能使用__cplusplus来移除模板声明,如果您像希望的那样将它们包装起来,您的头文件将基本上无法使用。因此,如已经提到的那样,确保您的声明正确,并让其他头文件自由地进行它们的操作。即使是标准库头文件也可能会出现问题(因为实现者可能会认为它们将被共享,然后在内部执行一些专家友好的操作)。 - StoryTeller - Unslander Monica
3个回答

2

一般情况下,不应该将其移动以包含头文件。

extern "C"用于指示函数使用C调用约定。声明对变量和#define没有影响,因此不需要包含它们。如果#includeextern "C"块内部,则这实际上修改了该头文件内部的函数声明!

背景:如果没有extern "C"声明,则在使用C编译器进行编译时,假定函数遵循C约定,在使用C++编译器进行编译时,则假定遵循C++约定。如果相同的头文件用于C和C++代码,则会出现链接器错误,因为编译的函数在C和C++中具有不同的名称。

虽然可以将所有代码放在#ifdef块之间,但我个人不喜欢这样做,因为它实际上只适用于函数原型,并且我经常看到人们将其复制粘贴到不应该的位置。最清晰的方法是将其保留在C / C ++头文件中的函数原型周围。

所以,回答你的问题“我应该将extern"C"位移到包含头文件指令之前吗?”,我的答案是:不,你不应该。

但是,这是可能的吗?是的,在许多情况下,这不会破坏任何东西。有时它甚至是必要的,如果外部头文件中的函数原型不正确(例如,当它们是C函数并且您想从C ++调用它们),并且您无法更改该库。

然而,也有一些情况下这样做会破坏构建。以下是一个简单的示例,如果使用extern "C"包装include,则编译失败:

foo.h:

#pragma once

// UNCOMMENTING THIS BREAKS THE BUILD!
//#ifdef __cplusplus
//extern "C" {
//#endif

#include "bar.h"

bar_status_t foo(void);

//#ifdef __cplusplus
//}
//#endif

foo.c:

#include <stdio.h>
#include "foo.h"
#include "bar.h"

bar_status_t foo(void)
{
    printf("In foo. Calling bar wrapper.\n");
    return bar_wrapper();
}

bar.h:

#pragma once

typedef enum {
    BAR_OK,
    BAR_GENERIC_ERROR,
    BAR_OUT_OF_BEAR,
    // ...
} bar_status_t;

extern "C" bar_status_t bar_wrapper(void);
bar_status_t bar(void);

bar.cpp:

#include <iostream>
#include "bar.h"

extern "C" bar_status_t bar_wrapper(void)
{
    std::cout << "In C/C++ wrapper." << std::endl;
    return bar();
}

bar_status_t bar(void)
{
    std::cout << "In bar. One bear please." << std::endl;
    return BAR_OK;
}

main.cpp:

#include <stdio.h>
#include <stdlib.h>
#include "foo.h"
#include "bar.h"

int main(void)
{
    bar_status_t status1 = foo();
    bar_status_t status2 = bar();
    return (status1 != BAR_OK) || ((status2 != BAR_OK));    
}

当我在中取消注释这些块时,我遇到了以下错误:
main2.cpp:(.text+0x18): undefined reference to `bar'
collect2.exe: error: ld returned 1 exit status
Makefile:7: recipe for target 'app2' failed

如果没有这个问题,它可以顺利构建。一个C主函数只调用来自foo和bar的C函数,无论哪种方式都可以顺利构建,因为它不受#ifdef __cplusplus块的影响。


只有在您包含的外部库是用C编写的且不包含所提到的C++指令时,才需要这样做。嗯,如果我包含一个外部库,显然它是用C编写的(否则我就不会包含它)。现在,如果它包含extern C - 那么嵌套它不会有影响;如果它不包含extern C - 那么使用extern C覆盖包含应该有帮助。因此,我不理解您的论点。 - einpoklum
“嗯,如果我要包含一个外部库,显然它是用C语言编写的(否则我就不会包含它了)。”为什么呢?你也可以包含一个C++库。我不知道你当前是在使用C还是C++进行编程,因为这个问题被标记为两者都有,但这并不重要,“extern C”只有在混合两种语言时才需要。 - wovano
@wovano:为什么? 因为 foo 是用 C 语言编写的。它的源代码只包括其他 C 语言头文件。但我会在问题文本中澄清这一点。 - einpoklum
@einpoklum,如果foo是用C语言编写的,那么您当前的头文件确实是我会编写的方式 :-) - wovano

0

令人惊讶的是。现在即使是我,也会遵循标准来编写代码。

#ifndef FOO_H_
#define FOO_H_

#ifdef __cplusplus
extern "C" {
#endif

#include <bar.h>

#include <stdarg.h>
#include <stddef.h>


void foo_func();

#ifdef __cplusplus
}
#endif
#endif 

有两个原因:

1. 嵌套使用extern "C"没有问题,所以你的bar.h包含文件是可以的。这样看起来更清晰、更重要。

2. 为了真正做到可移植性,你需要为C++用户在你的C头文件周围包装一个extern "C",如果他们不想要的话。

因为我刚刚查阅了C++标准,16.5.2.3链接[using.linkage]说明如下:

从具有外部链接的C标准库中声明的名称是否具有extern "C"或extern "C++"链接是实现定义的。建议实现使用extern "C++"链接来实现此目的。1

为了安全起见,你确实应该在这些包含文件周围包装一个extern "C",但是你不必这样做,因为这意味着在D.9 C头文件[depr.c.headers]中,例如你正在使用的<stdlib.h>


1
我认为,“implementation”指的是编译器和相应的库和头文件。它并不是指你和我(假设你不是在写编译器;p)。我认为这意味着C++编译器可以使用C++链接实现标准C库,但相应的头文件应该与代码对应。因此,它不会对使用这些库(和头文件)的应用程序产生任何影响。 - wovano
2
“令人惊讶的是,是的。”...在我看来,那将是一个非常糟糕的实现。我可能不想将标准C头文件放在C++“保护”内部,这样它就会崩溃,我就可以说:“我们不支持你的实现。” - Andrew Henle
1
啊,是的,Stackoverflow,那里的观点比来自来源/参考文献/文档/标准的证据或结论更有价值。 - Superlokkus
1
我也感到惊讶。我给出了一个答案,证明始终使用extern "C"包装头文件可能会破坏构建。另一个答案提供了一个可靠的来源链接,其中问题得到了清晰的回答。然而,一个提到“看起来更重要”的原因的答案获得了更多的赞... - wovano
1
@wovano 嘿,你的回答比我的赞更多(至少总数上是这样):-)。问题是,有时即使来自可靠资源的好FAQ也不知道一些标准惊喜,特别是对于很少相关/使用的东西。但我承认可靠的FAQ表明我的解释是错误的。然而,有些人在没有查看标准或实现文档/官方讨论的情况下就声称某些东西。我和你没有争吵 :-) - Superlokkus
显示剩余3条评论

0

这个问题在C++ FAQ中有详细解答。

首先,如何在C++代码中包含一个标准的C头文件? 不需要特别处理,标准的C头文件可以与C++无缝衔接。因此,你不需要stdarg.hstddef.h中加入extern "C"

然后,对于非标准的C头文件,有两种可能性:要么你不能更改头文件,要么你可以更改头文件。

当你 不能更改C头文件时,将#include包装在extern "C"中。

// This is C++ code
extern "C" {
  // Get declaration for f(int i, char c, float x)
  #include "my-C-code.h"
}
int main()
{
  f(7, 'x', 3.14);   // Note: nothing unusual in the call
  // ...
}

当你可以更改标题时,编辑它以在标题本身中有条件地包含extern "C"

#ifdef __cplusplus
extern "C" {
#endif

. . .

#ifdef __cplusplus
}
#endif

在您的情况下,关键是您是否控制bar.h的内容。如果这是您的头文件,则应修改它以包括extern "C",并且不要#include本身包装起来。

无论何时/何地/如何包含头文件,都应该以相同的方式工作。将#include包装在extern "C"#pragma pack、特殊的#define等中,应该保留为最后的解决方法,因为这可能会干扰头文件在不同场景下的行为,从而降低系统的可维护性。

正如StoryTeller在评论中所说:

有些头文件明确地基于外层的“作用域”具有 C++ 语言链接进行编写。它们可能使用 __cplusplus 删除模板声明,如果您像希望的那样对其进行包装,则您的头文件将基本上无法使用。因此,正如已经提到的,要使您的声明正确,并让其他头文件自由地完成它们的任务。即使是标准库头文件也可能会中断(因为实现者可能会推断它将被共享,然后在内部做一些专家友好的事情)。
请注意,标准 C 头文件可以使用 C 语言链接进行实现,但也可能使用 C++ 语言链接进行实现。在这种情况下,将它们包装在 extern "C" 中可能会导致链接错误。

那个常见问题解答是针对使用C++编写的库“用户”的,而我是使用C编写的库“作者”。 - einpoklum
我认为C++标准中的§16.5.2.3 Linkage [using.linkage]比isocpp网站上的常见问题解答更具权威性。 - Superlokkus
@einpoklum,FAQ也是针对库编写者的。你的问题基本上在这个问题中得到了回答:如何修改自己的C头文件以便更容易地在C++代码中#include它们?(上面的问题描述了如果要包含的头文件没有按照这种方式编写时,你可以做什么来解决问题。) - wovano
@Superlokkus [using.linkage]/2说:“C标准库中具有外部链接的名称是否具有extern“C”或extern“C ++”链接是实现定义的。”这意味着我们无法知道标准库使用的链接方式,因此应避免在任何extern中包装它。 标准和常见问题解答之间不存在差异。只是不要这样做。 - rustyx

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