代码改变世界

C++静态库与动态库

2015-07-19 09:46  rangers  阅读(12718)  评论(1编辑  收藏  举报

1、一个程序从源文件编译生成可执行文件的步骤:

预编译 -->  编译 -->  汇编 --> 链接

(1)预编译主要处理在源代码文件中以“#”开始的预编译指令,如宏展开、处理条件编译指令、处理#include指令等。

(2)编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

(3)汇编是将汇编代码转变成机器指令。

(4)链接主要是把分散的数据和代码收集并合成一个单一的可加载并可执行的的文件。链接可以发生在代码静态编译、程序被加载时以及程序执行时。

链接过程的主要工作是符号解析和重定位。

2、库 

库是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。而最常见的库就是运行时库(Runtime Library),如C运行库CRT.

库一般分为两种:

静态库(.a 、.lib)

动态库(.so 、.dll )

所谓静态、动态是指链接过程。 

3、静态链接与静态链接库

静态链接:源程序经过编译器生成目标文件,目标文件和库一起生成最终的可执行文件。链接的过程就是把分布在各个可重定位的目标文件中相应的节合并起来,同时完成符号解析和重定位。

静态库:一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。

静态库可以作为链接器的输入,如果程序需要引用静态库提供的某个函数,链接时只需要在命令行中输入该库。连接器只拷贝被程序引用的目标模块,以及该目标模块索依赖的那些目标模块。

特点:

(1)静态链接是在编译时期完成的

(2)静态链接形成可执行文件后,运行时与静态库再无关系,方便移植。

(3)浪费内存和磁盘空间。通过静态链接产生可执行程序时,会复制所依赖的静态库中所有数据和代码到该可执行程序中。

(4)更新困难。当程序所依赖的静态库有任何更新,整个程序就要重新链接。

Windows下创建与使用静态库:

以VS2010为例

编写一个简单的计算器类,编成静态库供其他程序使用,代码如下:

 

ifndef CALCULATOR_H_
#define CALCULATOR_H_

extern double g_max_number;

double GetMinNumber();

class Calculator
{
public:
	double Add(double a, double b) const;

	double Sub(double num1, double num2) const;

	double Mul(double num1, double num2) const;

	double Div(double num1, double num2) const;

};
#endif

cpp文件

#include "calculator.h"

double g_max_number = 999;

double Calculator::Add( double a, double b ) const
{
	return (a + b);
}

double Calculator::Sub( double num1, double num2 ) const
{
	return (num1 - num2);
}

double Calculator::Mul( double num1, double num2 ) const
{
	return (num1 * num2);
}

double Calculator::Div( double num1, double num2 ) const
{
	if (num2 != 0)
	{
		return (num1 / num2);
	}
	return 0;
}

double GetMinNumber()
{
	return (-999);
}

 

在创建VS工程时,在设置工程属性时,配置类型选择静态库即可,build即可生成静态库。

 

使用静态库:

需要.h .lib文件

(1)添加头文件包含目录

(2)设置所依赖的库目录

(3)添加所依赖的Lib文件,在Additional Dependencies中输入库名称即可。

注:直接在源代码中加入代码 #pragma comment(lib,"mylib.lib") 也可以。

设置完这三步后,就可以正常使用静态库中的类、函数、变量等。

 

 4、动态链接与动态库

 引入动态库的原因:

如上所述静态库有两个重大缺点:

1)空间浪费

2)静态链接对程序的更新、部署和发布会带来很多麻烦。一旦程序中有任何模块更新,整个程序就要重新链接,发布给用户。

动态链接的基本思想:把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是想静态链接一样把所有的程序模块都链接成一个单独的可执行文件。

特点:

1)代码共享,所有引用该动态库的可执行目标文件共享一份相同的代码与数据。

2)程序升级方便,应用程序不需要重新链接新版本的动态库来升级,理论上只要简单地将旧的目标文件覆盖掉。

3)在运行时可以动态地选择加载各种应用程序模块

下面重点介绍Windows下动态链接库DLL.

DLL即动态链接库(Dynamic-Link Libaray)的缩写,相当于Linux下的共享对象。Windows系统中大量采用了DLL机制,甚至内核的结构很大程度依赖与DLL机制。Windows下的DLL文件和EXE文件实际上是一个概念,都是PE格式的二进制文件。

为了更好的理解DLL,首先介绍一下导出和导入的概念。

(1)导出与导入

在ELF(Linux下动态库的格式),共享库中所有的全局函数和变量在默认情况下都可以被其他模块使用,即ELF默认导出所有的全局符号。DLL不同,需要显式地“告诉”编译器需要导出某个符号,否则编译器默认所有的符号都不导出。

程序使用DLL的过程其实是引用DLL中导出函数和符号的过程,即导入过程。对于从其他DLL导入的符号,需要使用“__declspec(dllimport)”显式声明某个符号为导入符号。在ELF中,使用外部符号时,不需要额外声明该符号是从其他共享对象导入的。

指定符号的导入导出一般有如下两种方法:

1)MSVC编译器提供了一系列C/C++的扩展来指定符号的导入导出,即__declspec属性关键字。

__declspec(dllexport) 表示该符号是从本DLL导出的符号

__declspec(dllimport) 表示该符号是从别的DLL中导入的

2)使用“.def”文件来声明导入到导出符号,详细参考《程序员的自我修养--链接、装载与库》。

(2)创建动态库

还是上面的那个简单计算器程序的例子,创建一个动态库供其他程序使用。

建立一个VS Win32工程,在创建向导中选择DLL ,如下:

可以看到生成了一个dllmain.cpp文件,里面有一个DllMain函数,是DLL的入口函数,一般不需要做修改。入口函数如下:

 

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

 

添加自己的代码CalculatorDll.h

#ifdef CALCULATORDLL_EXPORTS
#define CALCULATORDLL_API __declspec(dllexport)
#else
#define CALCULATORDLL_API __declspec(dllimport)
#endif

// This class is exported from the CalculatorDll.dll
class CALCULATORDLL_API CCalculatorDll {
public:
	CCalculatorDll(void);
	// TODO: add your methods here.
	double Add(double a, double b) const;

	double Sub(double num1, double num2) const;

	double Mul(double num1, double num2) const;

	double Div(double num1, double num2) const;
};

extern CALCULATORDLL_API int nCalculatorDll;

CALCULATORDLL_API int fnCalculatorDll(void);

 

cpp文件

#include "CalculatorDll.h"


// This is an example of an exported variable
CALCULATORDLL_API int nCalculatorDll=0;

// This is an example of an exported function.
CALCULATORDLL_API int fnCalculatorDll(void)
{
	return 42;
}

// This is the constructor of a class that has been exported.
// see CalculatorDll.h for the class definition
CCalculatorDll::CCalculatorDll()
{
	return;
}

double CCalculatorDll::Add( double a, double b ) const
{
	return (a + b);
}

double CCalculatorDll::Sub( double num1, double num2 ) const
{
	return (num1 - num2);
}

double CCalculatorDll::Mul( double num1, double num2 ) const
{
	return (num1 * num2);
}

double CCalculatorDll::Div( double num1, double num2 ) const
{
	if (num2 != 0)
	{
		return (num1 / num2);
	}
	return 0;
}

 

1)在这里使用__declspec扩展来声明的类、函数和变量的导入导出。

#ifdef CALCULATORDLL_EXPORTS
#define CALCULATORDLL_API __declspec(dllexport)
#else
#define CALCULATORDLL_API __declspec(dllimport)
#endif

在DLL工程中,默认定义了宏CALCULATORDLL_EXPORTS ,CALCULATORDLL_API 为__declspec(dllexport),用来声明导出。

在使用该DLL的其他工程中,没有定义CALCULATORDLL_EXPORTS宏,CALCULATORDLL_API为 __declspec(dllimport),用来声明导入。

2)动态链接库的生成文件

动态链接一般会生成两个文件 .dll  和 .lib

.dll : 动态链接库

.lib: 导入库

静态链接的.lib  : 一组目标文件的集合,包含代码和数据。

动态链接的.lib : 并不包含代码与数据,它包含了.dll中的导出符号以及一部分桩代码。

该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供。

一句话总结:.lib是编译链接时使用的,dll是运行时使用的。

(3)使用动态库

1)隐式的调用

隐式的调用是指可执行程序的源代码通过函数名直接调用DLL的导出函数,调用方法和调用程序内部的其他函数是一样的。

隐式调用须用到 .h .lib(编译时)   .dll(运行时)

当建立DLL时,会自动生成一个与之对对应的lib导入库。该库文件包含了DLL的导出函数的符号名和可选的序列号,但不包含实际的代码。在构建可执行应用时,需要把该导入库与构建可执行程序的目标文件进行静态链接。通过链接该导入库,链接器可以解析可执行程序的所有外部符号的引用,并且把相应的DLL文件名(不是完整路径)存储在可执行应用文件内部。

隐式调用时,被应用程序使用的DLL都会在可执行应用EXE被加载时加载到内存中。

隐式链接的使用方式与静态库的使用类似:

a)添加头文件包含目录

b)设置导入库目录

c)添加导入库Lib文件或在程序中使用 #pragma comment(lib,"CalculatorDll.lib")

 

2)显式调用

在可执行应用运行时,显示加载需要的DLL并且显式连接到需要的输出符号。

Windows提供了3各API来完成运行时加载:

LoadLibrary(LoadLibraryEx) :装载一个DLL到进程的地址空间,返回HINSTANCE值用于标识DLL文件映像映射到的虚拟地址,如果加载失败返回NULL.

GetProcAddress : 用来查找某个符号的地址,该函数将符号名或序列号转换为DLL内部的地址。

FreeLibrary: 用来卸载已加载的DLL.

显示调用一般只需要使用.dll即可。

显式调用DLL示例:

显式加载CalculatorDll.dll,并调用fnCalculatorDll函数。

注意:显式调用DLL时,fnCalculatorDll要声明为

extern "C" CALCULATORDLL_API int fnCalculatorDll(void);

//加extern "C",是为了使C++的名称修饰机制将不起作用,保证编译时生成的函数名不变,这样动态调用dll时才能正确获取函数的地址

如下:

#ifdef CALCULATORDLL_EXPORTS
#define CALCULATORDLL_API __declspec(dllexport)
#else
#define CALCULATORDLL_API __declspec(dllimport)
#endif

// This class is exported from the CalculatorDll.dll
class CALCULATORDLL_API CCalculatorDll {
public:
	CCalculatorDll(void);
	// TODO: add your methods here.
	double Add(double a, double b) const;

	double Sub(double num1, double num2) const;

	double Mul(double num1, double num2) const;

	double Div(double num1, double num2) const;
};

extern CALCULATORDLL_API int nCalculatorDll;

//加extern "C",是为了使C++的名称修饰机制将不起作用,保证编译时生成的函数名不变,这样动态调用dll时才能正确获取函数的地址
extern "C" CALCULATORDLL_API int fnCalculatorDll(void);

 

显示加载Dll:

typedef int (*DLLFunc)(void); 
int _tmain(int argc, _TCHAR* argv[])
{
       //加载DLL
	HINSTANCE dllHandle = LoadLibrary(_T("CalculatorDll.dll"));
	if (NULL == dllHandle)
	{
		cout << "Load dll failed!" << endl;
		return;
	}
	//根据符号名得到函数地址
	DLLFunc f = (DLLFunc)GetProcAddress(dllHandle,"fnCalculatorDll");
	if (NULL == f)
	{
		cout << "Unable find dll function!" << endl;
	}
	else
	{
		cout << "Result:" << f() << endl;
	}

	//卸载DLL
	FreeLibrary(dllHandle);
        	
	return 0;
}    

 

3)显式调用C++动态库

一个DLL可以导出C++类,声明导出类很简单在该类前加上__declspec(dllexport),使用导出类的程序须声明该类为__declspec(dllimport).

隐式调用很简单,包含头文件、设置导入库。然后跟使用普通类一样就可以了。

通过LoadLibrary显式调用函数,如上所述方法很简单。通过GetProcAddress获得函数的在DLL的地址就可以访问了。但DLL中的Class访问就相对很复杂了。

在目前的项目中常用的一种做法是:

继承接口基类,将需要在外部使用的类成员函数在基类中声明为虚,子类实现这些函数,通过导出函数返回基类指针。利用运行时多态就可以通过该指针来访问这些函数。

该方法的具体原理了解的不是很透彻,猜想应该是通过虚表来访问成员函数,而不是直接通过函数符号,所以不会出现链接错误。

实现如下:

创建一个DLL:

 

//接口
class ICalculator
{
public:
	ICalculator()
	{
		m_Number = 1;
	}
	virtual ~ICalculator()
	{

	}
	virtual double Add(double a, double b) const = 0;
	virtual double Sub(double num1,double num2) const = 0;
	virtual double Mul(double num1, double num2) const = 0;
	virtual double Div(double num1, double num2) const = 0;

	double GetNumber()
	{
		return 99999;
	}
	int m_Number;
};

//具体实现类
class  Calculator2 : public ICalculator
{
public:
	double Add(double a, double b) const;

	double Sub(double num1, double num2) const;

	double Mul(double num1, double num2) const;

	double Div(double num1, double num2) const;
};

//全局函数 返回基类指针
extern "C" MYDLL_API ICalculator * CreateCalculator();

extern "C" MYDLL_API void DestroyCalculator(ICalculator * &p);

 

cpp文件:

#include "calculator2.h"

extern "C" MYDLL_API ICalculator * CreateCalculator()
{
	return new Calculator2();
}

extern "C" MYDLL_API void DestroyCalculator( ICalculator * &p )
{
	delete p;
	p = NULL;
}

double Calculator2::Add( double a, double b ) const
{
	return (a + b);
}

double Calculator2::Sub( double num1, double num2 ) const
{
	return (num1 - num2);
}

double Calculator2::Mul( double num1, double num2 ) const
{
	return (num1 * num2);
}

double Calculator2::Div( double num1, double num2 ) const
{
	if (num2 != 0)
	{
		return (num1 / num2);
	}
	return 0;
}

显式调用:

typedef ICalculator * (*CreateFunc)(void);
typedef void (*FreeFunc)(ICalculator * &p);

void LoadDllTest()
{
	HINSTANCE dllHandle = LoadLibrary(_T("mydll.dll"));
	if (NULL == dllHandle)
	{
		cout << "Load dll failed!" << endl;
		return;
	}

	CreateFunc cfunc = (CreateFunc)GetProcAddress(dllHandle,"CreateCalculator");
	FreeFunc freeFunc = (FreeFunc)GetProcAddress(dllHandle,"DestroyCalculator");
	if (cfunc == NULL || freeFunc == NULL)
	{
		cout << "Unable find dll function CreateCalculator!" << endl;
	}
	else
	{
		ICalculator * pCalculator = cfunc();
		cout << "Add:" << pCalculator->Add(199,1) << endl;
		cout << "Sub:" << pCalculator->Sub(1,2) << endl;
		cout << "Not Virtaul Method:" << pCalculator->GetNumber() << endl;
		cout << "Var:" << pCalculator->m_Number << endl; 
		freeFunc(pCalculator);

		if (NULL == pCalculator)
		{
			cout << "already free"  << endl;
		}
	}
	FreeLibrary(dllHandle);
}
  

最后附上Windows定位DLL的搜索顺序:

1.包含EXE文件的目录
2.进程的当前工作目录
3.Windows系统目录
4.Windows目录
5.列在Path环境变量中的一系列目录