程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> c++動態庫生成與調用

c++動態庫生成與調用

編輯:關於C++

一、生成動態庫(含頭文件、不含頭文件)

以生成dllTest.dll為例(工程名為dllTest、 頭文件名為dllTest.h、 源文件名為dllTest.cpp)

1.1 不含頭文件的動態庫

我們生成的動態庫想要被別人調用,那麼一定要將想要被調用的函數導出,使用_declspec(dllexport)進行導出。
//dllTest.cpp
_declspec(dllexport) int add(int a, int b)
{
	return a+b;
}

_declspec(dllexport) int sub(int a, int b)
{
	return a-b;
}

編譯之後,在工程目錄的Debug目錄下我們可以看到以下的幾個文件,分別是 \ 得到上述的文件之後,我們就可以進行調用該動態鏈接庫了。其中,dll文件是包含了函數具體實現的可執行文件;lib文件是導入庫文件,主要包含的是函數名和符號名。我們可以用vs提供的dumpbin工具查看生成的動態庫中導出的函數以及名字。我的vs安裝在C:\Program Files (x86)\Microsoft Visual Studio 10.0,進入該文件夾後點擊VC進入該目錄,可以看到一個vcvarsall.bat的文件。依次點擊Microsoft Visual Studio 2010 -> visual studio tools ->Visual Studio Prompt 2010,如下圖所示:\ 即可打開一個vs的命令框,將vcvarsall.bat文件拖拽進入該命令框中,將得到下圖所示的內容:\   然後我們將得到的dllTest.dll文件放入到VC文件夾中,輸入dumpbin -exports dllTest.dll即可看到該動態庫導出的函數以及經過c++編譯器修飾後的函數名字。\   橢圓所示的即為導出的函數以及其經過修飾後的名字。(因為c++要支持函數重載以及命名空間等,因此需要將函數進行修飾)。

1.2 動態庫調用(隱式連接、動態連接)

1.2.1 隱式連接

首先建立一個工程,配置工程屬性,具體的步驟如下: (1)新建一個win32控制台應用程序,工程名dllCall; (2)配置工程屬性,添加對動態庫dllTest.lib的引用: a. 工程項目右鍵->屬性->鏈接器->gengeral->附加庫目錄,在其中添加導入庫dllTest.lib所在的文件目錄,如下圖: \ b.工程項目右鍵->屬性->鏈接器->輸入->附加依賴項,在其中添加導入庫dllTest.lib,如下圖: \   (3)此時,我們就可以在我們的工程中對該動態庫進行調用了。   調用函數dllCal.cppl如下:

// dllCall.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include  using namespace std; extern int add(int, int ); //告訴編譯器,add函數是在該源文件外部定義的函數 _declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數 //這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些 int _tmain(int argc, _TCHAR* argv[]) { int a = 5, b = 3; cout< (4)編寫好之後,編譯、鏈接都能通過,但是運行的時候會出錯,因為調用函數不知道int add(int, int ) 以及int sub(int, int)的可執行文件(dllTest.dll)在哪,因此當調用該函數時,就找不到具體的執行的代碼,因而就會報錯。此時,我們只需將dllTest.dll放入到調用函數的.exe文件所在的目錄中即可,結果如下圖:
\ 上述的兩種形式都能正常的調用。

1.2.2 動態調用

上述的隱式調用需要我們在工程屬性中配置一些動態庫導入庫(dllTest.lib)的目錄以及名稱,會很麻煩,有時候我們也會忘記配置這些屬性或者當動態庫較多的時候有遺漏,都會導致函數鏈接的時候出現unresolve external symbol的錯誤。而且,動態調用還有一個優點就是,什麼時候需要調用動態庫的函數的時候什麼時候加載該動態庫,這樣就不必在程序運行開始時加載所需的所有的動態庫,這樣也能加快啟動的速度。此外,我們僅僅只需要一個dllTest.dll文件即可。 (1)我們首先刪掉工程屬性中 “附加庫目錄”以及“附加依賴項”中我們輸入的內容。 (2)修改相應的調用代碼,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#include
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	HINSTANCE hInst = LoadLibraryA("dllTest.dll");
	typedef int (*pFun)(int, int);//定義一個函數指針類型pAdd
	pFun add = (pFun)GetProcAddress(hInst,"?add@@YAHHH@Z");
	cout<

	源文件:

//dllTest.cpp #define DLLTEST_API _declspec(dllexport) #include "dllTest.h" DLLTEST_API int add(int a, int b) { return a+b; } DLLTEST_API int sub(int a, int b) { return a-b; }



2.1 函數調用(隱式鏈接、動態鏈接)

2.1.1 隱式鏈接

(1) 新建工程,配置工程屬性(添加導入庫目錄、導入庫),具體的參照上面的隱式調用。此外,還要將動態庫頭文件所在的目錄加入到屬性中: 在 c\c++ -> gengeral -> additional include Directories 加入dllTest.h所在的目錄。 如下圖: \ (2)修改調用函數,具體的代碼如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#include "dllTest.h"
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#include "dllTest.h"
#pragma comment(lib,"dllTest.lib")
using namespace std;

//extern int add(int, int );                             //告訴編譯器,add函數是在該源文件外部定義的函數
//_declspec(dllimport) int sub(int, int);//告訴編譯器,sub函數是從動態庫導入的函數
//這兩種方式都可以正常的調用,但是下面的相對來說加載的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<3.1 extern "C"

3.1.1 默認調用方式_cedel

以含頭文件的動態庫為例進行修改,修改後的頭文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);

 
//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

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

DLLTEST_API int sub(int a, int b)
{
	return a-b;
}

進行編譯得到相應的.dll、.lib文件,通過dumpbin工具,我們可以查看導出函數的名字,如下:
\ 紅色部分為 沒有加extern "C" 導出的函數名,它經過了c++編譯器的修飾。 綠色部分為添加了extern "C" 導出的函數名,它的意思是告訴編譯器,以C的方式導出函數。

3.1.2 更改調用方式 _stdcall

但是如果改變了調用方式,通過這種方式進行導出,函數的名字依舊會改變,我們采用_stdcall調用方式,也就是

WINAPI,後面講為什麼window API 函數都采用該種調用方式:

(1)編寫_stdcall調用方式的動態庫(以含頭文件方式為例) 相應的頭文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else 
#define DLLTEST_API extern "C" _declspec(dllimport) 
#endif

DLLTEST_API int _stdcall add(int, int);
DLLTEST_API int _stdcall sub(int, int);

//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"

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

DLLTEST_API int _stdcall sub(int a, int b)
{
	return a-b;
}
編譯後利用dumpbin工具查看導出函數的名字如下: \
紅色的為采用c\c++默認調用方式導出的函數名字; 綠色的為采用_stdcall 調用方式導出的函數名字。至於為什麼會是這樣的名字在後面的名字修飾規則中進行說明。我們發現他的名字還是改變了。

3.2 模塊定義文件.def

3.2.1 默認調用方式(_cedel)動態庫

(1)新建一個Win32程序,選擇一個動態庫程序,勾選空工程。 (2)修改相應的代碼(源文件、模塊定義文件),具體的如下: 源文件:
//dllTest.cpp
 int  add(int a, int b)
{
	return a+b;
}

int  sub(int a, int b)
{
	return a-b;
}

模塊定義文件:
LIBRARY dllTest

EXPORTS
add
sub
其中,LIBRARY 後面要與生成的動態庫的名稱相同,EXPORTS下面寫需要導出的函數(add、sub),它會自動與你的源文件中的函數進行匹配。也可以用 add1 = add、 sub1 = sub 這樣的方式來改變導出函數的名字。
(3)通過dumpbin工具查看動態庫的導出函數,如下圖(分別是沒改名字以及改名字後的): \     \  

3.2.2 采用_stdcall,也就是WINAPI調用方式生成動態庫

(1) 源文件
//dllTest.cpp
 int  _stdcall add(int a, int b)
{
	return a+b;
}

int  _stdcall sub(int a, int b)
{
	return a-b;
}

(2)模塊定義文件
LIBRARY dllTest

EXPORTS
add
sub
(3)利用dumpbin查詢動態庫導出的函數
\

3.3 動態庫調用

3.3.1 動態調用

動態調用同上,只是變了一個地方,如下:
	pFun add = (pFun)GetProcAddress(hInst,"add");
	pFun sub = (pFun)GetProcAddress(hInst,"sub");
也可以使用每個函數前面的編號,如add函數的編號為1 、sub函數的編號為2。只需要將上述函數的第二個參數改為 MAKEINTATOM(1)。

3.3.2 隱式調用

<1> 動態庫與函數庫都采用_cedel調用方式 (1) 配置工程屬性,加入附加庫目錄;(附加依賴項使用#pragma comment(lib,"testdll.lib"代替) (2)調用函數如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<(2)編寫調用函數,如下:
// dllCall.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include 
#pragma comment(lib,"testdll.lib")
using namespace std;


_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
	int a = 5, b = 3;
	cout<
這是因為_stdcall是被調用的函數自己清理棧空間,而_cedel則是調用者清理棧。上述的調用方式會使棧得到兩次清理,使得函數的返回地址、ebp寄存器的值被更改從而導致失敗。通過觀察其生成的匯編語言就可以看到(下面的主要的,並不是全部): \ main函數在調用add函數之後清理的棧,而在add函數中,可以清楚的看到,add函數在運行完之後自己清理的棧。 \   因此,導致棧數據出現錯誤。\
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved