Windows下靜態庫、動態庫的創建和調用過程


   靜態庫和動態庫的使用包括兩個方面,1是使用已有的庫(調用過程),2是編寫一個庫供別人使用(創建過程)。這里不講述過多的原理,只說明如何編寫,以及不正確編寫時會遇見的問題。

  //注:本文先從簡單到復雜,動態庫的部分先說明了靜態鏈接方式,比較簡單,若想看動態鏈接過程會遇到的問題可直接跳過。

          后面說明動態鏈接方式有關extern "C” 、名字改變、 __stdcall 的影響的問題。

1.靜態庫

1)創建過程

   在VS環境下創建一個 “Win32 Static Libarary” 工程StaticLib,添加頭文件lib.h和源文件lib.cpp

   

//----lib.h----------
int add(int a,int b);


//-----lib.cpp-------
#include "lib.h"

int add(int a,int b)
{
	return a+b;
}

  Build之后會發現Debug下生成了StaticLib.lib  靜態庫文件。   將lib.h和StaicLib.lib給別人,別人就可以使用庫中的函數add了。

 

2)調用過程

    新建一個簡單的控制台工程,只有一個StaticLibCall.cpp 。 將lib.h和StaticLib.lib放在同目錄下。

#include "lib.h"
#pragma comment(lib,"StaticLib.lib")

#include <iostream>
using namespace std;

int main()
{
	int t = add(2,3);
	cout<<t<<endl;
	getchar();
}

  編譯鏈接運行成功。    

     此處若在add(2,3)設置斷點,調試態 F11進入函數可以跳入到StaticLib.cpp中進行執行。可以知道靜態庫在調用過程中是會和源文件一起編譯鏈接的。

     其中  #pragma comment(lib,"StaticLib.lib") 是用來說明靜態庫調用,也可以在VS界面上設置:依次選擇tools、options、directories、library files添加。

  標准Turbo C2.0中的C庫函數(我們用來的scanf、printf、memcpy、strcpy等)就來自這種靜態庫。F11同樣可以進入對應的**.c文件中。

  

2. 動態庫

   動態庫比靜態庫的創建和調用都復雜。因為動態庫中的函數分為兩種,1種為內部函數,只供庫內部使用。 2種為導出函數,只有聲明為導出函數,才可以給別人使用。

   創建時聲明導出方式有2種:  1.__declspec(dllexport) 方式

                                        2.DEF文件方式

  使用時鏈接動態庫方式也有2種:  1.靜態鏈接方式 :同靜態庫的調用方式   #pragma comment(lib,"***.lib")

                                             2.動態鏈接方式:使用Win32系列函數:LoadLibrary(...)  GetProcAddress(...)  FreddLibrary(...)

  所以,組合起來有4種方式完成動態庫的 ”創建和使用“ 的過程。 由於不同交叉的方式中注意的問題不同,所以分別說明。

1)__declspec(dllexport) 方式導出方式,靜態方式鏈接

   A.創建:  在VS環境下創建一個 “Win32 DLL ” 工程AddDll,添加頭文件dll.h和源文件dll.cpp

//-----dll.h---------
__declspec(dllexport) int  add(int x, int y);


//-----dll.cpp---------
#include "dll.h"

__declspec(dllexport) int  add(int x, int y)
{
	return x + y;
}

  Build之后發現,Debug中生成了AddDll.lib和AddDll.dll文件。  將.h .lib .dll提供給別人,別人就可以使用動態庫中的add函數了。

 B. 靜態方式調用:

    新建一個控制台工程,只有一個AddDllCall.cpp 文件。 並且將 dll.h放在同目錄下,AddDll.lib  AddDll.dll 放在該工程的Debug下。

 注意:因為動態庫是在運行時才調用,所以必須放在運行時的目錄下,否則會找不到dll庫。

 

//---------AddDllCall.cpp----- 

#include <iostream>
using namespace std ;
#include "dll.h"
#pragma comment(lib,"AddDll.lib")


int _tmain(int argc, _TCHAR* argv[])
{
	int t = add(2,3);
	cout<<t<<endl;
	getchar();
	return 0;
}

  運行成功。

  注:有些人說,導出方式為__declspec(dllexport)時,調用時的函數聲明需要為__declspec(dllimport) add(int a,int b);這里測驗發現並沒有這個問題

  調用時#include ”dll.h" 只有一句話 int add(int a,int b);正常的函數聲明。

 

 2)DEF文件導出函數,靜態鏈接方式調用

 

   A.創建:  在VS環境下創建一個 “Win32 DLL ” 工程AddDll,添加頭文件dll.h和源文件dll.cpp ,並添加DEF文件:dll.def 文件

//-----dll.h---------
int  add(int x, int y);

//-----dll.cpp---------
#include "dll.h"

int  add(int x, int y)
{
	return x + y;
}

//----dll.def--------
LIBRARY BlogUse
EXPORTS add @ 1

  編譯鏈接生成AddDll.lib AddDll.dll . 將*.h *.lib *.dll提供給別人。

B. 靜態方式調用:

    新建一個控制台工程,只有一個AddDllCall.cpp 文件。 並且將 dll.h,AddDll.lib 放在同目錄下。  AddDll.dll 放在該工程的Debug下。

 注意:此處與上處不同,這里的 AddDll.lib 必須和.h一起放在AddDllCall.cpp的同目錄下,否則會找不到lib.或者顯示指定路徑。

  代碼同上,只是放了不同的這三個文件。

 

3)__declspec(dllexport) 方式導出函數,動態鏈接方式使用動態庫

A.創建: 同1)中一樣 在VS環境下創建一個 “Win32 DLL ” 工程AddDll,添加頭文件dll.h和源文件dll.cpp 。代碼也一樣

B.動態鏈接方式:

  新建一個控制台工程,只有一個AddDllCall.cpp 文件。 同樣將 lib 和 dll放在Debug下。代碼不多解釋,可以參看文章最后給出的鏈接。

#include <iostream>
using namespace std;
#include "dll.h"
//int add(int x, int y);

typedef int(*lpAddFun)(int, int); //宏定義函數指針類型
#pragma warning(disable:4996)

int _tmain(int argc, _TCHAR* argv[])
{
	HINSTANCE hDll; //DLL句柄 
	lpAddFun addFun; //函數指針
	hDll = LoadLibrary(TEXT("..//Debug//AddDll.dll")); 

	if (hDll != NULL)
	{
		addFun = (lpAddFun)GetProcAddress(hDll, "add");
		if (addFun != NULL)
		{
			int result = addFun(2, 3);
			cout<< result <<endl;
		}
		FreeLibrary(hDll);
	}
	getchar();
	return 0;
}

  運行,發現沒反應,沒有輸出想要的5。這里強調,調用方式沒有任何問題,聲明函數,再獲得函數地址,用指針調用,沒有任何問題。

     那問題出在哪里了呢???

  調試狀態發現addFun句柄返回了null,也就是 GetProcAddress(hDll, "add"); 沒有正確的函數。這是為什么呢?通過函數名“add"查找函數,有問題嗎?

     了解過這塊的人會發現,很多動態庫的導出函數 都有extern "C" __decl~~~ 這里extern "C" 是什么意思呢?是不是這個的原因呢?

     沒錯,問題就出在extern "C"上。  因為沒有加extern "C"之前,動態庫里的add函數根本不叫”add".

 

 回到創建的代碼,使用工具Depends.  ( 該工具找不到可以在命令行 cmd 下 輸入Depend.exe 就彈出來了。)

 發現導出的函數不叫"add",而是一大串 ?add@YAHHH@Z 。這是因為C語言和C++編譯方式不同造成的。

 所以GetProcAddress用 "add"去查找時,根本找不到函數。

 

將創建的代碼其他都不變,就把add函數的聲明和定義前,加上extern "C" 。Build之后再看 

導出的函數名變成了 “add". 使用這個lib和dll會發現GetProcAddress 正常執行了,也輸出了5.

      所以在用__declspec(dllexport)方式導出函數時,一般都添加extern ”C". 且給對方提供的頭文件中函數的聲明為import 

externa " C" __declspec(import)  int add(int a,int b);

 

  注意:extern "C"只解決了C和C++語方之間調用的問題,它只能用於導出全局函數這種情況而不能導出一個類的成員函數。

另外如果導出函數的調用約定發生改變,即使使用了extern "C",編譯后的函數名還是會發生改編。比如我們加入_stdcall關鍵字說明調用約定為C調用約定.

將創建當中的函數聲明和定義修改為如下:

extern "C"  __declspec(dllexport)  int __stdcall add(int x, int y)
{
	return x + y;
}

  查看發現,函數名還是改變了。

     關於__stdcall 和 __cdecl可以參看編譯原理的東西。windows下多用__stdcall的方式,CALLBACK,WINAPI看宏定義發現都是__stdcall的重定義。

   而C/c++語言默認__cdecl的方式。

   調用方式不同不僅影響函數名的變化,最主要影響函數棧和回收等問題。接下的DEF文件中我們也會看到。

 

4)DEF文件方式導出函數,動態方式調用

 A.創建   同2)中,提供 .h .lib .dll 

   B.使用  同3)中使用過程 

 

使用Depend工具查看創建的文件,發現,add依然叫add. 所以調用時用add函數名可以找到,正確執行。

但是當加上__stdcall調用方式后,發現,add依然叫add 。 

  但是提供給調用函數調用時發現,編譯鏈接通過,運行的時錯誤如下:

   調試發現,add函數正確執行了,並返回了5 ,但是main函數返回的時候,出現了如上運行錯誤。

  這是因為,向上面所說的,__stdcall等調用方式不僅影響函數名字,更重要的是影響 函數棧的調用方式和回收方式。具體參考,編譯原理 的 運行時刻環境 一章。

 

因此,兩種導出方式都無法解決__stdcall調用方式的問題。DLL文件與調用方必須是同一種方式,否則會函數不匹配 或者 函數棧出錯。

 

總結: 綜上對比,可以發現,靜態調用方式很簡單,不用考慮很多。因為靜態方式是由編譯器完成了一起編譯,因此使用就像使用內部函數一樣。

       

       動態方式是用系統APII來加載、卸載DLL以及獲取DLL中導出函數的地址,由程序決定加載和釋放的位置。

       動態調用方式必須使用LoadLibrary GetProcAddress函數用指針調用,而不能直接使用 add(2,3).

   

      DEF導出方式更簡單,不用寫太多,考慮太多。

 

另外,還有導出變量和導出類的內容。此處不詳說。

只說明,導出變量需注意,使用的時候,得到的是地址,使用內容必須 cout<< *(int*) g_value <<endl;

         導出類的時候,兩邊都需要有類的定義,創建方用__declspec(export)  調用方用 __declspec(import)  .

 

參考: http://blog.csdn.net/friday5pm/article/details/1532226

         http://blog.csdn.net/anye3000/article/details/7481481

//--------------呼,寫了好幾個小番茄啊,測了很多問題,應該還有一些問題沒有考慮全。希望再接再厲,總結自己的收貨,提高自己也幫助別人,開森!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM