C 調用 C++:extern “C” 接口詳解與實踐
核心問題在于 C++ 編譯器會對函數名進行“修飾”(Name Mangling)以支持函數重載等特性,而 C 編譯器則不會。此外,C 語言本身沒有類、對象等概念。為了解決這個問題,我們需要在 C++ 代碼中提供一個 C 語言可以理解的接口 .cpp 文件。
1. 主要方法:使用 extern "C"
extern "C"
是 C++ 提供的一個關鍵字,用于指示編譯器以 C 語言的規則來編譯指定的代碼塊或函數聲明。這意味著:
- 禁用名稱修飾(Name Mangling):編譯器會按照 C 語言的方式處理函數名,使其在鏈接時能被 C 代碼找到。
- 遵循 C 調用約定:確保函數參數傳遞和棧處理方式與 C 語言兼容。
2. 實現步驟:構建 C 到 C++ 的橋梁
要讓 C 代碼能夠調用 C++ 的功能,我們需要搭建一座“橋梁”。核心思路是在 C++ 中創建一個符合 C 語言規范的接口層。以下是詳細步驟:
-
設計并實現 C++ 功能模塊 (
calculator.hpp
&calculator.cpp
):- 像往常一樣,在
.hpp
文件中定義你的 C++ 類(例如Calculator
),包含其成員變量和成員函數聲明。 - 在對應的
.cpp
文件中實現這些成員函數(構造函數、析構函數、add
,subtract
等)。這個文件只包含純粹的 C++ 類實現,不涉及extern "C"
。
- 像往常一樣,在
-
定義 C 語言接口頭文件 (
c_interface.h
):- 創建一個
.h
頭文件,這將是 C 代碼和 C++ 接口代碼共同的“契約”。 - 關鍵: 使用
#ifdef __cplusplus
和extern "C"
條件編譯指令。這確保:- 當被 C++ 編譯器包含時,
extern "C"
生效,聲明的函數將具有 C 鏈接規范(無名稱修飾,C 調用約定)。 - 當被 C 編譯器包含時,
extern "C"
部分被忽略,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 語言可以調用的接口。
- 創建一個
-
實現 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 鏈接規范的函數。
- 創建一個新的 C++ 源文件(
-
在 C 代碼中調用接口 (
main.c
):- 包含 C 接口頭文件
#include "c_interface.h"
。不要包含 C++ 的頭文件 (calculator.hpp
)。 - 像調用普通 C 函數一樣調用
c_interface.h
中聲明的接口函數。 - 使用指針類型的變量來存儲和傳遞 C++ 對象的句柄。
- 極其重要: 必須在使用完 C++ 對象后,顯式調用對應的銷毀函數(如
destroyCalculator(calcHandle)
) 來釋放資源,防止內存泄漏。
- 包含 C 接口頭文件
-
編譯和鏈接 (使用 C++ 編譯器):
- 使用 C++ 編譯器 (例如
g++
) 來編譯所有的 C++ 源文件 (calculator.cpp
,c_interface.cpp
)。 - 可以使用 C 編譯器 (
gcc
) 或 C++ 編譯器 (g++
) 來編譯 C 源文件 (main.c
)。(g++
通常也能很好地處理 C 代碼)。 - 關鍵鏈接步驟: 必須使用 C++ 編譯器 (
g++
) 將所有生成的目標文件 (.o
) 鏈接在一起形成最終的可執行文件。這
- 使用 C++ 編譯器 (例如
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; }
-