C 调用 C++:extern “C” 接口详解与实践
核心问题在于 C++ 编译器会对函数名进行“修饰”(Name Mangling)以支持函数重载等特性,而 C 编译器则不会。此外,C 语言本身没有类、对象等概念。为了解决这个问题,我们需要在 C++ 代码中提供一个 C 语言可以理解的接口 .cpp 文件。
1. 主要方法:使用 extern "C"
extern "C"
是 C++ 提供的一个关键字,用于指示编译器以 C 语言的规则来编译指定的代码块或函数声明。这意味着:
- 禁用名称修饰(Name Mangling):编译器会按照 C 语言的方式处理函数名,使其在链接时能被 C 代码找到。
- 遵循 C 调用约定:确保函数参数传递和栈处理方式与 C 语言兼容。
2. 实现步骤:构建 C 到 C++ 的桥梁
要让 C 代码能够调用 C++ 的功能,我们需要搭建一座“桥梁”。核心思路是在 C++ 中创建一个符合 C 语言规范的接口层。以下是详细步骤:
-
设计并实现 C++ 功能模块 (
calculator.hpp
&calculator.cpp
):- 像往常一样,在
.hpp
文件中定义你的 C++ 类(例如Calculator
),包含其成员变量和成员函数声明。 - 在对应的
.cpp
文件中实现这些成员函数(构造函数、析构函数、add
,subtract
等)。这个文件只包含纯粹的 C++ 类实现,不涉及extern "C"
。
- 像往常一样,在
-
定义 C 语言接口头文件 (
c_interface.h
):- 创建一个
.h
头文件,这将是 C 代码和 C++ 接口代码共同的“契约”。 - 关键: 使用
#ifdef __cplusplus
和extern "C"
条件编译指令。这确保:- 当被 C++ 编译器包含时,
extern "C"
生效,声明的函数将具有 C 链接规范(无名称修饰,C 调用约定)。 - 当被 C 编译器包含时,
extern "C"
部分被忽略,C 编译器只看到标准的 C 函数声明。
- 当被 C++ 编译器包含时,
- 推荐: 在此头文件中定义一个不透明指针类型(例如
typedef void* CalculatorHandle;
)作为 C 代码中代表 C++ 对象的“句柄”。这样可以完全隐藏 C++ 的类型细节。 - 声明一组 C 风格的函数原型(例如
CalculatorHandle createCalculator();
,void destroyCalculator(CalculatorHandle handle);
,double add(CalculatorHandle handle, double a, double b);
等),这些函数将构成 C 语言可以调用的接口。
- 创建一个
-
实现 C 语言接口包装层 (
c_interface.cpp
):- 创建一个新的 C++ 源文件(
.cpp
),专门用于实现c_interface.h
中声明的那些 C 风格接口函数。 - 包含必要的头文件:
#include "c_interface.h"
和#include "calculator.hpp"
。 - 实现包装函数 (Wrapper Functions):
createCalculator()
: 内部使用new Calculator()
创建 C++ 对象。destroyCalculator(CalculatorHandle handle)
: 接收 C 代码传来的指针,然后使用delete
销毁 C++ 对象。add(CalculatorHandle handle, double a, double b)
等函数: 接收句柄,将其转换回Calculator*
,然后调用实际的 C++ 成员函数 (calc->add(a, b)
),并返回结果。
- 注意: 这些函数的实现本身是在 C++ 文件中,可以使用 C++ 特性(如
new
,delete
,static_cast
,try-catch
处理异常等),但因为它们在c_interface.h
中被extern "C"
声明过,所以最终会被编译为 C 链接规范的函数。
- 创建一个新的 C++ 源文件(
-
在 C 代码中调用接口 (
main.c
):- 包含 C 接口头文件
#include "c_interface.h"
。不要包含 C++ 的头文件 (calculator.hpp
)。 - 像调用普通 C 函数一样调用
c_interface.h
中声明的接口函数。 - 使用指针类型的变量来存储和传递 C++ 对象的句柄。
- 极其重要: 必须在使用完 C++ 对象后,显式调用对应的销毁函数(如
destroyCalculator(calcHandle)
) 来释放资源,防止内存泄漏。
- 包含 C 接口头文件
-
编译和链接 (使用 C++ 编译器):
- 使用 C++ 编译器 (例如
g++
) 来编译所有的 C++ 源文件 (calculator.cpp
,c_interface.cpp
)。 - 可以使用 C 编译器 (
gcc
) 或 C++ 编译器 (g++
) 来编译 C 源文件 (main.c
)。(g++
通常也能很好地处理 C 代码)。 - 关键链接步骤: 必须使用 C++ 编译器 (
g++
) 将所有生成的目标文件 (.o
) 链接在一起形成最终的可执行文件。这
- 使用 C++ 编译器 (例如
3. 示例:使用 extern “C” 包装 C++ 计算器类程序
假设我们有一个简单的 C++ 类 Calculator,我们希望能在 C 程序中使用它的加减乘除功能。
calculator.hpp
(C++ 类头文件)
#ifndef CALCULATOR_HPP
#define CALCULATOR_HPP#include <stdexcept> // For exception handlingclass Calculator {
private:double last_result;public:Calculator();~Calculator(); // Destructordouble add(double a, double b);double subtract(double a, double b);double multiply(double a, double b);double divide(double a, double b);double getLastResult() const;
};#endif // CALCULATOR_HPP
说明: 这是标准的 C++ 头文件,定义了 Calculator 类的接口。C 代码不应该直接包含这个文件。
calculator.cpp
(C++ 实现文件)
#include "c_interface.h"
#include "calculator.hpp"Calculator* createCalculator() {return new Calculator();
}void destroyCalculator(Calculator* calc) {delete calc;
}double add(Calculator* calc, double a, double b) {return calc->add(a, b);
}double subtract(Calculator* calc, double a, double b) {return calc->subtract(a, b);
}double multiply(Calculator* calc, double a, double b) {return calc->multiply(a, b);
}double divide(Calculator* calc, double a, double b) {return calc->divide(a, b);
}double getLastResult(Calculator* calc) {return calc->getLastResult();
}
c_interface.h
(接口头文件,供 C++ 接口文件使用)
接口头文件 (c_interface.h) 的角色:
-
这个头文件是 C 和 C++ 之间的“契约”。
-
#ifdef __cplusplus / extern “C” / #endif 的组合是关键:
-
当被 C++ 编译器处理时(如 c_interface.cpp 包含它),extern “C” 生效,确保函数以 C 方式导出。
-
当被 C 编译器处理时(如 main.c 包含它),#ifdef __cplusplus 为假,extern “C” 部分被忽略,C 编译器只看到标准的 C 函数声明。
-
-
重要: C 代码 (main.c) 通过这个头文件只知道存在一些 C 风格的函数(如 createCalculator, add 等)。它完全不知道 C++ 的 Calculator 类的内部细节,这实现了良好的封装。
#ifndef C_INTERFACE_H
#define C_INTERFACE_H#include "calculator.hpp"#ifdef __cplusplus
extern "C" {
#endifCalculator* createCalculator();
void destroyCalculator(Calculator* calc);
double add(Calculator* calc, double a, double b);
double subtract(Calculator* calc, double a, double b);
double multiply(Calculator* calc, double a, double b);
double divide(Calculator* calc, double a, double b);
double getLastResult(Calculator* calc);#ifdef __cplusplus
}
#endif#endif
c_interface.cpp
(接口实现文件,间接实现其他 .C 文件调用面向对象功能)
#include "c_interface.h"
#include "calculator.hpp"Calculator* createCalculator() {return new Calculator();
}void destroyCalculator(Calculator* calc) {delete calc;
}double add(Calculator* calc, double a, double b) {return calc->add(a, b);
}double subtract(Calculator* calc, double a, double b) {return calc->subtract(a, b);
}double multiply(Calculator* calc, double a, double b) {return calc->multiply(a, b);
}double divide(Calculator* calc, double a, double b) {return calc->divide(a, b);
}double getLastResult(Calculator* calc) {return calc->getLastResult();
}
main.c
(C 主程序)
这是纯 C 代码。它只依赖 c_interface.h 定义的函数。注意,它需要手动调用 destroyCalculator 来管理 C++ 对象的生命周期。
#include "c_interface.h"
#include <stdio.h>int main() {Calculator* calc = createCalculator();double result = add(calc, 10, 20);printf("Result: %f\n", result);result = subtract(calc, 10, 20);printf("Result: %f\n", result);result = multiply(calc, 10, 20);printf("Result: %f\n", result);result = divide(calc, 10, 20);printf("Result: %f\n", result);destroyCalculator(calc);return 0;
}
编译和链接 (使用 G++)
以 vscode 中的task.json配置为例: 必须使用 C++ 编译器 (g++) 进行链接,因为它需要链接 C++ 标准库来支持 new, delete, 异常处理等。g++ 可以同时编译 C (.c) 和 C++ (.cpp) 源文件。
{"tasks": [{"type": "cppbuild","label": "Build Project (main.c + C++ files)","command": "D:\\mingw64\\bin\\g++.exe","args": ["-fdiagnostics-color=always","-g","${workspaceFolder}/main.c","${workspaceFolder}/c_interface.cpp","${workspaceFolder}/calculator.cpp","-o","${workspaceFolder}/main.exe"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": ["$gcc"],"group": {"kind": "build","isDefault": true},"detail": "Compiles main.c, c_interface.cpp, calculator.cpp and links them into main.exe"}],"version": "2.0.0"
}
g++ 编译命令实际为:
g++.exe -g ./main.c ./c_interface.cpp ./calculator.cpp -o ./main.exe
执行结果:
PS E:\Learning_Record\code> g++.exe -g ./main.c ./c_interface.cpp ./calculator.cpp -o ./main.exe PS E:\Learning_Record\code> .\main.exe C++ Calculator object created. Result: 30.000000 Result: -10.000000 Result: 200.000000 Result: 0.500000 C++ Calculator object destroyed.
4. 使用不透明指针 (void*) 作为句柄 (Handle)
虽然上面的程序 c_interface.h 直接使用了 Calculator*,并且在 C 代码中也能工作(因为 C 编译器把它当作一个未定义类型的指针),但更健壮和推荐的做法是在 C 接口中使用 不透明指针 (void*) 来代表 C++ 对象。
-
优点:
- 完全隐藏 C++ 类型: C 代码完全不需要知道 Calculator 这个名字,增加了封装性。
- 避免潜在的 C 编译器警告/错误: C 编译器看到 Calculator* 可能会有疑问,而 void* 是标准的未知类型指针。
- 隐藏 C++ 复杂性 (Hiding C++ Complexity): C 代码的开发者不需要了解 C++ 的特性,如类、构造/析构、模板、异常处理、名称修饰等。他们只需要像调用普通 C 函数一样使用接口。
-
修改示例:
c_interface.h
(接口头文件,供 C++ 接口文件使用)#ifndef C_INTERFACE_H #define C_INTERFACE_H// Remove direct include of calculator.hpp for C code if not strictly necessary // #include "calculator.hpp" // C code doesn't need the full C++ class definition// Define the opaque pointer type for C code typedef void* CalculatorHandle;#ifdef __cplusplus extern "C" { #endif// Functions now use CalculatorHandle CalculatorHandle createCalculator(); void destroyCalculator(CalculatorHandle calc); double add(CalculatorHandle calc, double a, double b); double subtract(CalculatorHandle calc, double a, double b); double multiply(CalculatorHandle calc, double a, double b); double divide(CalculatorHandle calc, double a, double b); double getLastResult(CalculatorHandle calc);#ifdef __cplusplus } #endif#endif // C_INTERFACE_H
c_interface.cpp
(接口实现文件,间接实现其他 .C 文件调用面向对象功能)#include "c_interface.h" #include "calculator.hpp" #include <stdexcept>CalculatorHandle createCalculator() {return new Calculator(); }void destroyCalculator(CalculatorHandle handle) {Calculator* calc = static_cast<Calculator*>(handle);delete calc; }double add(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->add(a, b); }double subtract(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->subtract(a, b); }double multiply(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->multiply(a, b); }double divide(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);try {return calc->divide(a, b);} catch (const std::exception& e) {fprintf(stderr, "Error in divide: %s\n", e.what());return 0.0;} }double getLastResult(CalculatorHandle handle) {Calculator* calc = static_cast<Calculator*>(handle);return calc->getLastResult(); }
static_cast<Calculator*>(handle): 这是 C++ 中的显式类型转换 (Explicit Type Casting)。
-
static_cast 是 C++ 提供的一种类型转换操作符,用于在编译时进行类型检查的转换。它比 C 风格的强制类型转换更安全、更明确。
-
<Calculator*> 指定了我们想要将 handle 转换成的目标类型,也就是“指向 Calculator 的指针”。
-
(handle) 是要被转换的源变量或表达式。在这个例子中,handle 的类型是 CalculatorHandle,也就是我们之前 typedef 定义的 void*。void* 是一种通用指针,它可以指向任何类型的对象,但它本身不包含类型信息。
类似于C 风格的强制类型转换:
// 假设 handle 是一个 void*,并且你知道它指向 MyStruct struct MyStruct *ptr = (struct MyStruct*)handle; // 现在可以使用 ptr->member
main.c
(C 主程序)#include "c_interface.h" #include <stdio.h> #include <stdlib.h>int main() {CalculatorHandle calc = createCalculator();if (calc == NULL) {fprintf(stderr, "Error: Failed to create Calculator object.\n");return EXIT_FAILURE;}printf("Calling C++ functions via C interface...\n");double res_add = add(calc, 10.5, 20.0);printf("add(10.5, 20.0) = %f\n", res_add);double res_sub = subtract(calc, res_add, 5.5);printf("subtract(%f, 5.5) = %f\n", res_add, res_sub);double res_mul = multiply(calc, res_sub, 2.0);printf("multiply(%f, 2.0) = %f\n", res_sub, res_mul);double res_div = divide(calc, res_mul, 10.0);printf("divide(%f, 10.0) = %f\n", res_mul, res_div);printf("Attempting division by zero...\n");double res_div_zero = divide(calc, 100.0, 0.0);printf("divide(100.0, 0.0) = %f (check for error indicator)\n", res_div_zero);double last_res = getLastResult(calc);printf("Last result stored in calculator: %f\n", last_res);destroyCalculator(calc);printf("Calculator object destroyed.\n");return 0; }
-