为什么需要 C 接口包装
让 Go、Python、Rust 等语言调用 C++ 代码,直接链接通常行不通——C++ 有三个 C 无法理解的机制:
- 名称修饰(Name Mangling):支持重载的 C++ 编译器会对函数名做修饰,C 链接器找不到原始符号;
- this 指针:成员函数隐含 this 参数,C 里没有;
- 异常 / 构造析构:C 没有异常机制,也无法表达对象的生命周期。
解法:用 extern "C" 导出一层纯 C 接口,用不透明指针(void*)代表 C++ 对象,手动处理生命周期和异常。
案例 1:简单类包装
原始 C++ 类:
// counter.hpp
class Counter {
public:
Counter(int init = 0) : count(init) {}
void increment(int by = 1) { count += by; }
int get() const { return count; }
private:
int count;
};
C 接口头文件(extern "C" 防止名称修饰):
// counter_c.h
#ifdef __cplusplus
extern "C" {
#endif
typedef void* CounterPtr; // 不透明指针
CounterPtr counter_create(int init);
void counter_increment(CounterPtr ptr, int by);
int counter_get(CounterPtr ptr);
void counter_destroy(CounterPtr ptr);
#ifdef __cplusplus
}
#endif
实现文件:create 里 new、destroy 里 delete,对象生命周期完全由 C 接口接管;异常在边界处转成错误返回值。
// counter_c.cpp
#include "counter.hpp"
#include "counter_c.h"
extern "C" {
CounterPtr counter_create(int init) {
try {
return new Counter(init);
} catch (...) {
return nullptr; // 异常转错误
}
}
void counter_increment(CounterPtr ptr, int by) {
auto c = static_cast<Counter*>(ptr);
if (c) c->increment(by);
}
int counter_get(CounterPtr ptr) {
auto c = static_cast<Counter*>(ptr);
return c ? c->get() : -1;
}
void counter_destroy(CounterPtr ptr) {
delete static_cast<Counter*>(ptr);
}
}
案例 2:继承与多态
多态照常生效——C 接口里用基类指针操作,虚函数分派由 C++ 内部完成,C 侧无感知。
// shapes.hpp
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double radius) : r(radius) {}
double area() const override { return 3.14159 * r * r; }
};
// C 接口:创建时返回具体对象,操作走基类指针
ShapePtr circle_create(double radius) {
try {
return new Circle(radius);
} catch (...) {
return nullptr;
}
}
double shape_area(ShapePtr shape) {
auto s = static_cast<Shape*>(shape);
return s ? s->area() : -1.0;
}
void shape_destroy(ShapePtr shape) {
delete static_cast<Shape*>(shape);
}
注意 Shape 的析构函数是 virtual——经基类指针 delete 时才不会泄漏派生类资源。
异常处理与内存管理
异常绝不能穿过 C 边界(C 没有异常栈展开机制,会直接崩溃)。所有可能抛异常的入口都要 try/catch 包住,转成错误码或错误字符串。
extern "C" int safe_op() {
try {
doCppWork();
return 0;
} catch (const std::exception& e) {
log(e.what());
return -1;
} catch (...) {
return -2;
}
}
内存管理两种模型:
- 显式 create/destroy:谁创建谁销毁,最简单,小对象场景够用;
- 引用计数:接口提供 add_ref/release,适合共享对象。
所有权规则必须写进头文件注释。跨语言的内存泄漏/重复释放防不胜防,根源都是所有权没写明。

STL 容器跨边界
STL 类型不能直接跨 C 边界——内存布局依赖实现,不稳定。正确姿势是拷贝到 C 数组:
// 把 vector 内容拷给调用方提供的缓冲区
int int_vector_copy_data(IntVectorPtr ptr, int* out, int max_size) {
auto vec = static_cast<std::vector<int>*>(ptr);
int n = std::min(max_size, (int)vec->size());
std::copy(vec->begin(), vec->begin() + n, out);
return n; // 返回实际拷出的元素数
}
string 同理:用 const char* + 长度显式传递,或拷贝到调用方缓冲区。
实战:Go 通过 CGO 调用
编译动态库,然后在 Go 侧用 CGO 直接调 C 接口:
# 编译动态库
g++ -fPIC -shared counter.cpp counter_c.cpp -o libcounter.so
package main
/*
#cgo LDFLAGS: -L. -lcounter
#include "counter_c.h"
*/
import "C"
func main() {
counter := C.counter_create(10)
C.counter_increment(counter, 5)
value := C.counter_get(counter) // 15
C.counter_destroy(counter) // 用 defer 更保险
}
Go 侧记得用 defer C.counter_destroy(counter),保证所有返回路径都释放。
常见问题
- 内存泄漏:每个 create 必须有对应 destroy,所有权规则写进接口文档;
- 线程安全:C++ 库非线程安全时,在包装层加锁,别指望调用方自觉;
- 类型安全:void* 无类型信息,可用结构体封装(
struct Counter; typedef struct Counter* CounterPtr;),让编译器帮忙检查类型混用; - 性能:减少 C/C++ 边界跨越次数,批量传数据而非逐元素。
工具辅助
- SWIG:从 .hpp 自动生成多语言包装,.i 配置文件控制行为;
- CppSharp:专门生成 C# 绑定;
- 手动包装:复杂项目更可控,本文就是手动方案。
核心要点一句话:extern “C” 去掉修饰、void* 藏住对象、显式管理生命周期、异常转错误码——记住这四件事,C++ 就能被任何支持 C ABI 的语言调用。

