C++ / a working model

14 / 80   ·   C++11   ·   约 9 分钟

extern "C":语言链接与 C ABI 边界

先记住这句话

extern "C" 为适用的函数和变量声明指定 C 语言链接,用于连接兼容的 C 接口。它不是把函数体切换为 C 语言,也不让任意 C++ 类型获得跨语言 ABI;名字、调用约定、布局和资源责任必须分别处理。

本篇内容
  1. 语言链接不只是去掉名字修饰
  2. 函数体仍然是 C++
  3. 稳定边界还需要协议
  4. 运行示例
  5. 动手练习

语言链接不只是去掉名字修饰

C++ 编译器通常会把参数类型等信息编码进外部符号名,以支持重载。extern "C" 指定 C 语言链接,便于同兼容 C 工具链生成的代码连接;标准层面语言链接还关系到函数类型和调用约定,并不规定统一的跨平台符号拼写。

它应出现在命名空间作用域。同一实体的声明必须保持一致的语言链接,不能先按普通 C++ 函数声明,随后把同一个函数重新说成 C 链接。定义源文件应包含接口头文件,从编译时就检查双方声明匹配。

函数体仍然是 C++

一个 C 链接函数的实现仍可使用 C++ 类、模板和 RAII,它仍由 C++ 编译器按 C++ 规则编译。示例的接口仅使用 int,函数体调用一个 C++ 类的静态成员函数,说明链接规范没有改变函数体的语言。

把 class 放进 extern C 大括号也不会使其成员函数变成普通 C 接口。C 端不理解引用、模板、重载集合或 std::string 的库布局;应导出小而明确的自由函数,必要时用不透明句柄把 C++ 对象隐藏在实现侧。

稳定边界还需要协议

供 C 与 C++ 共用的头文件通常用 #ifdef __cplusplus 包住 extern C 声明区,避免 C 编译器遇到不认识的语法。接口除了参数类型,还应说明指针是否可空、缓冲区长度、错误码,以及由哪一侧分配和释放资源。

C++ 异常不应越过不理解异常的 C 边界,包装层应捕获并转换为约定错误。相同 C 链接声明也不保证不同架构、编译器选项或运行库天然二进制兼容;实际 ABI 仍需目标工具链约定,跨进程或跨机器通信则应使用序列化协议。

容易答错的地方

  • 不能靠两个同名 C 链接函数的不同参数类型建立普通 C++ 重载集合;为 C 导出接口使用不同名字。
  • extern "C" 不规定所有权、不做编码转换,也不会自动阻止异常从实现中逸出。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <iostream>

extern "C" int guide_double_small(int value);

class Calculator {
public:
    static int double_small(int value) {
        return (value >= 0 && value <= 100) ? value * 2 : -1;
    }
};

extern "C" int guide_double_small(int value) {
    return Calculator::double_small(value);
}

int main() {
    assert(guide_double_small(4) == 8);
    assert(guide_double_small(101) == -1);
    std::cout << guide_double_small(4) << '\n';
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-extern-c.cpp -o example && ./example

预期结果

8

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

准备向 C 导出返回 std::string 的函数,仅添加 extern "C" 足够吗?提出一个更合适的边界。

查看参考答案

不够,C 无法理解 std::string 的构造、析构和 ABI。可以导出接受调用者缓冲区及容量的函数,用返回值报告所需长度或错误码,明确是否包含末尾零字符。C++ 包装层捕获异常并映射为错误结果,避免跨边界转交标准库对象及其释放责任。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录