首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C/C++ 動態鏈接庫創建全解析:extern "C" 與 __declspec(dllexport/dllimport) 深度解析

     編輯:關於C++

一、extern "C":跨語言鏈接的橋樑

1. 名稱修飾問題

C++ 支持函數重載,編譯器通過名稱修飾將函數簽名編碼進符號名:void foo(int, float) → _foo_int_float。C 語言無重載,符號名僅為 _foo。直接混用會導致鏈接階段找不到符號。

2. extern "C" 作用

  • 指示編譯器:按 C 語言規則生成符號名(不修飾)
  • 使 C++ 代碼可被 C 語言調用,或被 GetProcAddress/DllImport 按名查找

3. 標準頭文件護衛模板

#ifndef MYLIB_H
#define MYLIB_H

#ifdef __cplusplus
extern "C" {
#endif

// 函數聲明
int add(int a, int b);
void init();

#ifdef __cplusplus
}
#endif

#endif

4. 雙重含義解析

  • extern:聲明符號具有外部鏈接,可跨文件訪問(對應 static 內部鏈接)
  • "C":指定C 語言鏈接規約(無名稱修飾)

5. 混合編程四種場景

場景C++ 頭文件C 實現文件
C++ 調用 Cextern "C" { #include "c.h" }普通 extern int add();
C 調用 C++extern "C" int add();extern int add();(不可含 extern "C")
C++ 實現供 C 用extern "C" int add();extern int add();
C 實現供 C++ 用extern "C" { #include "c.h" }普通實現

二、__declspec(dllexport) / __declspec(dllimport)

1. dllexport:導出符號

將函數/類/變量放入 DLL 導出表,供外部調用。等價於 .def 文件 EXPORTS 節。

__declspec(dllexport) int add(int a, int b);  // 導出函數
__declspec(dllexport) class MyClass { ... };  // 導出整個類

2. dllimport:導入符號(優化調用)

  • 函數:非必需,但允許編譯器生成直接調用指令(跳過 IAT 間接跳轉),提升性能
  • 數據/變量:必須使用,否則無法正確訪問 DLL 中的全局/靜態變量

3. 經典宏切換模式

#ifdef MYLIB_EXPORTS  // DLL 端定義此宏
#define MYLIB_API __declspec(dllexport)
#else                 // 客戶端不定義
#define MYLIB_API __declspec(dllimport)
#endif

MYLIB_API int add(int a, int b);
MYLIB_API class MyClass { ... };

4. 類導出與靜態成員陷阱

導出類時,靜態成員變量必須在客戶端正確導入:

// MyClass.h
class MYLIB_API MyClass {
    static int s_value;  // 靜態成員
public:
    static int getValue();
};

// MyClass.cpp (DLL 端)
int MyClass::s_value = 0;  // 定義

// 客戶端若無 dllimport,鏈接時找不到 s_value!

原因:靜態成員實際在 DLL 數據段,客戶端需通過 dllimport 正確解析地址。類導出宏自動處理成員函數,但靜態數據成員需顯式 dllimport。

三、完整實戰:C++ DLL + C# 調用

1. C++ DLL 導出

// MathLib.h
#ifdef MATHLIB_EXPORTS
#define MATH_API __declspec(dllexport)
#else
#define MATH_API __declspec(dllimport)
#endif

extern "C" MATH_API int Add(int a, int b);
extern "C" MATH_API int Sub(int a, int b);

// MathLib.cpp
#include "MathLib.h"
extern "C" MATH_API int Add(int a, int b) { return a + b; }
extern "C" MATH_API int Sub(int a, int b) { return a - b; }

2. C# P/Invoke 調用

public static class MathLib {
    [DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Add(int a, int b);
    
    [DllImport("MathLib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Sub(int a, int b);
}

// 使用
int sum = MathLib.Add(10, 20);  // 30

關鍵點

  • CallingConvention.Cdecl:匹配 C/C++ 默認調用約定(調用方清棧)
  • x64 平台:C++ 編譯目標 x64,C# 目標平台 x64,DLL 放入輸出目錄
  • 依賴檢查:Dependency Walker 確認導出符號名無修飾(Add 而非 _Add@8)

四、常見錯誤與調試

總結

  1. extern "C" 解決名稱修飾,實現 C/C++/C# 互調
  2. dllexport/dllimport 管理導出導入,優化調用、正確訪問數據
  3. 宏切換 單一頭文件服務 DLL 端與客戶端
  4. 調用約定、位數、符號名 三一致是跨語言調用鐵律
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
錯誤原因解決
Unresolved external symbol缺少 dllexport / 宏未定義檢查 MYLIB_EXPORTS 定義、頭文件包含順序
BadImageFormatExceptionx86/x64 不匹配統一編譯目標平台
EntryPointNotFound名稱修飾 / 拼寫錯誤加 extern "C"、用 Dependency Walker 查導出名
靜態成員訪問失敗缺少 dllimport類導出宏需覆蓋靜態數據成員