理解 `extern “C”`:C++ 如何调用 C 函数 – wiki基地


深入理解 extern "C":C++ 如何无缝调用 C 函数

C++ 作为一种强大的多范式编程语言,在软件开发领域占据着重要地位。它在很大程度上兼容 C 语言,这意味着 C++ 代码可以直接使用 C 语言的许多语法特性。然而,尽管有这种兼容性,直接在 C++ 中调用标准的 C 函数或者在 C 代码中使用 C++ 特性(如类、函数重载等)并非总是那么简单。尤其是在链接阶段,两种语言底层实现上的差异可能会导致问题。而 extern "C" 关键字,正是解决这种跨语言调用障碍的关键桥梁。

本文将深入探讨 extern "C" 的作用、原理,以及如何在 C++ 项目中有效地调用 C 函数。

第一部分:冲突的根源——C++ 的名字修饰 (Name Mangling)

要理解 extern "C" 的必要性,首先需要了解 C++ 编译器在处理函数和变量名时与 C 语言的不同之处。

C++ 是一门支持函数重载、运算符重载、类、模板、命名空间等高级特性的语言。为了在编译后的目标文件中区分这些同名但实际上是不同实体(例如,参数列表不同的重载函数,或者属于不同类的同名成员函数),C++ 编译器会采用一种称为 名字修饰 (Name Mangling)名字重整 (Name Decoration) 的技术。

简单来说,名字修饰就是编译器根据函数的签名(函数名、参数类型、返回类型、所属的类或命名空间等信息)生成一个唯一且复杂的内部名称。例如,在 C++ 中,你可能有如下两个函数:

c++
void print(int i);
void print(double d);

在 C++ 编译器的符号表中,它们可能不会简单地表示为 printprint(int) 可能会被修饰成类似于 _Z5printi 的名字(这是一个简化的、依赖于编译器的示例),而 print(double) 可能被修饰成类似于 _Z5printd 的名字。这样,链接器就能根据这些唯一的修饰名准确地找到调用的是哪个 print 函数。

同样,一个类成员函数的修饰名通常会包含类的信息:

c++
class MyClass {
public:
void method();
};

MyClass::method() 在符号表中可能被修饰成类似于 _ZN7MyClass6methodEv 的名字。

关键点:

  1. 唯一性: 名字修饰确保了即使在源代码中函数或变量同名,在编译后的目标文件中它们也有唯一的标识符。
  2. 编译器依赖: 名字修饰的规则并非 C++ 标准的一部分,而是由具体的 C++ 编译器(如 GCC、Clang、MSVC 等)实现的。不同编译器之间的名字修饰规则通常是 不兼容 的。这是为什么通常不能直接链接由不同 C++ 编译器编译的二进制库的原因之一。

第二部分:C 语言的“大白话”——简洁的符号名

与 C++ 不同,C 语言没有函数重载、类成员函数等复杂特性。因此,C 编译器在处理函数和全局变量名时非常直观。在绝大多数情况下,C 编译器生成的符号名就是源代码中的函数名或全局变量名本身,前面可能加上一个简单的 ABI(Application Binary Interface)相关的修饰符(例如,在某些系统上,函数名可能仅仅是 _function_name)。

例如,C 语言中的一个函数:

c
void my_c_function(int a, int b);

在编译后的目标文件中,它的符号名很可能就是 my_c_function_my_c_function。这个名字不包含任何关于参数类型或个数的信息。

第三部分:链接阶段的冲突

现在问题来了。当你有一个 C++ 源文件,其中包含了对一个 C 函数的调用,并且这个 C 函数的定义在一个单独编译的 C 目标文件中时,会发生什么?

  1. C 编译器编译 C 源文件,生成目标文件(例如 .o.obj),其中的函数符号名是简单的 C 风格名字(如 my_c_function)。
  2. C++ 编译器编译 C++ 源文件。当它看到对 my_c_function(int, int) 的调用时,它会按照 C++ 的名字修饰规则,生成一个期望的目标符号名(例如 _Z15my_c_functionii,这只是一个假设)。
  3. 链接器尝试将 C++ 目标文件中对 _Z15my_c_functionii 的调用与 C 目标文件提供的符号定义进行匹配。
  4. 结果: 链接器在 C 目标文件中找不到名为 _Z15my_c_functionii 的符号,只能找到 my_c_function_my_c_function。名字不匹配,链接失败,通常会报告“符号未定义”(Undefined Symbol)错误。

这就是 C++ 和 C 语言在链接层面的核心冲突,也是 extern "C" 诞生的根本原因。

第四部分:extern "C"——C++ 与 C 的沟通桥梁

extern "C" 是 C++ 语言提供的一个链接规范 (Linkage Specification)。它的作用是告诉 C++ 编译器:被 extern "C" 修饰的函数或变量应该按照 C 语言的规则进行链接。

具体来说,extern "C" 主要影响两方面:

  1. 名字处理: 禁用 C++ 的名字修饰,告诉编译器生成 C 风格的、未修饰的(或只进行简单修饰的)符号名。这样,C++ 编译器在调用这些函数时,就会去查找 C 风格的符号名,从而与 C 编译器生成的目标文件中的符号名匹配。
  2. 调用约定 (Calling Convention): 虽然不是标准强制的,但 extern "C" 通常也意味着该函数将使用 C 语言的标准调用约定。调用约定决定了函数调用时参数传递的顺序、方式(栈传递还是寄存器传递)、栈清理的责任(调用者还是被调用者)等。不同的语言和编译器可能使用不同的调用约定。确保使用相同的调用约定是函数调用成功的另一个重要前提。

通过使用 extern "C",我们可以在 C++ 代码中声明一个 C 函数,告诉 C++ 编译器:“嗨,这个函数定义在 C 代码里,它的名字就是 my_c_function,不要对它进行 C++ 风格的名字修饰,并且调用时请使用 C 的调用约定。” 这样一来,C++ 编译器在生成调用代码时,就会使用 C 风格的符号名,链接器就能正确地将 C++ 代码中的调用与 C 目标文件中的定义连接起来。

第五部分:extern "C" 的语法

extern "C" 可以用于单个声明或一个声明块。

  1. 单个声明:

    c++
    extern "C" return_type function_name(parameters);
    extern "C" variable_type variable_name;

    例如:
    c++
    extern "C" void greet_from_c();
    extern "C" int add_in_c(int a, int b);
    extern "C" int global_c_variable;

  2. 块声明:

    当需要声明多个 C 风格的函数或变量时,使用块声明更方便:

    c++
    extern "C" {
    // 一系列 C 语言风格的函数和变量声明
    return_type function_name1(parameters);
    return_type function_name2(parameters);
    variable_type variable_name;
    }

    例如:
    c++
    extern "C" {
    void greet_from_c();
    int add_in_c(int a, int b);
    int global_c_variable;
    }

    这种语法更常用,尤其是在包含整个 C 头文件中的声明时。

第六部分:如何在 C++ 中调用 C 函数——实战演练

最常见的场景是你的项目是 C++ 项目,但你需要使用一个现有的 C 库或者一段 C 代码。标准的做法是修改 C 语言的头文件(或者创建一个新的 C 风格的头文件用于 C++ 项目),将需要被 C++ 代码调用的函数声明放在 extern "C" 块中,并且为了兼容 C 编译器,通常还会结合使用 #ifdef __cplusplus 预处理指令。

让我们通过一个具体的例子来说明。

假设我们有一个简单的 C 源文件 c_code.c 和一个对应的头文件 c_code.h

c_code.h (原始版本,不包含 extern "C")

“`c
// c_code.h

ifndef C_CODE_H

define C_CODE_H

// C 函数声明
void greet_from_c();
int add_in_c(int a, int b);

endif // C_CODE_H

“`

c_code.c

“`c
// c_code.c

include “c_code.h”

include

void greet_from_c() {
printf(“Hello from C!\n”);
}

int add_in_c(int a, int b) {
return a + b;
}
“`

现在,你想在一个 C++ 文件 cpp_main.cpp 中调用 greet_from_cadd_in_c

cpp_main.cpp (错误尝试)

“`c++
// cpp_main.cpp

include “c_code.h” // 直接包含原始的 C 头文件

include

int main() {
greet_from_c(); // 尝试调用 C 函数
int sum = add_in_c(5, 7); // 尝试调用 C 函数
std::cout << “Sum from C: ” << sum << std::endl;
return 0;
}
“`

如果你尝试编译和链接 c_code.ccpp_main.cpp,很可能会遇到链接错误,因为 C++ 编译器期望的是经过名字修饰的函数名,而 C 目标文件提供的是 C 风格的名字。

正确的做法——修改 c_code.h

为了让 c_code.h 既可以被 C 编译器包含(按标准 C 处理),又可以被 C++ 编译器包含(其中的声明被标记为 C 链接),我们需要结合使用 extern "C"#ifdef __cplusplus。C++ 编译器预定义了宏 __cplusplus,而标准 C 编译器则没有。我们可以利用这一点。

c_code.h (修改后,兼容 C 和 C++)

“`c
// c_code.h (适用于 C 和 C++ 项目)

ifndef C_CODE_H

define C_CODE_H

// 如果当前是 C++ 编译器环境,则应用 extern “C” 块
// 这样,块内的声明将被视为具有 C 链接

ifdef __cplusplus

extern “C” {

endif

// C 函数声明
// 注意:这里的声明使用的是 C 语言的语法和类型
void greet_from_c();
int add_in_c(int a, int b);

// 你也可以在这里声明 C 风格的全局变量
// extern int global_c_variable;

ifdef __cplusplus

} // extern “C” 块结束

endif

endif // C_CODE_H

“`

解释修改:

  • 当 C++ 编译器处理这个头文件时,__cplusplus 宏被定义,extern "C" { ... } 块生效。块内的 greet_from_c()add_in_c(int a, int b) 声明被 C++ 编译器视为具有 C 链接。这意味着 C++ 编译器知道在链接阶段查找它们的 C 风格符号名。
  • 当 C 编译器处理这个头文件时,__cplusplus 宏未定义,#ifdef __cplusplus ... #endif 之间的内容被忽略。头文件中的声明 void greet_from_c();int add_in_c(int a, int b); 就按标准的 C 语言声明处理。

现在,c_code.c 保持不变,cpp_main.cpp 保持不变,只是 #include "c_code.h" 现在包含的是修改后的头文件。

cpp_main.cpp (使用修改后的头文件)

“`c++
// cpp_main.cpp

include “c_code.h” // 包含修改后兼容 C++ 的 C 头文件

include

int main() {
// 现在可以无缝调用 C 函数了
greet_from_c();

int sum = add_in_c(5, 7);
std::cout << "Sum from C: " << sum << std::endl;

return 0;

}
“`

编译和链接过程:

  1. 使用 C 编译器编译 c_code.cgcc -c c_code.c -o c_code.o
  2. 使用 C++ 编译器编译 cpp_main.cppg++ -c cpp_main.cpp -o cpp_main.o
    • 在编译 cpp_main.cpp 时,#include "c_code.h" 引入了修改后的头文件。
    • C++ 编译器看到 extern "C" 块,知道 greet_from_cadd_in_c 是 C 链接的函数。
    • cpp_main.o 中,对这两个函数的调用将引用其 C 风格的符号名。
  3. 使用 C++ 链接器链接目标文件:g++ c_code.o cpp_main.o -o my_program
    • 链接器将 cpp_main.o 中对 C 风格符号名的引用与 c_code.o 中提供的 C 风格符号定义成功匹配。
    • 链接成功,生成可执行程序 my_program

运行 my_program 将会看到 C 函数的输出。

第七部分:数据类型兼容性注意事项

虽然 extern "C" 解决了名字修饰和链接层面的问题,但在传递数据时,仍然需要注意 C 和 C++ 之间的数据类型兼容性。

  • 基本类型: int, float, double, char, 指针等基本数据类型在 C 和 C++ 中通常是兼容的,可以直接传递。
  • 结构体 (Struct): C 语言的 struct 在 C++ 中也是一个有效的结构体类型。如果 C 的结构体只包含基本数据成员,且没有特殊的对齐要求或位域,那么在 C++ 中可以直接使用。然而,如果结构体包含函数指针、复杂的位域、或者与 C++ 的 POD (Plain Old Data) 规则不符,可能需要小心处理或进行数据转换。
  • C++ 特有类型: std::string, std::vector, std::map 等 C++ 标准库中的容器或对象不能直接作为参数传递给 C 函数,因为 C 语言不认识这些类型。同样,C 函数也不能直接返回这些类型的对象。如果需要在 C 和 C++ 之间传递复杂数据,通常需要通过 C 兼容的方式进行:
    • 传递原始数据指针和长度(例如,传递 char*size_t 来表示字符串)。
    • 传递简单结构体或基本类型。
    • 提供 C 风格的接口函数,这些函数内部在 C++ 代码中完成 C++ 对象到 C 兼容数据的转换,或者反之。

第八部分:extern "C" 的其他使用场景

除了在 C++ 中调用 C 函数,extern "C" 还有一些其他重要的使用场景:

  1. 编写可供 C 代码调用的 C++ 函数: 虽然本文主要讨论 C++ 调用 C,但有时也需要反过来。如果你有一个 C++ 函数希望被 C 代码调用,你也需要在 C++ 中将其声明为 extern "C"。但这通常只适用于非成员函数。对于 C++ 类的方法,你需要编写一个 extern "C" 的全局函数作为 C 语言的入口点,在这个全局函数内部创建 C++ 对象并调用其方法。
    “`c++
    // C++ 代码
    #ifdef __cplusplus
    extern “C” {
    #endif

    // 声明一个 C++ 函数,希望它具有 C 链接
    void call_cpp_function_from_c();

    ifdef __cplusplus

    }

    endif

    void call_cpp_function_from_c() {
    // 这是 C++ 函数体,可以使用 C++ 特性
    std::cout << “Hello from C++ function called by C!” << std::endl;
    }
    ``
    注意:C 语言那边只需要按普通函数声明和调用即可。
    2. **创建可跨语言调用的接口 (FFI):** 当你需要编写一个库,希望它不仅能被 C++ 调用,还能被 Python、Java、Go 等其他语言调用时,通常会提供一个 C 风格的 API。这是因为 C 语言的 ABI 相对稳定且被大多数语言支持,是进行跨语言互操作的常用“中间语言”。你的 C++ 库内部实现可以使用所有 C++ 特性,但对外导出的函数接口都使用
    extern “C”修饰,并只使用 C 兼容的数据类型。
    3. **创建动态链接库 (DLL/Shared Library):** 如果你正在构建一个动态链接库,并希望这个库能够被 C 或其他语言编写的程序加载和调用,那么库中对外暴露的函数(导出函数)通常需要使用
    extern “C”` 标记,以确保它们的符号名是标准的 C 风格,方便外部程序查找和链接。

第九部分:注意事项和最佳实践

  • 只用于声明: extern "C" 只能应用于函数和变量的 声明,不能应用于定义。
    “`c++
    // 正确
    extern “C” void my_c_function(); // 声明

    // 错误
    extern “C” void my_c_function() { // 定义 – extern “C” 不用于函数体
    // …
    }
    ``
    * **通常用于全局函数和变量:**
    extern “C”通常用于全局范围的函数和变量。它不能直接应用于类成员函数(如前所述,需要通过包装函数)。
    * **不要在 C 文件中使用
    extern “C”:** 除了包裹在#ifdef __cplusplus块内部的声明,标准的 C 编译器是不认识extern “C”关键字的。如果在纯 C 文件中不加区分地使用它,会导致编译错误。
    * **始终使用
    #ifdef __cplusplus:** 这是创建兼容 C 和 C++ 的头文件的标准做法。它确保了extern “C”链接规范只在 C++ 环境中生效,而在 C 环境中被忽略,避免了 C 编译器报错。
    * **理解链接 != 调用约定:**
    extern “C”` 主要强制使用 C 链接,但也通常隐含了使用 C 调用约定。虽然在大多数常见平台上 C++ 和 C 的默认调用约定是一致的,但在某些特定架构或配置下可能存在差异。了解并确保调用约定匹配也很重要。

结论

extern "C" 是 C++ 语言标准中一个至关重要的特性,它是连接 C++ 和 C 代码世界的关键桥梁。通过禁用 C++ 特有的名字修饰机制,并指定按 C 语言的方式进行链接,extern "C" 使得 C++ 代码能够成功地找到并调用由 C 编译器生成的目标文件中的函数和变量。

结合 #ifdef __cplusplus 预处理指令,我们可以创建既兼容 C 又兼容 C++ 的头文件,这是在 C++ 项目中安全、高效地集成和使用现有 C 代码库的标准和推荐方式。掌握 extern "C" 的原理和用法,是成为一名熟练的 C++ 开发者,特别是在处理遗留 C 代码、构建混合语言项目或设计跨语言接口时必不可少的技能。通过正确使用 extern "C",我们可以充分利用两种语言的优势,实现代码的互操作性和复用,构建更加强大和灵活的软件系统。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部