ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【C/C++extern“C”的用法,及C++调用C,C调用的C++案例】

【C/C++extern“C”的用法,及C++调用C,C调用的C++案例】 前言只要你做过一次 C 与 C 的混合编译大概率见过这类报错undefined reference to foo而foo明明就在那个.c文件里函数名一个字母都没写错。稍微改一下调用方式报错又变成了undefined reference to foo(int)。这两种报错的差别就是本文要讲的核心问题。很多人的第一反应是extern C是让函数用 C 的方式调用改变了参数入栈、返回值传递这些底层约定。 这个理解是不准确的。extern C在标准里叫链接规范linkage specification它改变的是符号symbol的名字也就是编译器把函数名写进目标文件符号表时采用的规则。而参数怎么传、谁负责清栈那是调用约定calling convention由编译器选项和目标平台决定extern C管不着。还有第二个常见误解以为在.c文件里写extern C就能让 C 代码认识 C。事实上 C 编译器根本不认识extern C这五个字符组合写了只会报语法错误。链接规范是给C 编译器看的。本文先讲清楚名字修饰name mangling这个根本原因然后给出两个方向的完整可编译案例C 调用 C 写的库以及 C 调用 C 写的代码。一、根因名字修饰与链接规范C 允许函数重载也就是同一个名字可以对应多个不同参数的函数void log(int); void log(double); void log(const char*);链接器linker工作在最底层它只认符号名。如果这三个函数在目标文件里都叫log链接器无法区分它们重载就没法实现。所以 C 编译器把参数类型信息编码进符号名这个过程叫名字修饰name mangling。C 语言不支持重载所以 C 编译器不做修饰函数foo在符号表里就叫foo。这就是冲突的来源。下面这张表是同一份代码分别用 C 和 C 编译时符号名的对照以 Linux 上 GCC / Itanium ABI 为例具体字符串以nm输出为准源码中的声明C 编译后的符号名C 编译后的符号名示意void foo(void);foo_Z3foovvoid foo(int);不允许重载_Z3fooiint add(int, int);add_Z3addiidouble g(double);g_Z1gd作者提示_Z开头是 Itanium C ABI 的修饰前缀GCC、Clang 在 Linux/macOS 上使用MSVC 使用另一套完全不同的修饰方案同样是int add(int,int)MSVC 会生成类似?addYAHHHZ的形式。这两套方案互不兼容每家的具体格式都是实现细节不要当成标准断言。想自己看符号表可以用这些命令nm在 Linux/macOS 上随 binutils 提供MSVC 用dumpbin# 编译成目标文件 gcc -c foo.c -o foo_c.o g -c foo.cpp -o foo_cpp.o # 查看符号 nm -C foo_c.o # C 版本T foo nm -C foo_cpp.o # C 版本T foo() -C 会做逆修饰方便阅读extern C做的事只有一件告诉 C 编译器这个名字不要修饰按 C 的规则写进符号表。extern C void foo(void); // 符号名就是 foo它不改变语义、不改变调用约定、不改变类型检查强度。C 依然会对foo做完整的类型检查只是最终生成的符号名不带参数编码而已。一个直接推论extern C块里的函数不能重载因为重载需要修饰过的名字来区分而这里名字被统一压平了。写两个extern C void log(int)和extern C void log(double)是编译错误。// ❌ 编译错误C 链接规范下无法重载 extern C void log(int); extern C void log(double); // error: conflicting declaration // ✅ 正确要么只留一个要么改用 C 链接、在更外层做包装 extern C void log_int(int); extern C void log_double(double);二、C 调用 C 代码这是最常见的场景你有一个现成的 C 库zlib、libcurl、自己写的math_utils.c想在 C 项目里用。关键在于C 库的头文件会被 C 编译器读到所以必须让 C 编译器知道这里面的函数不修饰同时这个头文件还要能被纯 C 编译器读。标准做法是用__cplusplus宏做条件编译。完整例子三个文件。先看头文件/* math_utils.h —— 同时被 C 和 C 包含 */ #ifndef MATH_UTILS_H #define MATH_UTILS_H #ifdef __cplusplus extern C { #endif int mu_add(int a, int b); long mu_factorial(int n); double mu_average(const int* data, int count); #ifdef __cplusplus } #endif #endif /* MATH_UTILS_H */实现文件是纯 C不含任何 C 语法/* math_utils.c —— 纯 C用 gcc 编译 */ #include math_utils.h int mu_add(int a, int b) { return a b; } long mu_factorial(int n) { long r 1; int i; if (n 0) return -1; for (i 2; i n; i) r * i; return r; } double mu_average(const int* data, int count) { long sum 0; int i; if (count 0) return 0.0; for (i 0; i count; i) sum data[i]; return (double)sum / (double)count; }C 侧的调用者/* main.cpp —— C17用 g 编译 */ #include math_utils.h // C 编译器在这里看到 extern C #include iostream #include vector int main() { std::cout mu_add(2, 3) mu_add(2, 3) \n; std::cout mu_factorial(5) mu_factorial(5) \n; std::vectorint v{10, 20, 30, 40}; std::cout mu_average mu_average(v.data(), static_castint(v.size())) \n; return 0; }编译与链接gcc -stdc11 -Wall -Wextra -c math_utils.c -o math_utils.o g -stdc17 -Wall -Wextra main.cpp math_utils.o -o app ./app注意几点.c文件用gcc编.cpp文件用g编最后用g链接因为需要链接 C 运行库libstdc。如果把.c文件丢给g编g会把它当 C 编译此时__cplusplus会被定义头上的extern C生效符号名变成未修饰的反而也是对的——但语义上你写的 C 代码就被当成 C 了遇到void* p malloc(...)这类隐式转换会报错。推荐始终用对应语言的编译器编译。如果 C 库已经装到系统路径比如-lcurl直接g main.cpp -lcurl即可extern C保护已经写在它的头文件里了。三、C 调用 C 代码反方向更麻烦因为 C 不能理解类、引用、模板、异常、命名空间。原则是在 C 侧做一层薄的 C 接口把 C 的丰富类型全部挡在后面对外只暴露 C 能懂的东西——不透明指针opaque pointer通常写作void*或指向不完整类型的指针、基本类型、C 结构体。完整例子一个 C 的圆形类导出给 C 使用。/* circle.hpp —— C 内部头C 不包含它 */ #pragma once #include string namespace geo { class Circle { public: Circle(double r) : radius_(r) { validate(); } double area() const; double perimeter() const; std::string describe() const; private: void validate() const; double radius_; }; } // namespace geo/* circle.cpp —— C 实现 */ #include circle.hpp #include cmath #include stdexcept #include sstream namespace geo { void Circle::validate() const { if (radius_ 0.0) throw std::invalid_argument(radius must be 0); } double Circle::area() const { return 3.14159265358979323846 * radius_ * radius_; } double Circle::perimeter() const { return 2.0 * 3.14159265358979323846 * radius_; } std::string Circle::describe() const { std::ostringstream os; os circle r radius_; return os.str(); } } // namespace geo/* circle_c_api.cpp —— 唯一的 extern C 边界 */ #include circle.hpp #include cstring #include new extern C { /* 用不透明句柄代表 C 对象 */ typedef void* CircleHandle; CircleHandle circle_create(double radius) { try { return new geo::Circle(radius); // 可能抛异常 } catch (...) { return nullptr; // 绝不把异常放出去 } } void circle_destroy(CircleHandle h) { delete static_castgeo::Circle*(h); // 对 nullptr 调用 delete 是合法的 } double circle_area(CircleHandle h) { if (!h) return -1.0; return static_castgeo::Circle*(h)-area(); // area() 只做算术不会抛 } /* 把 std::string 拷进调用者提供的缓冲区 */ int circle_describe(CircleHandle h, char* buf, int buf_size) { if (!h || !buf || buf_size 0) return -1; try { const std::string s static_castgeo::Circle*(h)-describe(); if (static_castint(s.size()) 1 buf_size) return -2; // 缓冲区不够 std::memcpy(buf, s.c_str(), s.size() 1); return static_castint(s.size()); } catch (...) { return -3; } } } // extern CC 侧的调用/* main.c —— 纯 C用 gcc 编译 */ #include stdio.h typedef void* CircleHandle; extern CircleHandle circle_create(double radius); extern void circle_destroy(CircleHandle h); extern double circle_area(CircleHandle h); extern int circle_describe(CircleHandle h, char* buf, int buf_size); int main(void) { CircleHandle c circle_create(2.5); if (!c) { printf(create failed\n); return 1; } printf(area %f\n, circle_area(c)); char desc[64]; int n circle_describe(c, desc, sizeof desc); if (n 0) printf(desc %s\n, desc); circle_destroy(c); return 0; }g -stdc17 -Wall -Wextra -c circle.cpp -o circle.o g -stdc17 -Wall -Wextra -c circle_c_api.cpp -o circle_c_api.o gcc -stdc11 -Wall -Wextra -c main.c -o main_c.o g main_c.o circle.o circle_c_api.o -o app_c这里最值得说的是异常。C 代码里没有try/catch一个从 C 抛出的异常一旦穿过extern C边界进入 C 栈帧就没有任何代码能接住它程序会走向未定义行为。实践中的结局通常是调用std::terminate直接终止进程。所以包装函数内部必须把所有异常吃掉转换成返回值错误码——上面每个extern C函数都套了try { ... } catch (...) { }这不是啰嗦是硬性要求。四、容易忽略的细节链接规范是声明的一部分不是定义的一部分。C 编译器看到的每一个声明都必须带一致的链接规范。只在.cpp定义处写extern C而头文件里没写是最经典的坑定义处符号名未修饰调用处符号名被修饰链接器两边都找不到对方。// ❌ 错误定义是 C 链接可见的声明是 C 链接 // header.h void foo(int); // 调用方看到的是 _Z3fooi // foo.cpp extern C void foo(int) { } // 定义处生成的是 foo → 链接错误 // ✅ 正确头文件里就带链接规范实现文件包含头文件 // header.h #ifdef __cplusplus extern C { #endif void foo(int); #ifdef __cplusplus } #endifextern C块里不要#includeC 标准头。把#include vector放进extern C { }会让标准库里的所有函数都按 C 链接规范声明模板、重载、内联全部崩掉报出成百上千行莫名其妙的错误。// ❌ 灾难 extern C { #include string #include vector } // ✅ 正确标准头一律放在外面 #include string #include vector extern C { #include my_c_lib.h }变量也能有 C 链接。extern C int g_counter;是合法的全局变量同样受名字修饰影响C 里全局变量的符号名一般不做重载修饰但不同编译单元间仍会受命名空间影响。函数指针类型也带链接规范。标准里C 链接的函数和 C 链接的函数是不同的函数类型。实践中 GCC/Clang/MSVC 都不会因此报错它们不把链接规范编进函数类型但严格按标准写回调函数指针的typedef也应该放进extern C块或使用带链接规范的声明。常见坑点1. 头文件被纯 C 编译器包含时extern C直接语法错误。/* ❌ gcc 编译这个文件会报 expected identifier */ /* mylib.h */ extern C { int foo(int); }/* ✅ 用 __cplusplus 保护 */ /* mylib.h */ #ifdef __cplusplus extern C { #endif int foo(int); #ifdef __cplusplus } #endif2. 只保护了头文件忘了 C 侧手写的声明。C 文件里自己写extern int foo(int);没问题但如果它包含了带extern C的头文件又同时手写一遍两边必须完全一致。3.extern C函数里用 C 类型做接口C 侧无法声明。// ❌ C 侧写不出 std::string、引用、默认参数 extern C void greet(const std::string name); extern C void set(int a, int b 0); // 默认实参在 C 里不存在// ✅ 只暴露 C 能表达的类型 extern C void greet(const char* name); extern C void set(int a, int b);4. 让异常穿过extern C边界。这是未定义行为级别的错误实际表现一般是std::terminate。// ❌ 异常直接穿出边界 extern C int parse(const char* s) { return std::stoi(s); // 输入非法时抛 std::invalid_argument }// ✅ 内部消化转成错误码 extern C int parse(const char* s) { try { return std::stoi(s); } catch (const std::exception) { return -1; } catch (...) { return -2; } }5. 用extern C做重载或命名空间。命名空间对 C 是不存在的extern C里的东西即使在命名空间内也会用未修饰名导出容易撞名。// ❌ 想让 C 代码区分两个同名函数 extern C void draw(int); extern C void draw(double); // 编译错误 // ✅ 起不同的 C 名字 extern C void draw_int(int); extern C void draw_double(double);6. 两边内存分配与释放不配对。C 侧用malloc拿到的指针交给 C 用delete释放或反过来是未定义行为标准不保证任何行为。跨边界的分配/释放最好由同一侧提供配对函数。// ❌ 在 C 里 new却暴露给 C 用 free 释放 extern C char* make_buf() { return new char[64]; }// ✅ 提供配对的释放函数 extern C void free_buf(char* p) { delete[] p; }7. C 的 struct 与 C 的 struct 对齐假设不一致。两边用#pragma pack、__attribute__((packed))或编译选项改了结构体布局时跨边界传结构体就会错位。传结构体时务必确认两侧的编译选项一致。总结事项C 调用 CC 调用 C谁加extern CC 侧的导出包装层C 库自己的头文件接口类型限制只能用 C 能表达的类型无额外限制C 头本就如此异常处理包装层必须catch(...)不存在 C 异常来源典型错误抽象类型漏进接口头文件缺__cplusplus保护内存管理两侧配对提供 destroyC 库自带释放接口一句话结论extern C解决的是符号名不匹配这一个具体问题它的作用域是编译期到链接期的名字生成规则不涉及调用约定与数据布局。混合编程剩下的复杂度——类型系统差异、异常传播、内存归属、ABI 对齐——都得靠你自己在边界上手动摆平清单式的做法是接口只用 C 能表达的东西异常在边界前吃掉内存谁分配谁释放。标准依据可查阅 C 标准中关于language linkage的条款或 cppreference 的 Language linkage 词条具体符号名格式以你所用工具链的nm -C/dumpbin /symbols输出为准。
返回列表