一、为什么需要混合编程?

说实话,C++ 和 C 的混合编程,几乎是每个 C++ 工程师都绕不开的坎。

我刚开始做项目时,接手的是一个老系统。底层驱动、网络协议栈全是 C 写的,上层业务逻辑却要用 C++ 重构。那时候我就想:难道要把所有 C 代码重写一遍?

当然不用。C++ 天生就支持与 C 代码协作。但这里有个坑——名字修饰(Name Mangling)机制不一样。

嗯,咱们先把这个概念讲清楚。

二、名字修饰:C 和 C++ 的“语言隔阂”

C 编译器编译函数时,函数名基本保持不变。比如:

// C 代码
void hello() { }
// 编译后符号名:hello

但 C++ 支持函数重载,编译器必须给同名函数加上额外信息。比如:

// C++ 代码
void hello(int) { }
void hello(double) { }
// 编译后符号名:_Z5helloi 和 _Z5hellod

你看,C++ 编译器把函数名“搅乱”了。如果 C++ 代码直接调用 C 函数,链接器会去找 _Z5helloi,但 C 库里的符号是 hello——这就找不到了。

核心问题:C 和 C++ 的 ABI(应用程序二进制接口)不兼容,主要就体现在名字修饰规则上。

三、extern "C":打通任督二脉

C++ 提供了 extern "C" 这个关键字,告诉编译器:这段代码请用 C 的方式编译和链接

3.1 基本用法

// C++ 代码中声明 C 函数
extern "C" {
    void c_function(int x);
    int  c_global_var;
}

// 或者单行声明
extern "C" void another_c_func();

这样,C++ 编译器就不会对 c_function 做名字修饰,链接时就能找到 C 库里的符号。

3.2 头文件的标准写法

我个人习惯在头文件里这样写,兼容 C 和 C++:

// my_c_lib.h
#ifdef __cplusplus
extern "C" {
#endif

void c_api_func(int x);
int  c_api_get_value(void);

#ifdef __cplusplus
}
#endif

这个 __cplusplus 宏,C++ 编译器会自动定义。C 编译器不认识它,所以会跳过 extern "C" 部分。这样同一个头文件,C 和 C++ 都能用。

小技巧:我建议所有 C 库的头文件都加上这个保护。哪怕现在只给 C++ 用,谁知道以后会不会有 C 项目接入呢?

四、ABI 兼容性:不止是名字修饰

名字修饰只是冰山一角。C 和 C++ 的 ABI 差异还包括:

差异点 C C++
函数调用约定 通常 cdecl 可能不同(如 thiscall)
异常处理 不支持 支持栈展开
对象模型 有虚表、RTTI 等
结构体对齐 可能不同 可能不同

我曾经在一个项目里,C 代码用 #pragma pack(1) 压缩结构体,C++ 那边没注意,结果传过来的数据全错位了。排查了半天,最后发现是对齐方式不一致。

警告:跨语言传递结构体时,务必确认两边的内存对齐设置一致。否则数据错位,查错能查到怀疑人生。

五、混合编程的注意事项

5.1 不要在 extern "C" 里放 C++ 特性

// 错误示例
extern "C" {
    void func() {
        std::vector<int> v;  // C 不支持!
        throw std::runtime_error("error");  // C 不支持!
    }
}

extern "C" 只是改变了链接方式,不会让 C++ 代码变成 C 代码。里面仍然不能使用 C 不支持的特性。

5.2 函数指针的类型安全

C 和 C++ 的函数指针类型可能不兼容。举个例子:

// C 库中的回调函数类型
typedef void (*C_Callback)(int);

// C++ 中注册回调
extern "C" void register_callback(C_Callback cb);

// 错误:不能传 C++ 静态成员函数
class MyClass {
    static void myCallback(int);  // 这是 C++ 链接
};
register_callback(&MyClass::myCallback);  // 链接错误!

解决办法:用 extern "C" 修饰回调函数本身。

extern "C" void myCallback(int) {
    // 这里可以调用 C++ 代码
}
register_callback(myCallback);  // 正确

5.3 全局对象的构造与析构

C++ 全局对象在 main() 之前构造,在 main() 之后析构。但 C 代码没有这个机制。

如果你在 C 代码里调用 C++ 库,要确保 C++ 的全局对象已经构造好了。我遇到过的情况是:C 代码在 main() 一开始就调用了 C++ 函数,那个函数依赖一个全局对象,结果对象还没构造,直接崩溃。

建议:尽量让 C++ 库提供初始化/反初始化函数,由调用方显式管理生命周期。

六、封装 C 库为 C++ 类

这是实际项目中最常见的需求。把 C 风格的 API 封装成 C++ 类,用起来舒服多了。

6.1 一个完整的例子

假设有一个 C 库,管理某种设备:

// c_device.h
#ifdef __cplusplus
extern "C" {
#endif

typedef struct DeviceHandle* DevicePtr;

DevicePtr device_open(const char* name);
void      device_close(DevicePtr dev);
int       device_read(DevicePtr dev, char* buf, int size);
int       device_write(DevicePtr dev, const char* data, int size);

#ifdef __cplusplus
}
#endif

我们可以封装成 C++ 类:

// Device.hpp
#include "c_device.h"
#include <string>
#include <stdexcept>

class Device {
public:
    Device(const std::string& name) 
        : handle_(device_open(name.c_str())) 
    {
        if (!handle_) {
            throw std::runtime_error("Failed to open device");
        }
    }

    ~Device() {
        if (handle_) {
            device_close(handle_);
        }
    }

    // 禁止拷贝
    Device(const Device&) = delete;
    Device& operator=(const Device&) = delete;

    // 允许移动
    Device(Device&& other) noexcept 
        : handle_(other.handle_) 
    {
        other.handle_ = nullptr;
    }

    int read(char* buf, int size) {
        return device_read(handle_, buf, size);
    }

    int write(const char* data, int size) {
        return device_write(handle_, data, size);
    }

private:
    DevicePtr handle_;
};

6.2 封装的好处

  • RAII 管理资源:构造函数打开设备,析构函数自动关闭,不会忘记释放。
  • 类型安全:C 的 void* 句柄被封装成私有成员,外部无法误操作。
  • 异常支持:错误处理从返回码变成异常,更符合 C++ 风格。
  • 禁止拷贝:避免多个对象持有同一个 C 句柄导致重复释放。

封装原则:在类的内部调用 C 函数,对外暴露纯 C++ 接口。这样既利用了 C 库的稳定性,又享受了 C++ 的便利性。

七、知识体系总览

下面这张图,帮你理清本章的核心脉络:

C 与 C++ 混合编程知识体系 核心问题:ABI 不兼容 主要表现:名字修饰规则不同 解决方案:extern "C" 最佳实践:封装为 C++ 类 头文件兼容写法 函数指针处理 RAII 资源管理 异常安全

八、总结

混合编程说白了就三件事:

  1. extern "C" 解决名字修饰问题——这是最基础、最常用的手段。
  2. 注意 ABI 细节——结构体对齐、函数调用约定、异常处理,这些坑我基本都踩过。
  3. 封装成 C++ 类——让 C 库用起来像 C++ 库,代码更安全、更易维护。

我个人觉得,掌握这些技巧后,C 和 C++ 的协作其实挺顺畅的。老代码不用重写,新功能又能用上 C++ 的现代特性,何乐而不为呢?

一句话记住:extern "C" 是桥梁,封装是艺术,细节是魔鬼。


公众号:蓝海资料掘金营,微信deep3321