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.pswp.cn/pingmian/78534.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/78534.shtml
英文地址,請注明出處:http://en.pswp.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…