C++ 语言是在 C 语言基础上发展起来的,它既兼容大部分 C 语言的特性,又引入了许多新的概念,如类、对象、模板、异常处理、命名空间等。这使得 C++ 编译器在处理函数和变量名时,会进行一种称为“名字修饰”(Name Mangling 或 Name Decoration)的过程,以便支持函数重载、命名空间等特性。而 C 语言编译器则不会进行这种修饰。
当需要在 C++ 项目中使用 C 语言编写的库,或者在 C 项目中使用 C++ 编写的库时,这种名字修饰的差异就会导致链接错误。extern "C" 就是为了解决这个问题而引入的机制。
名字修饰 (Name Mangling)
我们先来看一个简单的例子,理解名字修饰。
C++ 代码 (mangle_test.cpp):
// mangle_test.cpp
void myFunction(int x) {}
void myFunction(double x) {}
namespace MyNamespace {
void myFunction(int x) {}
}
class MyClass {
public:
void memberFunction(int x) {}
};
如果我们用 C++ 编译器编译(例如 GCC/G++),然后查看导出的符号(例如使用 nm 命令 nm mangle_test.o | c++filt),可能会看到类似这样的结果(具体修饰规则因编译器而异):
- myFunction(int) 可能被修饰成类似 _Z10myFunctioni
- myFunction(double) 可能被修饰成类似 _Z10myFunctiond
- MyNamespace::myFunction(int) 可能被修饰成类似 _ZN11MyNamespace10myFunctionEi
- MyClass::memberFunction(int) 可能被修饰成类似 _ZN7MyClass14memberFunctionEi
这些修饰后的名字包含了函数的参数类型、所在的命名空间或类等信息,使得链接器能够区分同名但签名不同的函数。
C 代码 (c_function.c):
// c_function.c
void cFunction(int x) {}
C 编译器编译后,cFunction 的符号名通常就是 cFunction 或 _cFunction(取决于平台和编译器),没有复杂的修饰。
extern "C"的作用
extern "C" 的主要作用是告诉 C++ 编译器,其声明的函数或变量应该按照 C 语言的方式来编译和链接,即不进行名字修饰。
extern "C" 可以用于:
- 在 C++ 代码中声明 C 语言函数:让 C++ 代码能够正确链接到 C 语言编译的库。
- 在 C++ 代码中定义能被 C 语言调用的函数:让 C++ 编写的函数能够被 C 代码调用。
语法
extern "C" 可以修饰单个函数声明/定义,也可以修饰一个代码块。
修饰单个函数:
extern "C" void c_style_function(int params);
extern "C" int c_style_variable;
修饰代码块:
extern "C" {
// 所有在这里声明或定义的函数和变量都按C方式处理
void func1(int);
int var1;
// ...
}
场景一:C++ 调用 C 代码
假设我们有一个用 C 语言编写的库 my_c_lib.h 和 my_c_lib.c。
my_c_lib.h:
#ifndef MY_C_LIB_H
#define MY_C_LIB_H
// C库的头文件,提供给C和C++使用者
// 为了C++编译器能正确处理,使用条件编译
#ifdef __cplusplus
extern "C" {
#endif
void print_message_from_c(const char* message);
int add_integers_from_c(int a, int b);
#ifdef __cplusplus
}
#endif
#endif // MY_C_LIB_H
my_c_lib.c:
#include "my_c_lib.h"
#include <stdio.h>
void print_message_from_c(const char* message) {
printf("C_Lib: %s\n", message);
}
int add_integers_from_c(int a, int b) {
return a + b;
}
C++ 调用代码 (main.cpp):
#include "my_c_lib.h" // 包含C库的头文件
#include <iostream>
int main() {
print_message_from_c("Hello from C++!");
int sum = add_integers_from_c(10, 20);
std::cout << "C++_Main: Sum from C_Lib = " << sum << std::endl;
return 0;
}
编译链接 (GCC/G++):
# 编译C库 (生成目标文件)
gcc -c my_c_lib.c -o my_c_lib.o
# 编译C++主程序 (生成目标文件)
g++ -c main.cpp -o main.o
# 链接C和C++的目标文件
g++ main.o my_c_lib.o -o program
# 运行
./program
解释:
- 在 my_c_lib.h 中,#ifdef __cplusplus 是一个预处理指令。__cplusplus 是 C++ 编译器会自动定义的一个宏。因此,当这个头文件被 C++ 编译器包含时,extern "C" { ... } 块会生效,告诉 C++ 编译器 print_message_from_c 和 add_integers_from_c 是 C 风格的函数,链接时应该查找未修饰的符号名。
- 当这个头文件被 C 编译器包含时,__cplusplus 未定义,extern "C" 部分被忽略,C 编译器按其默认方式处理。
- 这样,同一个头文件可以同时被 C 和 C++ 代码使用。
场景二:C 调用 C++ 代码
假设我们有一个用 C++ 编写的库,希望提供一些接口给 C 语言调用。
my_cpp_lib.h (供 C++ 内部使用和导出 C 接口):
#ifndef MY_CPP_LIB_H
#define MY_CPP_LIB_H
#include <string>
// C++ 内部类
class MyCppClass {
public:
MyCppClass(const std::string& name);
void greet();
private:
std::string _name;
};
// 导出给C语言的接口,必须用 extern "C"
#ifdef __cplusplus
extern "C" {
#endif
// 使用不透明指针来隐藏C++对象的实现细节
typedef struct MyCppObjectHandle MyCppObjectHandle;
MyCppObjectHandle* create_my_cpp_object(const char* name);
void use_my_cpp_object_greet(MyCppObjectHandle* handle);
void destroy_my_cpp_object(MyCppObjectHandle* handle);
#ifdef __cplusplus
}
#endif
#endif // MY_CPP_LIB_H
my_cpp_lib.cpp:
#include "my_cpp_lib.h"
#include <iostream>
#include <vector> // 只是为了演示C++特性
// C++类实现
MyCppClass::MyCppClass(const std::string& name) : _name(name) {
std::cout << "C++_Lib: MyCppClass constructor for '" << _name << "'" << std::endl;
}
void MyCppClass::greet() {
std::cout << "C++_Lib: Hello from '" << _name << "'! Using std::vector size: " << std::vector<int>().capacity() << std::endl;
}
// C接口的实现
// extern "C" 再次确保这些函数是C链接方式
extern "C" {
// 定义不透明指针的实际类型
struct MyCppObjectHandle {
MyCppClass* instance;
};
MyCppObjectHandle* create_my_cpp_object(const char* name) {
if (!name) return nullptr;
MyCppObjectHandle* handle = new MyCppObjectHandle();
if (!handle) return nullptr;
try {
handle->instance = new MyCppClass(name);
} catch (const std::bad_alloc&) {
delete handle;
return nullptr;
}
return handle;
}
void use_my_cpp_object_greet(MyCppObjectHandle* handle) {
if (handle && handle->instance) {
handle->instance->greet();
}
}
void destroy_my_cpp_object(MyCppObjectHandle* handle) {
if (handle) {
delete handle->instance; // 调用C++析构函数
delete handle; // 释放句柄本身
}
}
} // extern "C"
C 调用代码 (main_c_caller.c):
#include "my_cpp_lib.h" // 包含C++库提供的C接口头文件
#include <stdio.h>
int main() {
printf("C_Main: Calling C++ library.\n");
MyCppObjectHandle* obj1 = create_my_cpp_object("ObjectOne");
if (obj1) {
use_my_cpp_object_greet(obj1);
} else {
printf("C_Main: Failed to create ObjectOne.\n");
}
MyCppObjectHandle* obj2 = create_my_cpp_object("ObjectTwo");
if (obj2) {
use_my_cpp_object_greet(obj2);
}
// 清理资源
destroy_my_cpp_object(obj1);
destroy_my_cpp_object(obj2);
printf("C_Main: Finished using C++ library.\n");
return 0;
}
编译链接 (GCC/G++):
# 编译C++库 (生成目标文件)
g++ -c my_cpp_lib.cpp -o my_cpp_lib.o
# 编译C主程序 (生成目标文件)
gcc -c main_c_caller.c -o main_c_caller.o
# 链接C和C++的目标文件
# 注意:因为C++库可能使用了C++标准库,链接时通常需要使用g++
g++ main_c_caller.o my_cpp_lib.o -o program_c_calls_cpp
# 运行
./program_c_calls_cpp
解释:
- 在 my_cpp_lib.h 中,我们为 C 语言使用者提供了 extern "C" 保护的函数声明。
- 由于 C 语言没有类的概念,我们不能直接将 C++ 类暴露给 C。因此,使用了不透明指针 (MyCppObjectHandle*) 的技巧。C 代码只知道这是一个句柄,具体它指向什么以及如何操作由 C++ 库内部实现。
- create_my_cpp_object 函数在内部创建 C++ 对象,并返回一个包含该对象指针的句柄。
- use_my_cpp_object_greet 函数接收这个句柄,并在内部将其转换回 C++ 对象指针来调用成员函数。
- destroy_my_cpp_object 函数负责释放 C++ 对象和句柄。
- 在 my_cpp_lib.cpp 中,C 接口函数的定义也需要放在 extern "C" 块内(或者每个函数单独用 extern "C" 修饰),以确保它们以 C 链接方式导出。
- 链接时使用 g++ 是因为 C++ 代码 (my_cpp_lib.o) 可能依赖于 C++ 标准库(例如 iostream, string, new/delete 操作等),g++ 链接器会自动处理这些依赖。
注意事项
- 仅限 C 兼容特性:通过 extern "C" 暴露给 C 的接口,其参数和返回值类型必须是 C 语言兼容的。这意味着不能直接使用 C++ 特有的类型,如类、引用、模板、异常等作为接口的一部分。
- 可以使用基本数据类型(int, char*, double 等)。
- 可以使用结构体(但结构体成员也必须是 C 兼容的,不能有 C++ 特有的成员如构造函数、成员函数等)。
- 可以使用指针(包括函数指针)。
- 异常处理:C 语言没有异常处理机制。如果 extern "C" 修饰的 C++ 函数可能会抛出异常,这个异常不能跨越 extern "C" 边界传播到 C 代码中。必须在 C++ 函数内部捕获所有异常,并将其转换为 C 能够理解的错误码或状态。
extern "C" int cpp_function_for_c(int input) {
try {
// C++ code that might throw
if (input < 0) throw std::runtime_error("Negative input");
return input * 2;
} catch (const std::exception& e) {
fprintf(stderr, "C++ Exception: %s\n", e.what());
return -1; // Return an error code
} catch (...) {
fprintf(stderr, "Unknown C++ Exception\n");
return -2; // Return a generic error code
}
}
- 头文件管理:使用 #ifdef __cplusplus 和 extern "C" 是管理跨语言头文件的标准做法。
- C++特性封装:当 C 调用 C++ 时,应将 C++ 的复杂性(如类、模板、STL容器)封装在 C++ 实现内部,只通过 C 兼容的接口(通常是不透明指针和一组操作函数)暴露给 C。
- 链接器:当项目中同时包含 C 和 C++ 代码时,通常推荐使用 C++ 编译器/链接器(如 g++ 或 MSVC 的 cl.exe 链接 C++ 项目)进行最终链接,因为它能更好地处理 C++ 运行时库和依赖。
- extern "C" 只影响链接名,不改变语义:它告诉编译器如何命名函数以供链接器查找,但函数本身仍然是 C++ 函数(如果定义在 .cpp 文件中并用 C++ 编译器编译),可以访问 C++ 特性。
总结
extern "C" 是 C 和 C++ 混合编程中不可或缺的工具。它通过控制名字修饰,使得两种语言编写的代码能够相互调用。
- C++ 调用 C:在 C++ 代码中,使用 extern "C" 声明 C 函数,以确保链接器查找 C 风格的符号名。
- C 调用 C++:在 C++ 代码中,使用 extern "C" 定义供 C 调用的函数,以 C 风格导出符号名。同时,需要注意接口设计,保持 C 兼容性,通常通过不透明指针封装 C++ 对象。
正确使用 extern "C" 并理解其背后的名字修饰和调用约定差异,是实现健壮、可维护的 C/C++ 混合项目的关键。