在 C++ 中,如果需要从 C 语言导入函数或与 C 代码交互,需要使用 extern "C"
关键字。这是因为 C++ 和 C 在编译过程中的 符号命名机制(即 "名称修饰" 或 "name mangling")不同。
1. extern "C"
的作用
extern "C"
关键字的主要作用是告诉 C++ 编译器,所包含的函数或变量使用的是 C 语言的命名和链接规则,而不是 C++ 的。这对于编译和链接 C++ 代码与 C 代码特别重要,因为 C++ 编译器在编译函数时会进行名称修饰,而 C 编译器不会。
C++ 名称修饰:C++ 支持函数重载、类成员函数等复杂的功能,因此 C++ 编译器会对函数名进行编码,以区分同名的不同函数。这个过程称为 名称修饰(name mangling)。
而 C 语言不支持函数重载,其编译器不会对函数名进行修饰,保持函数名的简单性。因此,为了在 C++ 中使用 C 编写的函数,必须使用 extern "C"
来告知 C++ 编译器使用 C 语言的链接规则。
2. extern "C"
的用法
导入单个 C 函数
extern "C" {
void my_c_function(int a);
}
这段代码告诉 C++ 编译器,my_c_function
函数是用 C 语言实现的,并且在编译时应遵循 C 语言的符号命名规则。
导入多个 C 函数
可以将多个函数的声明包含在 extern "C"
块中:
extern "C" {
void my_c_function1(int a);
int my_c_function2(double b);
}
导入 C 头文件
如果你想在 C++ 中包含一个由 C 编写的头文件,可以使用 extern "C"
包装整个头文件的内容:
extern "C" {
#include "my_c_header.h"
}
或者,头文件本身可以被修改,使用条件编译保证其既可以在 C++ 中使用,也可以在 C 中使用:
#ifdef __cplusplus
extern "C" {
#endif
// C 函数声明
void my_c_function(int a);
#ifdef __cplusplus
}
#endif
在这种情况下,当头文件在 C++ 中被包含时,extern "C"
生效,而在 C 中编译时则不受影响。
3. C++ 和 C 编译的不同
3.1. 名称修饰(Name Mangling)
如前所述,C++ 编译器会对函数名进行名称修饰,以支持函数重载、命名空间和类成员函数等特性。名称修饰使得同一个函数名可以根据其参数、命名空间等特征进行区分。C 编译器则不做名称修饰,所有的函数名在编译后保持原样。
C++ 名称修饰示例:
在 C++ 中,编译器可能会将函数 int my_function(int)
的名字修饰成 _Z11my_functioni
,其中包括函数名和参数类型的编码。
C 函数名示例:
在 C 语言中,函数 int my_function(int)
仍然保持为简单的 my_function
。
名称修饰的影响:
- C++ 中的函数重载:C++ 支持函数重载,因此会为每个重载的函数生成不同的修饰名称。
- C 函数命名:由于 C 不支持函数重载,所有函数的名称在编译时保持唯一,编译器不需要进行名称修饰。
3.2. 链接规则
- C 链接:C 编译器在链接时只需要处理简单的函数名称。
- C++ 链接:C++ 编译器在链接时需要处理名称修饰后的符号,因而 C++ 函数可以根据参数、类、命名空间等进行链接。
3.3. 语言特性
- C++ 编译器:处理复杂的 C++ 语言特性,如类、模板、命名空间、异常处理、虚函数等。C++ 代码在编译时往往比 C 代码复杂得多。
- C 编译器:处理 C 语言的相对简单的语法和功能,不需要考虑面向对象的特性。
4. extern "C"
的使用场景
- C++ 调用 C 函数:当你在 C++ 代码中需要使用 C 库时,必须通过
extern "C"
来确保 C++ 编译器按照 C 的方式处理函数的链接。典型场景包括使用 C 编写的系统库、第三方库(如 POSIX、OpenSSL 等)。 - C++ 提供 C 接口:如果你编写了 C++ 库,且需要提供给 C 语言使用,那么可以通过
extern "C"
来导出 C 风格的接口,使 C 语言能够调用 C++ 编译的库。
示例:C++ 调用 C 函数
假设有一个 C 编写的函数 my_c_function
:
// my_c_code.c
#include <stdio.h>
void my_c_function(int a) {
printf("Value: %d\n", a);
}
在 C++ 中调用这个 C 函数时,需要使用 extern "C"
:
// my_cpp_code.cpp
extern "C" {
void my_c_function(int a);
}
int main() {
my_c_function(10); // 调用C函数
return 0;
}
示例:C++ 提供 C 接口
// my_cpp_code.cpp
extern "C" {
void my_cpp_function(int a);
}
void my_cpp_function(int a) {
// C++ 实现
std::cout << "C++ Function: " << a << std::endl;
}
这个函数可以被 C 代码调用,因为它的符号遵循 C 的规则。
总结
extern "C"
:用于告诉 C++ 编译器按照 C 的方式处理函数的链接,使 C++ 和 C 代码能够互相调用。- C++ 编译和 C 编译的不同:
- 名称修饰:C++ 编译器会进行名称修饰,以支持函数重载和其他高级功能,而 C 编译器不会。
- 链接规则:C++ 链接时处理更复杂的符号,而 C 语言保持简单的函数名。
- 常见使用场景:
- C++ 调用 C 库时需要
extern "C"
。 - 提供 C++ 库接口给 C 语言使用时,也需要使用
extern "C"
。
- C++ 调用 C 库时需要
C++ 程序从编写代码到生成可执行的二进制文件,通常经历以下 五个步骤,即:编写代码、预处理、编译、汇编和链接。每个步骤都会转换或处理代码,最终生成一个可执行文件。
C++ 编译过程的五个主要阶段
- 编写代码(Source Code Writing)
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
1. 编写代码(Source Code Writing)
这是程序员编写的 C++ 源代码,通常使用文件扩展名 .cpp
或 .h
。C++ 源文件可能包含类、函数、数据结构和其他逻辑。
- 源文件:例如
main.cpp
,MyClass.cpp
,MyClass.h
。
2. 预处理(Preprocessing)
预处理是编译的第一个阶段,C++ 编译器中的 预处理器 会处理所有以 #
开头的指令,如 #include
, #define
等。这一步的主要任务是处理宏替换、文件包含和条件编译指令。
主要任务:
- 文件包含:将
#include
的头文件内容插入源文件。 - 宏替换:将所有宏定义(通过
#define
定义的内容)替换为对应的值。 - 条件编译:根据条件指令如
#ifdef
、#endif
等执行代码的保留或忽略。 - 注释删除:删除所有的注释内容,包括单行注释
//
和多行注释/* ... */
。
预处理后的文件仍然是文本文件,通常称为 扩展文件,但它包含了所有展开的宏、包含的头文件等。
预处理示例:
#include <iostream>
#define MAX 100
int main() {
std::cout << MAX << std::endl;
return 0;
}
预处理后变成:
// 假设 <iostream> 已被展开
// 省略头文件展开的内容...
int main() {
std::cout << 100 << std::endl; // 宏 MAX 被替换为 100
return 0;
}
3. 编译(Compilation)
在这一步,编译器 将预处理后的源代码文件(仍然是人类可读的 C++ 代码)转换为 中间代码,通常称为 汇编代码。汇编代码是面向特定硬件架构的一种低级别语言,但仍然是人类可读的符号代码。
编译的过程:
- 语法分析:编译器对源代码进行语法分析,确保代码符合 C++ 的语法规则。
- 语义分析:检查代码的逻辑是否合理,比如类型检查、变量是否已声明等。
- 生成汇编代码:编译器根据目标架构将 C++ 代码转换为汇编语言。
编译后的文件:通常以 .s
为扩展名,它仍然是人类可读的汇编代码。
编译示例:
int main() {
int a = 10;
return a;
}
编译后的汇编代码(示例):
.section __TEXT,__text,regular,pure_instructions
.globl _main
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
movl $10, %eax
popq %rbp
retq
.cfi_endproc
4. 汇编(Assembly)
汇编器(Assembler)将编译生成的汇编代码转换为机器码(Machine Code),也称为 目标文件(Object File)。目标文件是特定于目标机器的二进制文件,但还不是完整的可执行文件,因为它可能仍然有外部依赖(比如其他模块中的函数)。
汇编的过程:
- 将汇编代码转换为机器指令,这些指令是计算机 CPU 可以直接理解并执行的二进制代码。
目标文件:
- 汇编器生成的目标文件通常具有
.o
(Unix/Linux/Mac 系统)或.obj
(Windows 系统)扩展名。 - 每个 C++ 源文件都会生成一个对应的目标文件。
示例: 一个目标文件内部包含计算机能执行的机器代码(无法直接展示二进制内容)。
5. 链接(Linking)
链接器(Linker)将多个目标文件和库文件组合起来,生成最终的可执行文件。链接器的主要任务是解决符号引用,即将函数调用和全局变量的引用链接到其实际定义上。
链接的过程:
- 符号解析:链接器会将各个目标文件中的未解析的符号(如函数、全局变量等)与其他目标文件或库中定义的符号进行匹配。
- 库链接:如果程序使用了外部库(如标准库、动态库或静态库),链接器会将这些库与目标文件链接起来。
- 生成可执行文件:链接器最终生成一个完整的二进制可执行文件,该文件可以直接在目标平台上运行。
链接示例:
g++ main.o MyClass.o -o my_program
这条命令会将 main.o
和 MyClass.o
链接在一起,生成可执行文件 my_program
。
6. 可执行文件(Executable File)
生成的可执行文件是二进制文件,包含了最终的机器代码以及所有的依赖库和符号。这个文件可以在目标系统上直接运行。
- Linux 和 Mac 系统的可执行文件没有特定的扩展名,通常通过运行
./my_program
执行。 - Windows 系统上的可执行文件通常有
.exe
扩展名,可以通过双击或命令行运行。
可视化编译过程
编译过程中的工具
- 预处理器:负责处理
#
开头的预处理指令(如#include
,#define
)。 - 编译器:将 C++ 代码翻译为汇编代码(如
g++
、clang
)。 - 汇编器:将汇编代码转为机器码,生成目标文件(如
as
)。 - 链接器:负责将目标文件链接成可执行文件(如
ld
)。
编译选项
在编译时可以使用一些编译器选项来控制不同的阶段。例如,使用 GCC 时:
-E
:只执行预处理,不编译。-S
:将 C++ 代码编译成汇编代码,生成.s
文件。-c
:只编译,不链接,生成目标文件.o
。-o
:指定生成的可执行文件的名称。
总结
C++ 程序从代码到可执行文件的过程是通过预处理、编译、汇编和链接这几个步骤完成的。每个阶段都有特定的任务,最终生成可以在目标平台上运行的二进制文件。