一、c不能直接调用c++的原因
1.1 语言语义层面的根本差异
c是过程式编程语言,而c++是多范式编程语言,c不支持以下c++核心特性:
- 类与对象(
class/struct成员函数、this指针) - 继承与多态(
virtual函数、虚函数表) - 函数重载与运算符重载
- 命名空间(
namespace) - 模板(
template) - 异常处理(
try/catch/throw) - rtti(运行时类型信息)
1.2 最根本的技术障碍:abi层的名称编码 (name mangling)
名称编码定义:
c++编译器会对函数名、变量名进行编码,以支持重载、命名空间、类成员等特性。不同编译器的名字修饰规则完全不同。
abi(application binary interface)应用程序二进制接口
作用:规定二进制程序、动态库、操作系统之间底层交互规范(函数调用约定、内存布局、符号规则、系统调用等),保证二进制文件无需重新编译即可跨程序兼容。
示例
// c++函数
void func(int a);
void func(double b);
class myclass {
public:
void method();
};gcc编译后的名字:
void func(int)→_z4funcivoid func(double)→_z4funcdmyclass::method()→_zn7myclass6methodev
而c编译器只会生成简单的符号:func、myclass_method。
iso c++20标准 [dcl.link] 第1条:“two function types with different language linkages are distinct types even if they are otherwise identical.”
两种具有不同语言链接的函数类型,即使它们在其他方面相同,也是不同的类型
1.3 其他abi差异
- 调用约定:c和c++默认调用约定通常相同(如x86的cdecl),但成员函数有特殊的
this指针传递规则
✅c++ 全局函数 = 默认 cdecl(和 c 一样)
❌c++ 成员函数 = thiscall(和 c 完全不一样) - 异常处理:c++有栈展开机制,c没有
- rtti:c++运行时类型信息需要额外的数据结构
- 标准库:c++标准库(
libstdc++/libc++)与c标准库(libc)完全独立
c 和 c++ 在 x86 平台上,默认函数调用规则是一样的,都叫 cdecl(c declaration)。
1.参数从右向左压入栈 2.由调用者清理栈
3.返回值存在 eax 寄存器 4. 不使用寄存器传递参数(纯栈传参)
参数怎么传、栈怎么清理、返回值怎么放,c、c++完全相同。
只要解决名字重整(name mangling)问题,c 和 c++ 函数就能互相调用。
二、唯一标准解决方案:extern "c"+ 句柄模式
2.1extern "c"的精确语义
定义:
- extern 是 c++ 中的一个存储类说明符,用于声明一个变量或函数是在别处(其他源文件或本文件的后面)定义的,而不是在当前声明处定义它。
- extern 与字符串字面量(如 “c”)结合,指定语言链接。
extern “c” 告诉 c++ 编译器:“这个函数使用 c 的命名和调用约定,不要做 c++ 的名称修饰。”
作用
- 告诉c++编译器使用c语言的链接规则生成符号
- 禁止对函数名进行名字修饰
- 确保函数使用c语言的调用约定
正确用法
// 头文件中必须使用条件编译
#ifdef __cplusplus
extern "c" { //使用 c 语言的链接规范(不进行名称修饰、使用 c 的调用约定)
#endif
// 所有c可见的声明都放在这里
void c_function(int a);
#ifdef __cplusplus
}
#endif注意:extern "c"只能修饰全局函数和变量,不能修饰类、成员函数、模板等。
2.2 句柄模式(handle pattern)
对c完全隐藏c++类的实现细节,只暴露一个不透明指针(void*)作为对象的句柄。所有操作都通过c风格的函数进行,这些函数内部将句柄转换为真实的c++对象指针。
标准实现步骤
- 编写c++类(正常实现所有功能)
- 编写c接口头文件,定义句柄类型和c函数声明
- 编写c接口实现文件,用
extern "c"函数封装c++类的方法 - c代码只包含c接口头文件,调用c函数
三、调用实现:覆盖绝大多数c++特性
3.1 普通类的封装
完整示例
// myclass.h (c++内部头文件)
#ifndef myclass_h
#define myclass_h
class myclass {
private:
int value;
public:
myclass(int initial = 0);
~myclass();
int getvalue() const;
void setvalue(int v);
int add(int a) const;
};
#endif// myclass.cpp (c++实现)
#include "myclass.h"
myclass::myclass(int initial) : value(initial) {}
myclass::~myclass() {}
int myclass::getvalue() const { return value; }
void myclass::setvalue(int v) { value = v; }
int myclass::add(int a) const { return value + a; }// myclass_c.h (c接口头文件,给c用)
#ifndef myclass_c_h
#define myclass_c_h
#ifdef __cplusplus
extern "c" {
#endif
// 不透明句柄
typedef void* myclasshandle;
// 构造函数
myclasshandle myclass_create(int initial);
// 析构函数
void myclass_destroy(myclasshandle handle);
// 成员函数封装
int myclass_getvalue(myclasshandle handle);
void myclass_setvalue(myclasshandle handle, int v);
int myclass_add(myclasshandle handle, int a);
#ifdef __cplusplus
}
#endif
#endif// myclass_c.cpp (c接口实现)
// 1. 包含c语言接口头文件(给c调用的声明)
#include "myclass_c.h"
// 2. 包含c++类的头文件(才能操作myclass对象)
#include "myclass.h"
// 3. 安全转换函数:内部工具函数,只在当前文件生效
// static:内部链接,外部无法调用
// inline:内联优化,无函数调用开销
static inline myclass* to_myclass(myclasshandle h) {
// 把c的void*句柄,安全转换为c++的myclass对象指针
return static_cast<myclass*>(h);
}
// 4. 对外提供的c接口:创建c++对象
extern "c" myclasshandle myclass_create(int initial) {
// std::nothrow:内存分配失败时返回null,不抛异常(c不支持异常)
return new (std::nothrow) myclass(initial);
}
// 5. 对外提供的c接口:销毁c++对象
extern "c" void myclass_destroy(myclasshandle handle) {
// 转换句柄为对象指针,释放内存
delete to_myclass(handle);
}
// 6. 对外提供的c接口:调用c++成员函数getvalue()
extern "c" int myclass_getvalue(myclasshandle handle) {
return to_myclass(handle)->getvalue();
}
// 7. 对外提供的c接口:调用c++成员函数setvalue()
extern "c" void myclass_setvalue(myclasshandle handle, int v) {
to_myclass(handle)->setvalue(v);
}
// 8. 对外提供的c接口:调用c++成员函数add()
extern "c" int myclass_add(myclasshandle handle, int a) {
return to_myclass(handle)->add(a);
}3.2 继承与多态的封装
关键要点
- 基类必须有虚析构函数(iso c++标准要求,否则delete基类指针会导致未定义行为)
- 封装层只暴露基类句柄,子类创建函数返回基类句柄
- 多态在c++层自动生效,c完全无感
完整示例
// shape.h (c++内部头文件)
#ifndef shape_h
#define shape_h
class shape {
public:
virtual ~shape() = default; // 必须!虚析构函数
virtual double area() const = 0;
virtual double perimeter() const = 0;
};
class circle : public shape {
private:
double radius;
public:
circle(double r);
double area() const override;
double perimeter() const override;
};
class rectangle : public shape {
private:
double width, height;
public:
rectangle(double w, double h);
double area() const override;
double perimeter() const override;
};
#endif// shape_c.h (c接口头文件)
#ifndef shape_c_h
#define shape_c_h
#ifdef __cplusplus
extern "c" {
#endif
typedef void* shapehandle;
// 创建不同子类
shapehandle circle_create(double radius);
shapehandle rectangle_create(double width, double height);
// 统一接口
double shape_area(shapehandle handle);
double shape_perimeter(shapehandle handle);
// 销毁
void shape_destroy(shapehandle handle);
#ifdef __cplusplus
}
#endif
#endif// shape_c.cpp (c接口实现)
#include "shape_c.h"
#include "shape.h"
static inline shape* to_shape(shapehandle h) {
return static_cast<shape*>(h);
}
extern "c" shapehandle circle_create(double radius) {
return new (std::nothrow) circle(radius);
}
extern "c" shapehandle rectangle_create(double width, double height) {
return new (std::nothrow) rectangle(width, height);
}
extern "c" double shape_area(shapehandle handle) {
return to_shape(handle)->area();
}
extern "c" double shape_perimeter(shapehandle handle) {
return to_shape(handle)->perimeter();
}
extern "c" void shape_destroy(shapehandle handle) {
delete to_shape(handle); // 虚析构自动调用子类析构
}3.3 重载函数的封装
c不支持重载,因此需要为每个重载函数提供不同的c函数名:
// c++类
class math {
public:
int add(int a, int b);
double add(double a, double b);
};// c接口 int math_addint(mathhandle h, int a, int b); double math_adddouble(mathhandle h, double a, double b);
3.4 静态成员函数的封装
静态成员函数不依赖this指针,可以直接封装:
// c++类
class utils {
public:
static int version();
};// c接口 int utils_version();
// 实现
extern "c" int utils_version() {
return utils::version();
}
3.5 运算符重载的封装
将运算符重载转换为普通函数:
// c++类
class vector {
public:
vector operator+(const vector& other) const;
};
// c接口 vectorhandle vector_add(vectorhandle a, vectorhandle b);
// vector_c.cpp c接口实现
#include "vector_c.h"
#include "vector.h"
// 句柄转换
static inline vector* to_vec(vectorhandle h) {
return static_cast<vector*>(h);
}
// 封装运算符重载
extern "c" vectorhandle vector_add(vectorhandle a, vectorhandle b) {
// 1. 把c句柄转回c++对象
vector* vec_a = to_vec(a);
vector* vec_b = to_vec(b);
// 2. 调用c++的 operator+ 重载
vector result = *vec_a + *vec_b;
// 3. 堆上创建新对象,返回句柄(c只能操作堆对象)
return new vector(result);
}3.6 模板类的封装
模板类必须实例化后才能封装,因为c不支持模板:
// c++模板类
template<typename t>
class stack {
public:
void push(t value);
t pop();
};
// 显式实例化
template class stack<int>;
template class stack<double>;// c接口 typedef void* intstackhandle; typedef void* doublestackhandle; intstackhandle intstack_create(); void intstack_push(intstackhandle h, int value); int intstack_pop(intstackhandle h); doublestackhandle doublestack_create(); void doublestack_push(doublestackhandle h, double value); double doublestack_pop(doublestackhandle h);
3.7 异常处理的封装
异常是 abi 底层机制,c++ 异常需要 栈展开(stack unwinding)需要 异常表、类型信息、寄存器处理,而c 语言的二进制结构完全没有这些东西
异常一旦进入 c 代码栈帧 → 直接触发未定义行为 → 崩溃!
c不支持c++异常,因此必须在封装层捕获所有异常,并转换为错误码返回给c:
// c++类
class file {
public:
file(const char* path); // 可能抛出std::runtime_error
void write(const char* data); // 可能抛出std::ios_base::failure
};
// c接口(定义错误码) #define file_ok 0 #define file_error_open 1 #define file_error_write 2 int file_create(filehandle* out_handle, const char* path); int file_write(filehandle handle, const char* data); void file_destroy(filehandle handle);
必须在 c++ 侧把异常全部拦截,绝对不能飞到 c 侧,c 只收到返回值:0 = 成功,非 0 = 失败,程序才不会崩溃
// 实现
extern "c" int file_create(filehandle* out_handle, const char* path) {
try {
*out_handle = new file(path);
return file_ok;
} catch (const std::runtime_error& e) {
*out_handle = nullptr;
return file_error_open;
}
}
extern "c" int file_write(filehandle handle, const char* data) {
try {
static_cast<file*>(handle)->write(data);
return file_ok;
} catch (const std::ios_base::failure& e) {
return file_error_write;
}
}如果c++代码抛出异常但未被捕获,会直接导致程序崩溃(c没有异常处理机制)。
四、编译与链接:
4.1 gcc/g++编译流程
正确步骤
- 用
g++编译所有c++文件(.cpp) - 用
gcc编译所有c文件(.c) - 用
gcc或g++链接所有目标文件,必须链接c++标准库
完整命令
# 编译c++文件 g++ -c myclass.cpp -o myclass.o -std=c++17 -wall -wextra g++ -c myclass_c.cpp -o myclass_c.o -std=c++17 -wall -wextra # 编译c文件 gcc -c main.c -o main.o -std=c17 -wall -wextra # 链接(两种方式都可以) # 方式1:用gcc链接,手动指定-lstdc++ gcc main.o myclass.o myclass_c.o -o program -lstdc++ # 方式2:用g++链接(自动链接libstdc++) g++ main.o myclass.o myclass_c.o -o program
常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
undefined reference to 'function_name' | 函数未加extern "c",名字修饰不匹配 | 在函数声明和定义前加extern "c" |
undefined reference to '__gxx_personality_v0' | 未链接c++标准库 | 链接时加-lstdc++(gcc)或-lc++(clang) |
invalid conversion from 'void*' to 'myclass*' | c++中不能隐式转换void* | 使用static_cast显式转换 |
4.2 静态库与动态库
静态库
# 编译为静态库 ar rcs libmyclass.a myclass.o myclass_c.o
- ar工具名 = archive(归档工具)作用:把多个 .o 打包成一个 .a 静态库。
- rcs三个参数的缩写:
- r:替换或添加文件到库
- c:创建库(不存在就创建)
- s:生成索引(加快链接速度)
- rcs三个参数的缩写:
- r:替换或添加文件到库
- c:创建库(不存在就创建)
- s:生成索引(加快链接速度)
# c程序链接静态库 gcc main.c -o program -l. -lmyclass -lstdc++
- -l. 去当前目录下查找目标静态库
- -l 表示 库文件搜索路径
- . 表示 当前目录
- -lmyclass 链接 libmyclass.a 这个静态库
- -l = link(链接)后面写库名 必须去掉 lib 和 .a
- 库文件名:libmyclass.a 链接时写:-lmyclass
- -lstdc++ 链接 c++ 标准库
动态库(linux .so)
# 编译为动态库 g++ -fpic -shared myclass.cpp myclass_c.cpp -o libmyclass.so -std=c++17 # c程序链接动态库 gcc main.c -o program -l. -lmyclass -lstdc++ # 运行时指定库路径 ld_library_path=. ./program #先把当前目录加入动态库搜索路径,再运行程序
- -fpic非常重要:
- position independent code(位置无关代码)动态库必须加,否则不能动态加载,会报错
动态库(windows .dll)
# mingw编译 g++ -shared myclass.cpp myclass_c.cpp -o myclass.dll -wl,--out-implib=libmyclass.a # c程序链接 gcc main.c -o program -l. -lmyclass
4.3 不同编译器的兼容性
- gcc与clang:名字修饰规则基本兼容,但标准库不兼容(
libstdc++vslibc++) - msvc:名字修饰规则与gcc/clang完全不同,生成的库无法互操作
- armcc:支持
extern "c",但需要注意arm特定的调用约定
五、嵌入式系统特殊考虑
5.1 内存管理
嵌入式系统通常不允许动态内存分配,因此需要使用静态内存池:
静态内存池:提前在全局 / 静态区,一次性划分好一块固定大小的内存,程序运行后不再申请、不释放、不依赖 new/delete,自己手动管理这块内存来放对象。
- alignas(myclass):强制对齐(非常关键,否则对象地址错位崩溃)
- static:全局静态内存,程序一生成就有
- 定位 new = 内存你提前准备好,它只负责帮你调用构造函数造对象;不分配堆内存,不能用 delete,只能手动析构
// 静态内存池
alignas(myclass) static char myclass_pool[sizeof(myclass)];
static bool pool_used = false;
extern "c" myclasshandle myclass_create(int initial) {
if (pool_used) return nullptr;
pool_used = true;
return new (myclass_pool) myclass(initial); // 定位new,不申请新堆内存,只在现成静态内存里造对象
}
extern "c" void myclass_destroy(myclasshandle handle) {
static_cast<myclass*>(handle)->~myclass(); // 显式调用析构函数
pool_used = false;
}静态内存池:提前全局划好一块固定内存,不用堆、不用 new/delete,用定位 new 在这块内存上手动构造 c++ 对象,手动析构,专门给嵌入式裸机 / rtos 规避动态内存风险。
5.2 禁用不必要的c++特性
为了减小代码体积和提高性能,嵌入式系统通常禁用以下特性:
- 异常处理:
-fno-exceptions - rtti:
-fno-rtti - 动态内存分配:避免使用
new/delete - 标准库:避免使用
std::string、std::vector等
5.3 中断服务程序(isr)中调用c++代码
isr有严格的限制:
- 不能抛出异常
- 不能使用动态内存分配
- 不能调用非重入函数
正确做法:
- 在isr中只设置标志位
- 在主循环中调用c++代码处理标志位
可重入函数:多个任务 / 中断同时调用这个函数,不会乱、不会崩、数据不会串。
非重入函数:不能被打断、不能同时嵌套调用;一旦在执行中途被中断 / 多任务抢占,全局变量、静态变量会被篡改,逻辑错乱、死机、数据跑飞。
5.4 rtos环境下的使用
在freertos、rt-thread等rtos中使用时:
- 确保c++标准库是线程安全的(大多数现代标准库都是)
- 避免在多个任务中同时操作同一个c++对象,除非加锁
- 使用rtos提供的内存分配函数(如
pvportmalloc)代替new/delete
六、高级特性
6.1 c++成员函数作为c回调函数
c回调函数不支持this指针,因此需要使用静态成员函数+全局/静态数据:
// c回调函数类型
typedef void (*c_callback)(int data, void* user_data);
// c++类
class processor {
private:
int value;
void process(int data) {
// 处理数据
}
public:
static void callback_wrapper(int data, void* user_data) {
processor* self = static_cast<processor*>(user_data);
self->process(data);
}
};
// c接口
void register_callback(c_callback cb, void* user_data);
// 使用
processor proc;
register_callback(processor::callback_wrapper, &proc);6.2 性能开销分析
c调用c++类的性能开销非常小,主要包括:
- 一次函数调用(封装函数)
- 一次指针转换(
void*到c++对象指针)
实测数据(gcc 13.2 -o2优化):
- 直接调用c++成员函数:~1ns
- 通过c接口调用:~2ns
- 虚函数调用:~3ns
结论:在绝大多数嵌入式系统中,这个开销可以忽略不计。
6.3 常见陷阱与解决方案
- 内存泄漏:忘记调用销毁函数
- 解决方案:在c接口文档中明确要求调用销毁函数,使用raii包装(如果c++层允许)
- 空指针解引用:传入null句柄
- 解决方案:在每个封装函数中检查句柄是否为null
- 类型安全问题:错误的句柄类型
- 解决方案:使用带类型的句柄(如
struct myclasshandle*代替void*)
- 解决方案:使用带类型的句柄(如
- 异常未捕获:c++代码抛出异常导致程序崩溃
- 解决方案:在所有封装函数中捕获异常,转换为错误码
七、开发注意事项
- 严格分离c和c++代码:c代码只包含c接口头文件,不包含任何c++头文件
- 使用不透明句柄:永远不要在c接口中暴露c++类的定义
- 总是检查空指针:在每个封装函数中检查句柄是否为null
- 捕获所有异常:在封装层捕获所有c++异常,转换为错误码
- 使用
std::nothrow:在new时使用std::nothrow避免抛出异常 - 提供完整的文档:说明每个函数的用途、参数、返回值和错误码
- 编写单元测试:测试c接口的所有功能和边界条件
到此这篇关于c 语言调用 c++ --- extern关键字 + 句柄模式的文章就介绍到这了,更多相关c++ extern关键字内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论