靜態鏈接庫編寫與使用
與靜態鏈接相比,動態鏈接更靈活,修改動態鏈接庫的代碼不需要重新編譯exe程序,如果說靜態鏈接庫是編譯階段的模塊化,那麼動態鏈接就是真正的運行時模塊化。
下面將演示使用VC6編寫動態鏈接庫(dll),然後在另一個程序中鏈接使用。
一、創建動態鏈接庫工程
選擇WIN32動態鏈接庫
simple DLL project
二、編寫代碼
創建頭文件和CPP文件,分別用於聲明和定義函數
MyDll.h
#if !defined(AFX_MYDLL_H__EF374ACC_12D5_44C8_A50E_2B61E8E3FADD__INCLUDED_)
#define AFX_MYDLL_H__EF374ACC_12D5_44C8_A50E_2B61E8E3FADD__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
extern "C" _declspec(dllexport) __cdecl int Plus(int x, int y);
#endif // !defined(AFX_MYDLL_H__EF374ACC_12D5_44C8_A50E_2B61E8E3FADD__INCLUDED_)
其中,extern “C” 表示以C語言方式導出,這樣就不會修改函數名,如果不加說明,則會以C++方式導出,C++爲了實現函數重載,會把修改函數名。
_declspec(dllexport) 表示這是導出函數,__cdecl C語言默認的調用約定,表示由調用者負責平衡參數堆棧,相對的,windows api 的調用約定一般是 __stdcall,表示由函數自己負責平衡堆棧。在這裏,我將演示兩種調用約定的區別,先用默認的 __cdecl.
MyDll.cpp
int __cdecl Plus(int x, int y)
{
return x + y;
}
編譯後生成若干文件,我們把 TestDll.dll 和 TestDll.lib 拿出來備用。
這裏說明一下 __cdecl 和 __stdcall 對導出函數名的影響,我們現在使用的是默認的 __cdecl,則函數名和源文件中一樣。使用dependency walker 工具打開dll,可以查看內部的函數名:
可以看到,函數名是Plus,和源文件中定義的一樣。現在我把調用約定修改成 __stdcall,觀察變化。
extern "C" _declspec(dllexport) __stdcall int Plus(int x, int y);
生成DLL,用dependency walker打開,發現函數名變成了 _Plus@8,其中8表示參數的大小,兩個int剛好是8.
如果增加一個int參數,變成3個參數求和:
extern "C" _declspec(dllexport) __stdcall int Plus(int x, int y, int z);
生成的函數名被修改成 _Plus@12 是顯而易見的。
弄清楚兩種調用約定的區別後,下一步就要研究怎麼在其他工程中鏈接DLL了。注意,現在我的DLL使用的調用約定是默認的 __cdecl。如果要使用__stdcall,則要注意聲明函數指針類型時也要指定調用約定爲__stdcall,以及函數名要相應地修改成_Plus@12(舉例)
下面將演示兩種鏈接方式。
三、將 lib 和 dll 複製到工程目錄
隱式鏈接需要用到 lib 和 dll,而顯式鏈接則只需要 dll。
四、隱式鏈接
代碼如下:
#include "stdafx.h"
#pragma comment(lib, "TestDll.lib")
extern "C" __declspec(dllimport) __cdecl int Plus(int x, int y, int z);
int main(int argc, char* argv[])
{
printf("%d\n", Plus(1,2,3));
return 0;
}
lib的作用是告訴編譯器函數在哪,真正的代碼在dll裏面。注意函數聲明和之前編寫dll的時候聲明的是一樣的。
五、顯式鏈接
調用LoadLibrary函數獲取dll模塊,再調用GetProcAddress獲取函數指針。
代碼如下:
#include "stdafx.h"
int main(int argc, char* argv[])
{
HINSTANCE hModule = LoadLibrary("TestDll.dll");
int (__cdecl *Plus)(int, int, int);
Plus = (int (__cdecl *)(int, int, int)) GetProcAddress(hModule, "Plus");
printf("%d\n", Plus(1,2,3));
return 0;
}
六、發佈
不管是隱式鏈接還是顯式鏈接,發佈程序時都要將 dll 和 exe 一同發佈,lib 只在編譯鏈接階段用到。
如果沒有dll,會報錯!
七、__stdcall
如果使用 __stdcall,則要注意導入函數時,函數名和dll工程的函數名是不一樣的,這個可以用 dependency walker 確認。
現在我將dll函數的調用約定修改爲 __stdcall
extern "C" _declspec(dllexport) __stdcall int Plus(int x, int y, int z);
隱式鏈接
由於隱式鏈接是通過 lib 來獲取函數位置的,所以可以使用和DLL中定義的相同的函數名。
// Helloworld.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#pragma comment(lib, "TestDll.lib")
extern "C" __declspec(dllimport) int __stdcall Plus(int, int, int);
int main(int argc, char* argv[])
{
printf("%d\n", Plus(1,2,3));
return 0;
}
顯式鏈接
注意修改了調用約定和函數名
// Helloworld.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
int main(int argc, char* argv[])
{
HINSTANCE hModule = LoadLibrary("TestDll.dll");
int (__stdcall *Plus)(int, int, int);
Plus = (int (__stdcall *)(int, int, int)) GetProcAddress(hModule, "_Plus@12");
printf("%d\n", Plus(1,2,3));
return 0;
}