動手 | 奶奶級的動態庫入門
程式編譯過程
庫檔案
靜態連結和動態連結的區別?
從0開始 - 建立和使用靜態連結庫
建立靜態庫專案
向靜態庫中新增檔案
編譯靜態庫
建立引用靜態庫的C++控制檯應用
在應用中使用靜態庫功能
從0開始 - 建立和使用 DLL
建立 DLL 專案
向 DLL 中新增檔案
編譯動態庫
創造使用 DLL 的客戶端應用
隱式呼叫
顯式呼叫
演練
建立 DLL 專案
向 DLL 中新增檔案
建立使用 DLL 的客戶端應用
將 DLL 標頭新增到包含路徑
將 DLL 匯入庫新增到專案中
(這個目錄來的,別點了,還是這頁)
筆記不寫容易忘,誰腦袋記得住這麼多東西(禿)。
事情是這樣的,上學期老師給的一個閱讀程式碼的任務,但由於我常年身處文化沙漠死活讀不懂。一學期過去了,懶癌終於拿起花瓶澆自己了。
今天的筆記內容說的是平時經常能看見的,執行 VS 專案的時候老在下方載入的
。dll
。包括一小部分的理論和超大部分的實操。
什麼是動態連結庫(DLL)?
解構一下,”動態“、”連結“、”庫“。
動態:與靜態相對,對比理解。
連結:程式編譯過程的一步。
庫:一種程式碼倉庫。
程式編譯過程
編譯
:把文字形式的原始碼翻譯成機器語言,並形成目標檔案。
預處理
:處理 # 開頭的的指令。(。cpp 。i)
編譯最佳化
:確定指令是否符合規則,之後翻譯成彙編程式碼。(。i 。s)
彙編
:把組合語言翻譯成目標機器指令,生成目標檔案。(。s 。o)
連結
:把目標檔案、作業系統的啟動程式碼和庫檔案組織起來形成可執行程式。(。o 。exe )
庫檔案
可以簡單的把庫檔案看成一種程式碼倉庫,它提供給使用者一些可以直接拿來用的變數、函式或類。
庫檔案分為靜態庫和動態庫。
靜態連結和動態連結的區別?
預編譯 -> 編譯 -> 彙編 -> ==連結==
區別在於連結階段如何處理庫。
靜態庫(.lib)
:連結階段將彙編生成的目標檔案。o和引用的庫一起連結打包到可執行檔案。exe 中。
特點:移植方便、浪費空間和資源。
動態庫(.dll)
:程式執行時載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共享庫的例項。
特點:資源共享、模組化、簡化部署和安裝。
C++ 操作例項
Windows10、x64
VS2019、C++ 的桌面開發
接下來介紹如何建立和使用動態連結庫:
建立和使用靜態連結庫
建立和使用動態連結庫
隱式呼叫
顯式呼叫
C語言模組
模組定義檔案
為了便於學習,簡化理解,先
從0開始
建立靜態庫和動態庫,然後使用應用程式來連結庫。最後透過
演練
應用來實操 DLL 。
接下來的例項可以分解為以下任務:
建立 DLL 專案。
宣告和實現函式,匯出函式到專案。
建立控制檯應用專案。
從 DLL 匯入函式。
執行應用。
從0開始 - 建立和使用靜態連結庫
使用靜態庫是重用程式碼的一種絕佳方式。
不必在每個應用中重新實現同一例程,只需將其寫入靜態資料庫一次,然後引用它們即可。 從靜態庫連結的程式碼成為了應用的一部分,這樣就不必安裝另一個檔案來使用程式碼。
接下來從空專案開始建立靜態庫。
建立靜態庫專案
將“語言” 設定為“C++” ,將“平臺” 設定為“Windows” ,並將“專案型別” 設定為“庫”。選擇“Windows 桌面嚮導”。
靜態庫、空專案。
向靜態庫中新增檔案
新建標頭檔案、原始檔。
在
testlib。h
中新增宣告:
#ifndef TESTLIB_H
#define TESTLIB_H
// 防止標頭檔案重複包含
// 條件編譯指令
// 加減乘除
int
add
(
int
a
,
int
b
);
// 宣告
#else
int
a
=
0
;
// 滿足條件才會編譯,所以這裡程式碼沒有被高亮。
#endif
在
testlib。cpp
中實現功能:
#include
“testlib。h”
// 加法
int
add
(
int
a
,
int
b
)
// 實現
{
return
a
+
b
;
}
編譯靜態庫
在選單欄上依次選擇“生成” > “生成解決方案” ,將建立一個可供其他程式使用的靜態庫
testLib。lib
。
檢視專案路徑可以找到。
注:
。lib
檔案不能獨立執行。
至此,靜態庫已經建立完畢!
建立引用靜態庫的C++控制檯應用
在“解決方案資源管理器”中,右鍵單擊頂部節點“解決方案”,開啟快捷選單 。 選擇“新增” > “新建專案”,開啟“新增新專案”對話方塊 。
控制檯、空專案。
在應用中使用靜態庫功能
必須引用靜態庫才能使用其中的算術例程。 開啟“解決方案資源管理器”中
test
專案的快捷選單,然後選擇“新增” > “引用” 。然後選中之前編寫的
。lib
檔案。
若要引用
testlib。h
標頭檔案,需要修改引用路徑。
在“解決方案資源管理器”中,右鍵單擊
test
,選擇“屬性” 。
將“配置”下拉列表設定為“所有配置” 。 將“平臺”下拉列表設定為“所有平臺” 。
”連結器 - 常規 - 附加庫目錄“ 中指定存放
。lib
的資料夾路徑,推薦使用相對路徑,這樣遷移的時候不容易出錯。
在
test。cpp
中呼叫:
#include
#include
#include
“。。/testLib/testlib。h” // 引用路徑
using
namespace
std
;
int
main
()
{
cout
<<
“input 2 integer:”
;
int
a
,
b
;
cin
>>
a
>>
b
;
// 呼叫靜態庫
printf
(
“%d + %d = %d”
,
a
,
b
,
add
(
a
,
b
));
return
0
;
}
最後執行一下就行!
至此,靜態庫呼叫完成!
從0開始 - 建立和使用 DLL
與靜態連結庫不同,Windows 在載入時或在執行時將應用中的匯入連線到 DLL 中的匯出,而不是在連結時連線它們。
Windows 需要不屬於標準 C++ 編譯模型的額外資訊才能建立這些連線。 MSVC 編譯器實現了一些 Microsoft 專用 C++ 擴充套件,以提供此額外資訊。接下來我們將介紹這些擴充套件。
DLL 使用 C 呼叫約定。 只要平臺、呼叫約定和連結約定匹配,便可從採用其他程式語言編寫的應用中進行呼叫。客戶端應用使用隱式連結,其中 Windows 在載入時將應用連結到 DLL。此連結允許應用呼叫 DLL 提供的函式,就像呼叫靜態連結庫中的函式一樣。
建立 DLL 專案
新建動態庫、空專案(操作可參考 建立靜態庫專案 )
向 DLL 中新增檔案
然後新增標頭檔案、原始檔:
在
testdll。h
中新增宣告:
#ifndef TESTDLL_H
#define TESTDLL_H
// 條件編譯指令
// 在預處理器裡事先定義好_DLLAPI,保證dll專案有預定義;
// 而新程式專案裡沒有,從而區分匯入和匯出。
#ifdef _DLLAPI
#define DLLAPI __declspec(dllexport)
// 匯出
#else
#define DLLAPI __declspec(dllimport)
// 匯入
#endif
// 宣告匯出函式
DLLAPI
int
add
(
int
a
,
int
b
);
// 匯出add介面
#endif
並且在預處理器裡進行定義:
在
testdll。cpp
中實現功能:
#include
“testdll。h”
int
add
(
int
a
,
int
b
)
// 函式實現
{
return
a
+
b
;
}
編譯動態庫
在選單欄上依次選擇“生成” > “生成解決方案” ,將建立
。dll
和相關編譯器輸出。
檢視輸出:
至此,動態連結庫已經創造完成!
創造使用 DLL 的客戶端應用
建立新專案:控制檯、
。exe
(操作參考 建立引用靜態庫的C++控制檯應用 )
新增原始檔
使用 DLL 需要
:查詢宣告 DLL 匯出的標頭、連結器的匯入庫和 DLL 本身。
方法
:
建議在客戶端專案中設定包含路徑,使其直接包括 DLL 專案中的 DLL 標頭檔案。
在客戶端專案中設定庫路徑以包括 DLL 專案中的 DLL 匯入庫。
將生成的 DLL 從 DLL 專案複製到客戶端生成輸出目錄中。
可執行檔案可以透過以下兩種方法連結到DLL:
隱式連結
作業系統會與使用 DLL 的可執行檔案同時載入它。
客戶端可執行檔案呼叫 DLL 的匯出函式的方式與函式進行靜態連結幷包含在可執行檔案中時的方式相同。
隱式連結有時稱為靜態載入 或載入時動態連結 。
顯式連結
作業系統會在執行時按需載入 DLL。
透過顯式連結使用 DLL 的可執行檔案必須顯式載入和解除安裝 DLL。它還必須設定函式指標,用於訪問它從 DLL 使用的每個函式。
顯式連結有時稱為動態載入 或執行時動態連結 。
隱式呼叫
跟靜態庫呼叫類似。
右鍵
test
專案,屬性 - “連結器” - “常規” - 附加庫目錄,編輯新增
。lib
檔案存放路徑。
在
test。cpp
檔案裡寫入:
#include
#include
“。。/testDll/testdll。h”
#pragma comment(lib, “testDll。lib”)
// 隱式呼叫,類似靜態庫但不同
using
namespace
std
;
int
main
()
{
int
a
,
b
;
cout
<<
“input 2 integer:”
;
cin
>>
a
>>
b
;
// 呼叫
printf
(
“%d + %d = %d”
,
a
,
b
,
add
(
a
,
b
));
return
0
;
}
最後執行一下就行!
至此,隱式呼叫完成!
顯式呼叫
應用程式必須在執行時進行函式呼叫以顯式載入 DLL。
在
test。cpp
檔案中手動載入動態庫。在需要使用的地方載入,使用完後釋放。
#include
#include
“。。/testDll/testdll。h”
#include
using
namespace
std
;
typedef
int
(
*
PADD
)(
int
a
,
int
b
);
// 定義所呼叫的匯出函式的呼叫指標
int
main
()
{
// 顯式呼叫
// 載入 DLL 檔案,獲取模組控制代碼。
HMODULE
hDLL
=
LoadLibrary
(
L
“testDll。dll”
);
if
(
hDLL
==
NULL
)
{
cout
<<
“載入 DLL 失敗!
\n
”
;
return
0
;
}
int
a
,
b
;
cout
<<
“input 2 integer:”
;
cin
>>
a
>>
b
;
PADD
pAdd
=
(
PADD
)
GetProcAddress
(
hDLL
,
“add”
);
// 獲取函式指標
// 顯性呼叫,在需要的時候使用。
printf
(
“%d + %d = %d”
,
a
,
b
,
pAdd
(
a
,
b
));
FreeLibrary
(
hDLL
);
// 使用完後釋放。
return
0
;
}
注意:呼叫
GetProcAddress
以獲取指向名為“DLLFunc1”的函式的指標,呼叫該函式並儲存結果。
此處如果按照之前的
testdll。h
內的宣告,則會改變引用的函式名,導致
PADD pAdd = (PADD)GetProcAddress(hDLL, “add”);
返回函式值時候引用函式名
“add”
出錯。
所以需要修改標頭檔案的宣告方式,讓編譯器使用C語言編譯,保留函式名。
匯出 C++ 函式以用於 C 語言可執行檔案
如果要從 C 語言模組訪問用 C++ 編寫的 DLL 中的函式,則應使用 C 連結(而不是 C++ 連結)宣告這些函式。增加
extern “C”
。
修改
testdll。h
當中的宣告:
#ifndef TESTDLL_H
#define TESTDLL_H
// 條件編譯指令
// 在預處理器裡事先定義好_DLLAPI,保證dll專案有預定義;
// 而新程式專案裡沒有,從而區分匯入和匯出。
#ifdef _DLLAPI
#define DLLAPI __declspec(dllexport)
// 匯出
#else
#define DLLAPI __declspec(dllimport)
// 匯入
#endif
// 宣告匯出函式
extern
“C”
DLLAPI
int
add
(
int
a
,
int
b
);
// 匯出add介面
// 使用C連結宣告函式,匯出時不會改變函式名。
// 使用C語言的方式進行編譯。
#endif
執行一下
至此,顯性呼叫完成!
使用模組定義檔案呼叫
如果不想使用 匯出 C++ 函式以用於 C 語言可執行檔案 的方式還原函式名,還可以另外新增一個
。def
(模組定義)檔案。
將標頭檔案
testdll。h
修改為:
#ifndef TESTDLL_H
#define TESTDLL_H
int
add
(
int
a
,
int
b
);
#endif
新增一個模組定義檔案:
在
Source。def
裡寫入:
LIBRARY
testDll
EXPORT
add
注意:這裡編譯出錯了,似乎是指標找不到函式名。上網查詢之後,可能是函式名已經被使用過了,所以換一個名字,並且加上
__stdcall
呼叫約定。
提示:就是把所有的
add
改名並且加上約定。
將標頭檔案
testdll。h
修改為:
#ifndef TESTDLL_H
#define TESTDLL_H
int
__stdcall
Add
(
int
a
,
int
b
);
#endif
將
testdll。cpp
修改為:
#include
“testdll。h”
int
__stdcall
Add
(
int
a
,
int
b
)
// 函式實現
{
return
a
+
b
;
}
將
test。cpp
修改為:
#include
#include
“。。/testDll/testdll。h”
#include
using
namespace
std
;
typedef
int
(
__stdcall
*
PADD
)(
int
a
,
int
b
);
// 定義所呼叫的匯出函式的呼叫簽名
int
main
()
{
// 顯式呼叫
// 載入 DLL 檔案,獲取模組控制代碼。
HMODULE
hDLL
=
LoadLibrary
(
L
“testDll。dll”
);
if
(
hDLL
==
NULL
)
{
cout
<<
“載入 DLL 失敗!
\n
”
;
return
0
;
}
int
a
,
b
;
cout
<<
“input 2 integer:”
;
cin
>>
a
>>
b
;
PADD
pAdd
=
(
PADD
)
GetProcAddress
(
hDLL
,
“Add”
);
// 獲取函式指標
// 顯性呼叫,在需要的時候使用。
printf
(
“%d + %d = %d”
,
a
,
b
,
pAdd
(
a
,
b
));
FreeLibrary
(
hDLL
);
// 使用完後釋放。
return
0
;
}
在
Source。def
裡寫入:
LIBRARY
testdll
EXPORTS
Add
執行一下:
至此,
。def
檔案呼叫完成!
演練
上面介紹的方法都是從0開始建立並使用的,但我面對的專案不是從0開始的,多了很多初始化檔案和模組。這裡加一個演練版本的動態庫操作例項。
建立 DLL 專案
建立新 DLL 專案。
起名,取消勾選“將解決方案和專案放在同一目錄下”,這樣建立的資料夾會整齊一點,這一步對結果沒有任何影響。
建立完後初始化介面如下,幫我們初始了一些檔案。
向 DLL 中新增檔案
新增標頭檔案和原始檔,起名如下:
接下來在標頭檔案
MathLibrary。h
中新增程式碼:
// MathLibrary。h - Contains declarations of math functions
#pragma once
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
// The Fibonacci recurrence relation describes a sequence F
// where F(n) is { n = 0, a
// { n = 1, b
// { n > 1, F(n-2) + F(n-1)
// for some initial integral values a and b。
// If the sequence is initialized F(0) = 1, F(1) = 1,
// then this relation produces the well-known Fibonacci
// sequence: 1, 1, 2, 3, 5, 8, 13, 21, 34, 。。。
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b。
// This function must be called before any other function。
extern
“C”
MATHLIBRARY_API
void
fibonacci_init
(
const
unsigned
long
long
a
,
const
unsigned
long
long
b
);
// Produce the next value in the sequence。
// Returns true on success and updates current value and index;
// false on overflow, leaves current value and index unchanged。
extern
“C”
MATHLIBRARY_API
bool
fibonacci_next
();
// Get the current value in the sequence。
extern
“C”
MATHLIBRARY_API
unsigned
long
long
fibonacci_current
();
// Get the position of the current value in the sequence。
extern
“C”
MATHLIBRARY_API
unsigned
fibonacci_index
();
注意:
標頭檔案用於宣告函式。
預處理語句用於定義介面,檢視 “屬性 - 預處理器 - 預處理定義“ 會發現新建專案時已經幫忙定義了。原因如 向 DLL 中新增檔案 裡
testdll。h
程式碼處註釋所說。
向原始檔
MathLibrary。cpp
裡寫入程式碼:
// MathLibrary。cpp : Defines the exported functions for the DLL。
#include
“pch。h” // use stdafx。h in Visual Studio 2017 and earlier
#include
#include
#include
“MathLibrary。h”
// DLL internal state variables:
static
unsigned
long
long
previous_
;
// Previous value, if any
static
unsigned
long
long
current_
;
// Current sequence value
static
unsigned
index_
;
// Current seq。 position
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b。
// This function must be called before any other function。
void
fibonacci_init
(
const
unsigned
long
long
a
,
const
unsigned
long
long
b
)
{
index_
=
0
;
current_
=
a
;
previous_
=
b
;
// see special case when initialized
}
// Produce the next value in the sequence。
// Returns true on success, false on overflow。
bool
fibonacci_next
()
{
// check to see if we‘d overflow result or position
if
((
ULLONG_MAX
-
previous_
<
current_
)
||
(
UINT_MAX
==
index_
))
{
return
false
;
}
// Special case when index == 0, just return b value
if
(
index_
>
0
)
{
// otherwise, calculate next sequence value
previous_
+=
current_
;
}
std
::
swap
(
current_
,
previous_
);
++
index_
;
return
true
;
}
// Get the current value in the sequence。
unsigned
long
long
fibonacci_current
()
{
return
current_
;
}
// Get the current index position in the sequence。
unsigned
fibonacci_index
()
{
return
index_
;
}
注意:該檔案用於實現函式。
到這裡可以編譯 DLL 檢視是否能成功生成。”生成 - 生成解決方案“。如果出現如下輸出,則說明一切正常。
至此,已經成功建立了一個 DLL !
建立使用 DLL 的客戶端應用
解決方案上右鍵,新新增控制檯專案。
新增一個原始檔。
在
MathClient。cpp
裡新增程式碼:
// MathClient。cpp : Client app for MathLibrary DLL。
// #include “pch。h” Uncomment for Visual Studio 2017 and earlier
#include
#include
“MathLibrary。h”
int
main
()
{
// Initialize a Fibonacci relation sequence。
fibonacci_init
(
1
,
1
);
// Write out the sequence values until overflow。
do
{
std
::
cout
<<
fibonacci_index
()
<<
“: ”
<<
fibonacci_current
()
<<
std
::
endl
;
}
while
(
fibonacci_next
());
// Report count of values written before overflow。
std
::
cout
<<
fibonacci_index
()
+
1
<<
“ Fibonacci sequence values fit in an ”
<<
“unsigned 64-bit integer。”
<<
std
::
endl
;
}
注意:此時還不能直接執行,因為缺少一些配置
首先是
MathLibrary。h
標頭檔案無法找到,也就是說此時專案沒有包括標頭檔案。
將 DLL 標頭新增到包含路徑
屬性頁面。所有配置。“C/C++” > “常規” 。將包含
MathLibrary。h
標頭檔案的路徑寫入。
注意:此時程式碼可以編譯,但還無法連結。
需要告訴連結器如何找到
。lib
檔案。
將 DLL 匯入庫新增到專案中
屬性。所有配置。 “連結器” > “輸入” > “附加依賴項”,寫入
MathLibrary。lib
。
“連結器” > “常規” > “附加庫目錄” ,寫入指向 MathLibrary。lib 檔案位置的路徑,可以從我們之前編譯 DLL 的輸出視窗得知路徑。
執行一下。
至此,已經成功呼叫 DLL 了!
參考資料
微軟文件-靜態庫
微軟文件-動態庫
建立純資源 DLL
將可執行檔案連結到 DLL
使用 DEF 檔案從 DLL 匯出
bilibili :操作流程