C 调用 C++:extern “C” 接口详解与实践 C/C++混合编译

C 调用 C++:extern “C” 接口详解与实践

核心问题在于 C++ 编译器会对函数名进行“修饰”(Name Mangling)以支持函数重载等特性,而 C 编译器则不会。此外,C 语言本身没有类、对象等概念。为了解决这个问题,我们需要在 C++ 代码中提供一个 C 语言可以理解的接口 .cpp 文件。

1. 主要方法:使用 extern "C"

extern "C" 是 C++ 提供的一个关键字,用于指示编译器以 C 语言的规则来编译指定的代码块或函数声明。这意味着:

  1. 禁用名称修饰(Name Mangling):编译器会按照 C 语言的方式处理函数名,使其在链接时能被 C 代码找到。
  2. 遵循 C 调用约定:确保函数参数传递和栈处理方式与 C 语言兼容。

2. 实现步骤:构建 C 到 C++ 的桥梁

要让 C 代码能够调用 C++ 的功能,我们需要搭建一座“桥梁”。核心思路是在 C++ 中创建一个符合 C 语言规范的接口层。以下是详细步骤:

  1. 设计并实现 C++ 功能模块 (calculator.hpp & calculator.cpp):

    • 像往常一样,在 .hpp 文件中定义你的 C++ 类(例如 Calculator),包含其成员变量和成员函数声明。
    • 在对应的 .cpp 文件中实现这些成员函数(构造函数、析构函数、add, subtract 等)。这个文件只包含纯粹的 C++ 类实现,不涉及 extern "C"
  2. 定义 C 语言接口头文件 (c_interface.h):

    • 创建一个 .h 头文件,这将是 C 代码和 C++ 接口代码共同的“契约”。
    • 关键: 使用 #ifdef __cplusplusextern "C" 条件编译指令。这确保:
      • 当被 C++ 编译器包含时,extern "C" 生效,声明的函数将具有 C 链接规范(无名称修饰,C 调用约定)。
      • 当被 C 编译器包含时,extern "C" 部分被忽略,C 编译器只看到标准的 C 函数声明。
    • 推荐: 在此头文件中定义一个不透明指针类型(例如 typedef void* CalculatorHandle;)作为 C 代码中代表 C++ 对象的“句柄”。这样可以完全隐藏 C++ 的类型细节。
    • 声明一组 C 风格的函数原型(例如 CalculatorHandle createCalculator();, void destroyCalculator(CalculatorHandle handle);, double add(CalculatorHandle handle, double a, double b); 等),这些函数将构成 C 语言可以调用的接口。
  3. 实现 C 语言接口包装层 (c_interface.cpp):

    • 创建一个新的 C++ 源文件(.cpp),专门用于实现 c_interface.h 中声明的那些 C 风格接口函数。
    • 包含必要的头文件: #include "c_interface.h"#include "calculator.hpp"
    • 实现包装函数 (Wrapper Functions):
      • createCalculator(): 内部使用 new Calculator() 创建 C++ 对象。
      • destroyCalculator(CalculatorHandle handle): 接收 C 代码传来的指针,然后使用 delete 销毁 C++ 对象。
      • add(CalculatorHandle handle, double a, double b) 等函数: 接收句柄,将其转换Calculator*,然后调用实际的 C++ 成员函数 (calc->add(a, b)),并返回结果。
    • 注意: 这些函数的实现本身是在 C++ 文件中,可以使用 C++ 特性(如 new, delete, static_cast, try-catch 处理异常等),但因为它们在 c_interface.h 中被 extern "C" 声明过,所以最终会被编译为 C 链接规范的函数。
  4. 在 C 代码中调用接口 (main.c):

    • 包含 C 接口头文件 #include "c_interface.h"不要包含 C++ 的头文件 (calculator.hpp)。
    • 像调用普通 C 函数一样调用 c_interface.h 中声明的接口函数。
    • 使用指针类型的变量来存储和传递 C++ 对象的句柄。
    • 极其重要: 必须在使用完 C++ 对象后,显式调用对应的销毁函数(如 destroyCalculator(calcHandle)) 来释放资源,防止内存泄漏。
  5. 编译和链接 (使用 C++ 编译器):

    • 使用 C++ 编译器 (例如 g++) 来编译所有的 C++ 源文件 (calculator.cpp, c_interface.cpp)。
    • 可以使用 C 编译器 (gcc) 或 C++ 编译器 (g++) 来编译 C 源文件 (main.c)。(g++ 通常也能很好地处理 C 代码)。
    • 关键链接步骤: 必须使用 C++ 编译器 (g++) 将所有生成的目标文件 (.o) 链接在一起形成最终的可执行文件。这

3. 示例:使用 extern “C” 包装 C++ 计算器类程序

假设我们有一个简单的 C++ 类 Calculator,我们希望能在 C 程序中使用它的加减乘除功能。

calculator.hpp (C++ 类头文件)

#ifndef CALCULATOR_HPP
#define CALCULATOR_HPP#include <stdexcept> // For exception handlingclass Calculator {
private:double last_result;public:Calculator();~Calculator(); // Destructordouble add(double a, double b);double subtract(double a, double b);double multiply(double a, double b);double divide(double a, double b);double getLastResult() const;
};#endif // CALCULATOR_HPP

说明: 这是标准的 C++ 头文件,定义了 Calculator 类的接口。C 代码不应该直接包含这个文件。

calculator.cpp (C++ 实现文件)

#include "c_interface.h"
#include "calculator.hpp"Calculator* createCalculator() {return new Calculator();
}void destroyCalculator(Calculator* calc) {delete calc;
}double add(Calculator* calc, double a, double b) {return calc->add(a, b);
}double subtract(Calculator* calc, double a, double b) {return calc->subtract(a, b);
}double multiply(Calculator* calc, double a, double b) {return calc->multiply(a, b);
}double divide(Calculator* calc, double a, double b) {return calc->divide(a, b);
}double getLastResult(Calculator* calc) {return calc->getLastResult();
}

c_interface.h (接口头文件,供 C++ 接口文件使用)

接口头文件 (c_interface.h) 的角色:

  • 这个头文件是 C 和 C++ 之间的“契约”。

  • #ifdef __cplusplus / extern “C” / #endif 的组合是关键:

    • 当被 C++ 编译器处理时(如 c_interface.cpp 包含它),extern “C” 生效,确保函数以 C 方式导出。

    • 当被 C 编译器处理时(如 main.c 包含它),#ifdef __cplusplus 为假,extern “C” 部分被忽略,C 编译器只看到标准的 C 函数声明。

  • 重要: C 代码 (main.c) 通过这个头文件只知道存在一些 C 风格的函数(如 createCalculator, add 等)。它完全不知道 C++ 的 Calculator 类的内部细节,这实现了良好的封装。

#ifndef C_INTERFACE_H
#define C_INTERFACE_H#include "calculator.hpp"#ifdef __cplusplus
extern "C" {
#endifCalculator* createCalculator();
void destroyCalculator(Calculator* calc);
double add(Calculator* calc, double a, double b);
double subtract(Calculator* calc, double a, double b);
double multiply(Calculator* calc, double a, double b);
double divide(Calculator* calc, double a, double b);
double getLastResult(Calculator* calc);#ifdef __cplusplus
}
#endif#endif

c_interface.cpp (接口实现文件,间接实现其他 .C 文件调用面向对象功能)

#include "c_interface.h"
#include "calculator.hpp"Calculator* createCalculator() {return new Calculator();
}void destroyCalculator(Calculator* calc) {delete calc;
}double add(Calculator* calc, double a, double b) {return calc->add(a, b);
}double subtract(Calculator* calc, double a, double b) {return calc->subtract(a, b);
}double multiply(Calculator* calc, double a, double b) {return calc->multiply(a, b);
}double divide(Calculator* calc, double a, double b) {return calc->divide(a, b);
}double getLastResult(Calculator* calc) {return calc->getLastResult();
}

main.c (C 主程序)

​ 这是纯 C 代码。它只依赖 c_interface.h 定义的函数。注意,它需要手动调用 destroyCalculator 来管理 C++ 对象的生命周期。

#include "c_interface.h"
#include <stdio.h>int main() {Calculator* calc = createCalculator();double result = add(calc, 10, 20);printf("Result: %f\n", result);result = subtract(calc, 10, 20);printf("Result: %f\n", result);result = multiply(calc, 10, 20);printf("Result: %f\n", result);result = divide(calc, 10, 20);printf("Result: %f\n", result);destroyCalculator(calc);return 0;
}

编译和链接 (使用 G++)

以 vscode 中的task.json配置为例: 必须使用 C++ 编译器 (g++) 进行链接,因为它需要链接 C++ 标准库来支持 new, delete, 异常处理等。g++ 可以同时编译 C (.c) 和 C++ (.cpp) 源文件。

{"tasks": [{"type": "cppbuild","label": "Build Project (main.c + C++ files)","command": "D:\\mingw64\\bin\\g++.exe","args": ["-fdiagnostics-color=always","-g","${workspaceFolder}/main.c","${workspaceFolder}/c_interface.cpp","${workspaceFolder}/calculator.cpp","-o","${workspaceFolder}/main.exe"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": ["$gcc"],"group": {"kind": "build","isDefault": true},"detail": "Compiles main.c, c_interface.cpp, calculator.cpp and links them into main.exe"}],"version": "2.0.0"
}

g++ 编译命令实际为:

g++.exe -g ./main.c ./c_interface.cpp ./calculator.cpp -o ./main.exe

执行结果:

PS E:\Learning_Record\code> g++.exe -g ./main.c ./c_interface.cpp ./calculator.cpp -o ./main.exe
PS E:\Learning_Record\code> .\main.exe
C++ Calculator object created.
Result: 30.000000
Result: -10.000000
Result: 200.000000
Result: 0.500000
C++ Calculator object destroyed.

4. 使用不透明指针 (void*) 作为句柄 (Handle)

​ 虽然上面的程序 c_interface.h 直接使用了 Calculator*,并且在 C 代码中也能工作(因为 C 编译器把它当作一个未定义类型的指针),但更健壮和推荐的做法是在 C 接口中使用 不透明指针 (void*) 来代表 C++ 对象。

  • 优点:

    • 完全隐藏 C++ 类型: C 代码完全不需要知道 Calculator 这个名字,增加了封装性。
    • 避免潜在的 C 编译器警告/错误: C 编译器看到 Calculator* 可能会有疑问,而 void* 是标准的未知类型指针。
    • 隐藏 C++ 复杂性 (Hiding C++ Complexity): C 代码的开发者不需要了解 C++ 的特性,如类、构造/析构、模板、异常处理、名称修饰等。他们只需要像调用普通 C 函数一样使用接口。
  • 修改示例:

    c_interface.h (接口头文件,供 C++ 接口文件使用)

    #ifndef C_INTERFACE_H
    #define C_INTERFACE_H// Remove direct include of calculator.hpp for C code if not strictly necessary
    // #include "calculator.hpp" // C code doesn't need the full C++ class definition// Define the opaque pointer type for C code
    typedef void* CalculatorHandle;#ifdef __cplusplus
    extern "C" {
    #endif// Functions now use CalculatorHandle
    CalculatorHandle createCalculator();
    void destroyCalculator(CalculatorHandle calc);
    double add(CalculatorHandle calc, double a, double b);
    double subtract(CalculatorHandle calc, double a, double b);
    double multiply(CalculatorHandle calc, double a, double b);
    double divide(CalculatorHandle calc, double a, double b);
    double getLastResult(CalculatorHandle calc);#ifdef __cplusplus
    }
    #endif#endif // C_INTERFACE_H
    

    c_interface.cpp (接口实现文件,间接实现其他 .C 文件调用面向对象功能)

    #include "c_interface.h"
    #include "calculator.hpp"
    #include <stdexcept>CalculatorHandle createCalculator() {return new Calculator();
    }void destroyCalculator(CalculatorHandle handle) {Calculator* calc = static_cast<Calculator*>(handle);delete calc;
    }double add(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->add(a, b);
    }double subtract(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->subtract(a, b);
    }double multiply(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);return calc->multiply(a, b);
    }double divide(CalculatorHandle handle, double a, double b) {Calculator* calc = static_cast<Calculator*>(handle);try {return calc->divide(a, b);} catch (const std::exception& e) {fprintf(stderr, "Error in divide: %s\n", e.what());return 0.0;}
    }double getLastResult(CalculatorHandle handle) {Calculator* calc = static_cast<Calculator*>(handle);return calc->getLastResult();
    }
    

    static_cast<Calculator*>(handle): 这是 C++ 中的显式类型转换 (Explicit Type Casting)。

    • static_cast 是 C++ 提供的一种类型转换操作符,用于在编译时进行类型检查的转换。它比 C 风格的强制类型转换更安全、更明确。

    • <Calculator*> 指定了我们想要将 handle 转换成的目标类型,也就是“指向 Calculator 的指针”。

    • (handle) 是要被转换的源变量或表达式。在这个例子中,handle 的类型是 CalculatorHandle,也就是我们之前 typedef 定义的 void*。void* 是一种通用指针,它可以指向任何类型的对象,但它本身不包含类型信息。

    类似于C 风格的强制类型转换:

    // 假设 handle 是一个 void*,并且你知道它指向 MyStruct
    struct MyStruct *ptr = (struct MyStruct*)handle;
    // 现在可以使用 ptr->member
    

    main.c (C 主程序)

    #include "c_interface.h"
    #include <stdio.h>
    #include <stdlib.h>int main() {CalculatorHandle calc = createCalculator();if (calc == NULL) {fprintf(stderr, "Error: Failed to create Calculator object.\n");return EXIT_FAILURE;}printf("Calling C++ functions via C interface...\n");double res_add = add(calc, 10.5, 20.0);printf("add(10.5, 20.0) = %f\n", res_add);double res_sub = subtract(calc, res_add, 5.5);printf("subtract(%f, 5.5) = %f\n", res_add, res_sub);double res_mul = multiply(calc, res_sub, 2.0);printf("multiply(%f, 2.0) = %f\n", res_sub, res_mul);double res_div = divide(calc, res_mul, 10.0);printf("divide(%f, 10.0) = %f\n", res_mul, res_div);printf("Attempting division by zero...\n");double res_div_zero = divide(calc, 100.0, 0.0);printf("divide(100.0, 0.0) = %f (check for error indicator)\n", res_div_zero);double last_res = getLastResult(calc);printf("Last result stored in calculator: %f\n", last_res);destroyCalculator(calc);printf("Calculator object destroyed.\n");return 0;
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78534.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

汽车制造行业如何在数字化转型中抓住机遇?

近年来&#xff0c;随着新一轮科技革命和产业变革的深入推进&#xff0c;汽车制造行业正迎来一场前所未有的数字化转型浪潮。无论是传统车企还是新势力品牌&#xff0c;都在积极探索如何通过数字化技术提升竞争力、开拓新市场。那么&#xff0c;在这场变革中&#xff0c;汽车制…

k8s学习记录(五):Pod亲和性详解

一、前言 上一篇文章初步探讨了 Kubernetes 的节点亲和性&#xff0c;了解到它在 Pod 调度上比传统方式更灵活高效。今天我们继续讨论亲和性同时Kubernetes 的调度机制。 二、Pod亲和性 上一篇文章中我们介绍了节点亲和性&#xff0c;今天我们讲解一下Pod亲和性。首先我们先看…

HarmonyOS:Navigation实现导航之页面设置和路由操作

导读 设置标题栏模式设置菜单栏设置工具栏路由操作页面跳转页面返回页面替换页面删除移动页面参数获取路由拦截 子页面页面显示类型页面生命周期页面监听和查询 页面转场关闭转场自定义转场共享元素转场 跨包动态路由系统路由表自定义路由表 示例代码 Navigation组件适用于模块…

雪花算法

目录 一、什么是雪花算法 二、使用雪花算法 ​三、使用UUID 使用自增主键是数据库中常用的唯一标识&#xff0c;今天尝试使用mybatisplus来实现三种方式的主键ID 使用起来也很简单 用注解指定一下使用那种方式的主键 一、什么是雪花算法 一种特殊的算法可以计算得到一个唯…

HarmonyOs @hadss/hmrouter路由接入

参考文档&#xff1a;官方文档 在根目录oh-package.json5配置 {"dependencies": {"hadss/hmrouter": "^1.0.0-rc.11"} }加入路由编译插件 hvigor/hvigor-config.json文件 {"dependencies": {"hadss/hmrouter-plugin": &…

C++学习笔记(三十八)——STL之修改算法

STL 算法分类&#xff1a; 类别常见算法作用排序sort、stable_sort、partial_sort、nth_element等排序搜索find、find_if、count、count_if、binary_search等查找元素修改copy、replace、replace_if、swap、fill等修改容器内容删除remove、remove_if、unique等删除元素归约for…

Crawl4AI 部署安装及 n8n 调用,实现自动化工作流(保证好使)

Crawl4AI 部署安装及 n8n 调用&#xff0c;实现自动化工作流&#xff08;保证好使&#xff09; 简介 Crawl4AI 的介绍 一、Crawl4AI 的核心功能 二、Crawl4AI vs Firecrawl Crawl4AI 的本地部署 一、前期准备 二、部署步骤 1、检查系统的网络环境 2、下载 Crawl4AI 源…

32单片机——外部中断

STM32F103ZET6的系统中断有10个&#xff0c;外部中断有60个 1、中断的概念 中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的&#xff0c;中断功能的存在&#xff0c;很大程度上提高了单片机处理外部或内部事件的能力 eg&#xff1a;&#xff1a;你打开火&…

UG NX二次开发(C#)-获取具有相同属性名称的体对象

文章目录 1、前言2、在UG NX中的属性的赋予3、通过UG NX二次开发获取相同属性的体对象1、前言 UG NX中每个对象都可以属于属性的,包括体、面、边、特征、基准等。在QQ群中有个群有提出一个问题,就是获取相同属性的体对象,然后将这个体对象导出到一个part文件中。我们今天先…

手动实现legend 与 echarts图交互 通过元素和js事件实现图标某项的高亮 显示与隐藏

通过html实现legend的样式 提供调用echarts的api实现与echarts图表交互的效果 实现饼图element实现类似于legend与echartstu表交互效果 效果图 配置代码 <template><div style"height: 400px; width: 500px;background-color: #CCC;"><v-chart:opti…

SpringBoot与BookKeeper整合,实现金融级别的日志存储系统

BookKeeper的优势 高吞吐量和低延迟 分布式架构: Apache BookKeeper采用分布式的架构设计&#xff0c;能够支持高并发的写入和读取操作。 批量写入: 支持批量写入日志条目&#xff0c;显著提高写入效率。 异步I/O: 使用异步I/O操作&#xff0c;减少等待时间&#xff0c;提升…

【Bug】 [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

当你在进行深度学习相关操作时&#xff0c;若因缺少本地的 CA 证书而无法下载资源&#xff0c;下面为你介绍几种解决办法&#xff1a; 方法一&#xff1a;更新 CA 证书 在大多数 Linux 发行版中&#xff0c;你可以使用包管理器来更新 CA 证书。例如&#xff0c;在基于 Debian…

Vue3中AbortController取消请求的用法详解

在 Vue3 中&#xff0c;AbortController 用于取消 fetch 请求&#xff0c;避免组件卸载后仍执行异步操作导致的潜在问题&#xff08;如内存泄漏或更新已销毁组件的状态&#xff09;。以下是详细用法和最佳实践&#xff1a; 一、基本用法 创建 AbortController 实例 在组件 setu…

【刷题Day26】Linux命令、分段分页和中断(浅)

说下你常用的 Linux 命令&#xff1f; 文件与目录操作&#xff1a; ls&#xff1a;列出当前目录的文件和子目录&#xff0c;常用参数如-l&#xff08;详细信息&#xff09;、-a&#xff08;包括隐藏文件&#xff09;cd&#xff1a;切换目录&#xff0c;用于在文件系统中导航m…

Spring Boot 参考文档导航手册

&#x1f4da; Spring Boot 参考文档导航手册 &#x1f5fa;️ ✨ 新手入门 &#x1f476; 1️⃣ &#x1f4d6; 基础入门&#xff1a;概述文档 | 环境要求 | 安装指南 2️⃣ &#x1f527; 实操教程&#xff1a;上篇 | 下篇 3️⃣ &#x1f680; 示例运行&#xff1a;基础篇 …

卷积神经网络(CNN)详细教程

卷积神经网络&#xff08;CNN&#xff09;详细教程 一、引言 卷积神经网络&#xff08;Convolutional Neural Networks, CNN&#xff09;是一种深度学习模型&#xff0c;广泛应用于图像识别、视频分析、自然语言处理等领域。CNN通过模拟人类视觉系统的层次结构&#xff0c;能够…

解决SSLError: [SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC] decryption faile的问题

问题描述&#xff1a; 在pip安装第三方库时&#xff0c;出现SSL的问题。 传输层安全性协议&#xff08;TLS&#xff09;及其前身安全套接层&#xff08;SSL&#xff09;是现在的 HTTPS 协议中的一种安全协议&#xff0c;目的是为互联网通信提供安全及数据完整性保障。而较新版…

SpringBoot程序的创建以及特点,配置文件,LogBack记录日志,配置过滤器、拦截器、全局异常

目录 一、创建一个SpringBoot程序 二、SpringBoot的特点 ①主要特点 ②其他特点 ③热部署 启动热部署 关闭热部署 三、SpringBoot的配置文件 ①SpringBoot三种配置文件的格式&#xff08;以设置端口号为例&#xff09;&#xff1a; ②配置文件的优先级 ③常见配置项 1…

i18n-ai-translate开源程序,可以使用DeepSeek等模型将您的 i18nJSON翻译成任何语言

一、软件介绍 文末提供程序和源码下载 i18n-ai-translate开源程序使用 DeepSeek等模型可以将您的 i18n JSON 翻译成任何语言。 无缝翻译本地化文件。支持嵌套翻译文件的目录。需要i18next样式的JSON 文件&#xff08;文末一并提供下载&#xff09;。 二、模式 CSV 模式 三个…

Flask + ajax上传文件(一)--单文件上传

一、概述 本教程将教你如何使用Flask后端和AJAX前端实现文件上传功能,包含完整的代码实现和详细解释。 二、环境准备 1. 所需工具和库 Python 3.xFlask框架jQuery库Bootstrap(可选,用于美化界面)2. 安装Flask pip install flask三、项目结构 upload_project/ ├── a…