我的项目用例如下:
- 带有几个声明的外部函数的 C 头文件
- 定义了这些函数的 C++ 源文件
- 将 C++ 源代码编译为共享库
- 在 C 源文件中,通过链接 .so 使用 C 头文件中声明的函数
我在做这件事时发现了一些奇怪的事情。事情开始与未命名的命名空间一起工作。
这是我的示例文件:
c_sample.h
:
#include "stddef.h"
extern void hello(void);
extern void bye(void);
cpp_sample.cc
:
#include <iostream>
#include "c_sample.h"
extern "C" {
void hello(void) { std::cout << "HI" << std::endl; }
void bye(void) { std::cout << "BYE" << std::endl; }
}
在尝试构建共享库时,我看到了预期的错误,因为c_sample.h
它包含在extern "C"
块之外。
g++ cpp_sample.cc -shared -o libcppsample.so
cpp_sample.cc:5:7: error: declaration of 'hello' has a different language linkage
5 | void hello() { std::cout << "HI" << std::endl;}
| ^
./c_sample.h:3:13: note: previous declaration is here
3 | extern void hello();
| ^
cpp_sample.cc:6:7: error: declaration of 'bye' has a different language linkage
6 | void bye() { std::cout << "BYE" << std::endl;}
| ^
./c_sample.h:4:13: note: previous declaration is here
4 | extern void bye();
| ^
2 errors generated.
然而,当我把它包装在一个未命名的命名空间中时,奇迹发生了
cpp_sample.cc
:
#include <iostream>
#include "c_sample.h"
extern "C" {
namespace {
void hello(void) { std::cout << "HI" << std::endl; }
void bye(void) { std::cout << "BYE" << std::endl; }
}
}
编译成功了。当我尝试从另一个 C 源文件中使用它时,它甚至可以工作。
#include "stdio.h"
#include "c_sample.h"
int main() {
hello();
}
$ gcc another.c -L/tmp -lcppsample -o another
$ ./another
HI
仅将其包装在命名空间内如何工作?它如何将声明的函数与其定义链接起来?
以下是失败编译案例的稍微不同的重现:
我只是
#include "stddef.h"
从中删除了它c_sample.h
,因为它没有任何贡献,并且编译失败正是您所期望和理解的:现在这里有一个与您成功编译的
cpp_sample.cc
版本 #2 略有不同的 C++ 源代码。此处的区别在于,我
bye
在不同的命名空间 (ns
) 中进行了定义,而命名空间 (<anonymous>
) 包含 的定义hello
。这只是为了消除它造成的任何差异。但它没有任何区别:编译干净。输出目标文件的符号表为:
我们可以看到,源函数名称
void <anonymous>::hello(void)
和void ns::bye(void)
已被转换为定义的全局链接器符号hello
和,bye
没有 C++ 名称改编,正如extern "C"
声明和定义它们的作用域所要求的那样:链接器符号中没有编码命名空间和调用签名。它们只是普通的旧 C 符号,不像:所以:
很好。
extern "C"
不会改变编译器对 C++ 语法的解析。通过将hello
和的函数定义封装bye
在命名空间或不同的命名空间中,标识符与包含的声明区分开来c_sample.h
,就像它们始终一样。但会抑制在命名空间封闭标识符的目标代码中发出的符号extern "C"
的 C++ 名称改编,转而使用 C 符号化。然后命名空间和调用签名都会丢失。对于链接器来说,重要的是它可以在输入、、等的目标文件和库中找到引用符号的定义。hello
bye
_ZSt4cout
我们可以通过以下方式强调这一区别:
其中
void <anonymous>::hello(void)
和void ns::bye(void)
各被定义两次,一次在的范围内extern "C"
,一次不在。范围
extern "C"
对 C++ 解析没有影响:多重定义是非法的 C++。我们甚至没有发出任何符号,无论是否被 C++ 破坏。但如果它们不是非法的,并且我们确实做到了这一点,那么从默认 C++ 链接范围发出的符号将是:并且从范围发出的内容
extern "C"
将与以前一样:链接时没有多重定义。
您可以看到这
good.cc
相当于:显而易见的是,声明如下:
默认情况下,使用 C++ 链接的那些代码与命名空间封闭的定义完全无关,而且是多余的。因此,对于另一个 C++ 源文件:
我们可以编译并链接另一个程序:
其中它们不是多余的。在这里,使用 进行编译
g++
,main.c
被编译为 C++ 源代码(它节省了我复制带有扩展名的相同文件的时间.cc
),并且来自的声明c_sample.h
默认具有 C++ 链接:即它们将输出 C++ 混乱的符号。所以我们看到:main.o
对 C++ 损坏的符号_Z5hellov
和 进行外部引用_Z3byev
,这些符号和已被修复:定义在:
该程序的符号表:
具有
extern "C"
从 链接的符号good.o
和从 链接的 C++ 符号hellobye.o
,但是从 链接的符号good.o
是多余的:对于程序来说也类似:
其中的符号
hellobye.o
是多余的。您具体询问但您会对最近的或 MSVC 的默认标准
g++ -std=c++11
得出相同的结论。与不同,在输入文件时会发出警告,但像将其编译为 C++ 源。MSVC 需要选择使其将文件编译为 C++。clang/clang++
g++
clang++
*.c
g++
/TP
*.c
您想要实现的目标是用 C++ 编写一个具有 C API 的共享库。具体操作如下:
创建库
将其与 C 程序链接:
或者将其与 C++ 程序链接,其中 C API 将通过头文件激活:
当您在 C++ 文件中时,您使用C++ 链接
#include "c_sample.h"
声明了几个函数。当您在全局命名空间中手动声明函数时,就会发生冲突。在同一个命名空间中,具有 C 链接的函数不能与具有 C++ 链接的函数同名。
当您在未命名的命名空间中手动声明函数时,不会发生冲突。您可以在任何命名空间中声明一个函数,并在不同的命名空间中声明同名的函数,无论链接如何。
在任何命名空间中声明的 C 链接函数只是一个普通的 C 函数,它可以在 C 源文件中实现并由 C 编译器编译(如果不涉及 C++ 特定类型)。
这里没有任何未定义或特定于实现的内容,但代码很脆弱,用处不大。正确的做法是
extern "C"
在 C 标头中添加条件编译权限。