静态库与动态库

一、介绍

      :库从本质上来说是一种代码重用的方式,即预先编译为可执行代码的二进制格式,可以被载入内存中执行。比如我们熟悉的c运行库,里面就实现了很多基本的函数,我们无需再自己写一遍,直接调用接口使用即可,库分静态库和动态库两种。

      静态函数库:这类库的名字一般是xxx.lib;利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。

      动态函数库:这类库的名字一般是xxx.dll(也可以包含xxx.lib用于编译时候的链接处理,也可以不包含,直接动态调用,后面会讲区别);相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

二、静态函数库

建立静态库工程

新建工程->选择WIN32项目->输入工程名testlib-》“win32应用程序向导”的“应用程序类型”选择选择静态库,勾选上“预编译头”

说明:勾选上预编译头会生成stdafx.h/stdafx.cpp,预编译头是为了能够让编译器更加快速的完成编译。

添加静态库类

菜单“项目”-》“添加类”-》类名称CTestLib

头文件

 1 #pragma once
 2 class CTestLib
 3 {
 4 public:
 5     CTestLib(void);
 6     ~CTestLib(void);
 7 
 8 public:
 9     int Add(int a, int b);
10 };
11 
12 //extern "C" int Sub(int a, int b); 或者
13 extern "C"
14 {
15     int Sub(int a, int b);
16 };

源文件

 1 #include "StdAfx.h"
 2 #include "TestLib.h"
 3 
 4 
 5 CTestLib::CTestLib(void)
 6 {
 7 }
 8 
 9 
10 CTestLib::~CTestLib(void)
11 {
12 }
13 
14 int CTestLib::Add(int a, int b)
15 {
16     return a + b;
17 }
18 
19 int Sub(int a, int b)
20 {
21     return a - b;
22 }

使用lib库
      有多种方式使用这样的Lib,如果我们在同一个解决方案来使用这个库,那么我们就可以在新建的工程里面设置依赖项目,这样系统会自动的为我们连接这个库文件,我们只需要包含一下提供静态库函数接口的.h文件即可。但是如果这个静态库是单独发布给其他人用的,那么我们可以设置静态库和.h的目录,添加到VC工程的目录配置,这样就可以和使用系统库一样的使用了。

2、3行 使用lib库。也可以右键点击testexe工程属性,打开属性页面板,选到链接器选项,点开链接器左边的加号,选到输入,附加依赖项里面我们输入TestLib.lib

 1 #include "stdafx.h"
 2 #include "TestLib.h"
 3 #pragma comment(lib, "testlib.lib")
 4 
 5 int _tmain(int argc, _TCHAR* argv[])
 6 {
 7     int a = 5, b = 2;
 8     CTestLib test;
 9 
10     printf("a + b = %d\n", test.Add(a, b));
11     printf("a - b = %d\n", Sub(a, b));
12 
13     return 0;
14 }

三、动态库

      相对于静态库,动态库的形式更灵活一些,大多情况下,我们编译动态库的时候,除了获得dll文件,依然会得到.lib文件,大多情况下,接口的定义还是需要通过.h来做。

建立动态库工程
      新建工程->选择WIN32项目->输入工程名TestDll-》“win32应用程序向导”的“应用程序类型”选择选择DLL。

DllMain

      动态库dll和静态库最大的区别就是,动态库是可以独立运行的文件,通俗一点讲,和可执行文件没有太大的本质区别,所以当其他可执行文件(exe或者其他dll)调用到该dll的时候,系统会执行一个入口函数,做一些初始化之类的工作,当然这个入口函数和可执行文件exe有一个最大的区别就是这个入口函数不是必须的,也就是说没有这个入口函数依然能编译dll。

DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是
DLL_PROCESS_ATTACH、
DLL_PROCESS_DETACH、
DLL_THREAD_ATTACH、
DLL_THREAD_DETACH。
以下从这四种情况来分析系统何时调用了DllMain。

DLL_PROCESS_ATTACH
      大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。
      当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数, 传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次 调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用 DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
DLL_PROCESS_DETACH
      当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
      那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
       1)FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
       2)进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结 是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL 在进程结束前没有机会执行任何清理工作。)
     注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。
DLL_THREAD_ATTACH
      当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
      新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
      注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
DLL_THREAD_DETACH
      如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
      注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
      大多数情况下,我们并不用去实现DllMain的细节,即使是要关心的,一般来说都只是DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、这两种方式,举个例子,如果做一个MFC相关的dll,我们可以在DLL_PROCESS_ATTACH的时候,去AfxInitExtensionModule初始化MFC的相关模块,DLL_PROCESS_DETACH的时候AfxTermExtensionModule 来注销相关模块,这些代码在建立MFC dll的工程可以看得到,所以大多情况下我们并不关心。
dllexport和dllimport

      右键对工程点击属性->配置属性->C/C++->预处理器,有一个我们关心的值:TESTDLL_EXPORTS,这个值是建立工程时候系统自动生成的,我们也可以改成自己喜欢的值,作为区分dll是导入还是导出的区别,我们先记录一下这个值。新建一个类CMyTestDll,
头文件:

 1 #pragma once
 2 
 3 #ifdef TESTDLL_EXPORTS
 4 #define MY_TEST_API __declspec(dllexport)
 5 #else
 6 #define MY_TEST_API __declspec(dllimport)
 7 #endif
 8 
 9 class MY_TEST_API CMyTestDll
10 {
11 public:
12     CMyTestDll(void);
13     ~CMyTestDll(void);
14 
15     int Add(int a, int b);
16 };
17 
18 extern "C"
19 {
20     MY_TEST_API int Sub(int a, int b);
21 };

源文件:

 1 #include "StdAfx.h"
 2 #include "MyTestDll.h"
 3 
 4 
 5 CMyTestDll::CMyTestDll(void)
 6 {
 7 }
 8 
 9 
10 CMyTestDll::~CMyTestDll(void)
11 {
12 }
13 
14 int CMyTestDll::Add(int a, int b)
15 {
16     return a + b;
17 }
18 
19 int Sub(int a, int b)
20 {
21     return a - b;
22 }
      TESTDLL_EXPORTS如是建立工程时候系统自定义的,可以改成我们喜欢的名字,不影响,只要对应得上工程所定义的即可。
      __declspec( dllexport )和__declspec( dllimport )的区别,由于dll是独立执行的动态库,对于我们要使用到的接口,都希望有导入(import)来使用,对于dll自身来说,这类接口是导出的(export),所以在dll工程,我们希望编译为export,在其他用到我们的动态库的工程(dll的使用者),希望得到import结果,所以很自然地,上面的定义做的就是这个事情。

动态库的使用

静态链接
      编译会生成一个testdll.dll, testdll.lib。类似静态库的例子,我们创建一个“控制台CanDelete”,所有的操作都一样,唯一的区别就是我们多了个dll,把dll放到编译出来的exe目录,不然会提示找不到dll。

 1 #include <iostream>
 2 #include "MyTestDll.h"
 3 using namespace std;
 4 
 5 #pragma comment(lib, "TestDll.lib")
 6 
 7 void main()
 8 {
 9     CMyTestDll test;
10     cout << test.Add(1, 3) << endl;
11 }

动态链接

      动态库和静态库的编译过程都差不多,静态链接的使用方式也大同小异,但是和静态库有所区别,动态库Dll还有另外一种动态链接的方式:LoadLibrary,LoadLibraryEx,功能都一样,我们默认使用LoadLibrary来讲一下,所谓的动态载入,就是程序运行时候不必一开始就载入dll,在程序运行过程中,在需要的时候,再动态加入进来,这样我们可以不再使用.lib来做静态链接,甚至可以连.h都不用了,直接用GetProcAddress来使用,但是这样的方式一般来说,都最好只用extern“C”开头的接口.我们可以通过Microsoft Visual Studio 8\Common7\Tools\Bin里面的一个工具Depends.Exe来看看我们刚才编译出来的dll有什么内容。

#include <iostream>
#include <Windows.h>
using namespace std;;

typedef int (*FUNC_SUB)(int a, int b);

void main()
{
	HMODULE bDll = ::LoadLibrary("TestDll.dll");
	if (!bDll)
	{
		cout << "load fail\n";
	}

	FUNC_SUB func = (FUNC_SUB)::GetProcAddress(bDll, "Sub");
	if(func)
	{
		cout << "ret = " << func(1, 2);
	}

	FreeLibrary(bDll);
}

  在大多情况下,我们一般都是使用.h + .lib + .dll的方式来开发,这样是一个最方便的方式,但是也有类似刚才说的我们使用LoadLibrary的方式来开发,一般作为什么情况才使用呢,比如我们要做一个程序,支持外部插件,这种时候就需要LoadLibrary这种方式了,我们只需要提供插件规范(插件必须要导出某些名字的函数),供主程序使用,这样对于插件开发者来说,只需要按照规范,写dll导出相关函数,就可以实现扩展程序功能的作用了。

四、区别

      有时候在CPP里面如果没实现int Sub( int a, int b )的内容一样能编译得到Testlib.lib,这是为什么呢?原因是这样的:静态库文件本身,他会把已经实现的代码都编译成二进制格式,因为静态库不具备独立执行的条件,所以最终必须要配合目标程序一起链接成可执行文件,在链接时候,如果发现Sub函数只做声明,没实现的话,会报Link error,这个时候你还可以发现如果在目标程序实现int Sub的功能,可以编译通过(同样的问题,比如在class CTestLib,有3个接口,比如叫interface_1, interface_2, interface_3,我们如果把interface_1和interface_2在静态库工程的CPP文件实现了,interface_3我们不管,依然可以编译成TestLib.lib,但是这个静态库,在配合目标程序使用的时候,会报错interface_3没实现,这样的情况下我们依然可以在目标程序实现CTestLib::interface_3的功能来通过编译,所以这个是和动态库最大的区别)。
 

posted on 2013-10-19 16:47  奎哥  阅读(385)  评论(0编辑  收藏  举报