目录
0. 库技术简介
1. DLL用例示范
2. DLL的加载、卸载与多进程的数据段
3. DLL中的动态内存分配
4.为DLL添加头文件
5.在DLL中导出一个类
6. 静态库的编译
7. 动态库的手工加载
8. VC项目的静态编译
0. 库技术简介
在C/C++中可以将一种编译好的符号提供给第三方来使用,而源码是对用户不可见的,就是“库技术”。简单的来说就是“公开了技术,隐藏了代码”。库技术分为两种:动态库(Dynamic-Link Library, DLL)和静态库(Static Library)。
1. DLL用例示范
1> 创建一个简单的.dll程序
VS2015新建项目——windows控制台程序——点击dll创建完成。生成的项目中自动包含了两个头文件和三个cpp文件。
stdafx.h
targetver.h
dllmain.cpp
LibraryExercise.cpp //工程名
stdafx.cpp
检查三个cpp文件,发现dllmain.cpp中的dllMain作为项目的dll主函数,是系统运行最先进入的函数;stdafx.cpp与预编译信息有关;我们需要导出的核心算法在LibraryExercise.cpp中实现。打开LibraryExercise.cpp文件,输入以下代码:
// LibraryExercise.cpp : 定义 DLL 应用程序的导出函数。
#include "stdafx.h"
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
需要注意的是,如果需要导出一个全局函数,就需要在其前用关键字来声明:_declspec(dllexport),这是一个windows的函数,在Linux平台下不可用。最后还需要在项目属性中设置以下三个步骤:
取消“预编译头文件”:配置属性——C/C++——预编译头——创建/使用预编译头——不适用预编译头。 改为/Mtd编译:配置属性——C/C++——代码生成——运行时库——多线程调试(/Mtd)。 修改输出的dll名字:配置属性——链接器——输出文件 ——改为Add.dll。
编译后即可在当前目录下找到Add.dll以及Add.lib文件。下面解释Add.dll和Add.lib的区别:
.dll文件包含所有编译出的二进制指令
.lib文件包含一个列表,表明相对应的.dll文件中有哪些符号,每个符号对应在dll中的位置。
因此dll文件要比lib文件大得多。
2> 在应用程序中使用动态库
在新创建的项目中创建main.cpp,并在其中添加如下代码:
#include <stdio.h>
//使用库
#pragma comment(lib,"Add.lib")
//声明:此函数需从dll导入
_declspec(dllimport) int Add(int a, int b;)
int main(int argc, char** argv)
{
int result = Add(2, 3);
printf("Result is %d \n", result);
return 1;
}
同时将Add.dll和Add.lib放在规定目录下,否则找不到。强调:导入和导出分别使用命令:_declspec(dllimport)和_declspec(dllexport),需加以区分。dll文件需放在以下五个目录中的任意一个即可被操作系统找到:
-
可执行文件所在目录;
-
进程当前目录;
-
系统目录,如C:\Windows\System32等;
-
Windows目录,如C:\Windows;
-
环境变量(系统变量)PATH中的目录。(注:环境变量修改后重启生效!)
2. DLL的加载、卸载与多进程的数据段
需要明确的是,dll文件并不能独立运行,只有当可执行文件.exe加载它时,他才能运行。在exe中有一些标志性信息,表明了此exe文件需要依赖那些dll文件,操作系统即在上述五个目录开始寻找dll文件。exe被加载到内存,形成一个进程(process),dll也被加载到内存;此时进程可以直接调用dll里的函数。因此一个可执行文件实体可以有多个进程,那么我们就来考虑一个问题,当多个进程同时调用一个dll时,dll的全局数据段是共享的吗?考虑一下代码:
//export.cpp
#include <stdio.h>
static int value = 0;
_declspec(dllexport) void ValueSet(int a)
{
value = a;
}
_declspec(dlliexport) int ValueGet()
{
return value;
}
//import.cpp
#include<stdio.h>
#pragma comment(lib, "my.lib")
_declspec(dllimport) int ValueGet();
_declspec(dllimport) void ValueGet(int a);
int main()
{
int a = ValueGet();
printf("Value : %d", a);
ValueSet(120);
int a = ValueGet();
printf("Value : %d", a);
printf("&Value : %f", &Value);
getchar();
return 1;
}
从上述代码可以看出,value作为dll中的一个静态全局变量,在第一次运行import.exe退出进程前值被置为120,那么问题在于,当地一个进程不退出,再次运行import.exe时,输出的value是0还是120呢?答案是0!这说明一个问题:进程之间调用dll并没有影响。原因在于当dll被加载时,代码段被加载一次,是公共的;而数据段各自程序拷贝一次,是私有的。
注意事项:
-
当有exe运行时,dll被加载,文件处于被占用状态,不能删除,移动,直到它被卸载。
-
当所有进程都退出时,dll被卸载。
-
不同进程之间不能比较dll中的变量的地址,如上import.cpp中每次运行第三个printf输出结果都是相同的。(虽然数据段是私有的,但是地址却是相同的,原因在于采用了虚拟地址)
3. DLL中的动态内存分配
在dll中的malloc的内存,必须在dll中free。(这是由windows决定的)可看以下实例:
//export.cpp
#include <stdio.h>
#define MYDLL _declspec(export)
MYDLL int* MyAlloc(int size)
{
int* p = malloc(size * sizeof(int));
for(int i = 0; i < size; i++)
{
p[i] = i;
}
return p;
}
MYDLL void MyFree(int* p)
{
free(p);
}
//import.cpp
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "my.lib")
#define MyDLL _declspec(import)
MyDLL int* MyAlloc(int size);
MyDLL void MyFree(int* p);
int main(int argc, char** argv)
{
int* p = MyAlloc(4);
free(p); //在dll内分配的动态内存在dll外释放会报错
return 1;
}
正确的做法应该是将free(p)改成MyFree(p).
4.为DLL添加头文件
如果用户利用以上方法对dll中的函数进行声明,不仅给用户带来很多麻烦,而且用户要对dll中有哪些函数非常清楚,这显然是不可能做到的,因此按照惯例,应由模块的作者提供头文件,头文件中应有类型和函数声明。
1> 为DLL创建头文件
//mydll.h
//这个.h文件在编译生成APP的项目和用户实际使用的项目中是通用的,其起作用的方式是条件编译。
#ifndef _MYDLL_H_
#define _MTDLL_H_
#ifdef MYDLL_EXPORT
#define MYDLL _declspec(dllexport) //在编译dll工程的.cpp文件里,定义宏MYDLL_EXPORT
#else
#define MYDLL _declspec(dllimport) //在用户工程的.cpp文件里,什么都不用定义就行了
#endif
#include <stdio.h>
MYDLL int Add (int a, int b);
#endif
//mydll.cpp
#define MYDLL_EXPORT //预编译规定“输出选项”,用作编译dll工程里的cpp文件
#include <mydll.h>
int Add(int a, int b)
{
return a + b;
}
生成dll、lib文件后,将dll、lib和h文件一并拷贝至目标工程文件夹,目标工程cpp调用dll文件中函数如下,用户根据头文件即可知道dll中函数的相关信息,同时也省去了导入函数的麻烦。
#include <stdio.h>
#include <mydll.h>
//此处不需任何其他宏定义,h文件中MYDLL自动被编译为_declspec(import)
#pragma comment(lib, "mydll.lib")
int main(int argc, char** argv)
{
int result = Add(10, 11);
printf("Result : %d", result);
return 1;
}
2> DLL相关文件的部署
可将dll的相关文件部署到指定文件夹,并将头文件作为系统文件导入。创建mydll文件夹,包含子文件夹include与bin,其中include存放头文件,bin里存放dll与lib文件。这样就可以用尖括号包含了。
#include <mydll.h> //C++中尖括号表示系统文件,双引号表示用户自己的头文件
3> 用户DLL的配置
与OpenCV的配置相似,在VC++目录中配置包含目录——添加include,库目录——添加bin,同时将dll文件部署到系统能找到的5个目录之一即可(OpenCV的选项是添加到环境变量中)。
5.在DLL中导出一个类
导出类的定义,实际上就是导出类的成员函数,关于h文件等操作与导出普通函数极为相似,只需注重语法上的问题。
//myClassDll.h 在此文件中保存类的定义
#ifndef _MY_CLASS_DLL_H
#define _MY_CLASS_DLL_H
#ifdef MYDLL_EXPORT
#define MYDLL _declspec(dllexport)
#else
#define MYDLL _declspec(dllimport)
#endif
class MYDLL MyObj //整个类的导出和导入只需此处用到MYDLL
{
public:
MyObj(int v);
void display();
private:
int value;
}
#endif
//myClassDll.cpp
#include <stdio.h>
#define MYDLL_EXPORT
#include "myClassDll.h"
//在cpp中定义的成员函数无需再使用关键字MYDLL导出
MyObj:: MyObj(int v)
:value(v)
{
}
void MyObj:: diplay()
{
printf("Value: %d", value);
}
//Target.cpp 目标工程cpp
//包含头文件和lib后直接实用类创建对象
#include <stdio.h>
#include "myClassDll.h"
#pragma comment(lib, "myClassDll.lib")
int main(int argc, char** argv)
{
MyObj object(123);
object.display();
return 1;
}
6. 静态库的编译
静态库相比于动态库而言要简单得多,没有动态库的导入和导出操作,其优缺点、适用范围等也与动态库有着很多不同。首先我们看一下静态库的实质:静态库仅有一个.lib文件,其中直接含有代码段和数据段,在连接过程中,相当于直接把代码和数据替换过来,形成完整的可执行程序,因而最终生成的.exe文件是对.lib文件没有依赖的,从体积上看,静态库lib的大小要比动态库的lib大得多。
1> 编写一个静态库文件并调用
// mylib.h 静态库h文件
#ifndef _MYLIB_H_
#define _MYLIB_H_
int Add(int a, int b);
#endif
//mylib.cpp 静态库cpp文件
#include "mylib.h"
int Add(int a, int b)
{
return a + b;
}
从上述代码段来看,静态库文件的编写和普通h和cpp文件的编写并无差别。在配置属性——连接器——输出中可设定编译后lib文件的名称和位置,注意同dll文件一样,需要在配置属性——C/C++——预处理器中取消预编译头。
2> 调用一个静态库文件
将编译生成的lib文件同h文件一并拷贝至目标工程文件夹下,创建APP.cpp并输入以下代码,完成对库的调用。
//APP.cpp
#include <stdio.h>
#include "mylib.h"
#pragma comment(lib, "mylib")
int main(int argc, char** argv)
{
int result = Add(10, 11);
printf("Result : %d", result);
return 1;
}
3> 静态库的几点说明
-
对静态库的使用有所限制,某一版本VS编译的静态库文件只能供同一版本VS的工程调用;编译静态库的工程与调用静态库的工程以下属性必须相同:配置属性——C/C++——代码生成——运行时库(要是Mtd都是Mtd等等),否则会出现LINK Warning LNK4098。
-
静态库的优点:使用静态库生成的exe文件不再依赖于lib文件。
-
DLL的优点: 便于升级更新,只要接口保持不变,可直接替换DLL来升级程序,并不需要重新编译程序。
7. 动态库的手工加载
在上述加载DLL的过程中,exe文件在开始运行时首先加载所有用到的dll文件,再运行,被称为动态库的自动加载。也可以在编译时不指定dll,在运行时调用LoadLibrary来加载动态库,使用FreeLibrary来卸载动态库,这种方式被称为手动加载,它提供了一种在运行时、手工加载dll的技术手段,增加了编程的灵活性。
1> 手工加载示例
对DLL的3要求:
-
要求待调用函数按照“C”方式编译
-
dll文件可被系统搜索到
//mydll.h
#ifndef _MYDLL_H_
#define _MYDLL_H_
#include <stdio.h>
#ifdef MYDLL_EXPORT
#define MYDLL _declspec(dllexport)
#else
#define MYDLL _declspec(dllimport)
#endif
extern "C" MYDLL int Add(int a, int b); //将函数声明为一个C函数
#endif
//mydll.cpp
#include "mydll.h"
int Add(int a, int b)
{
return a + b;
}
2> 关于extern "C"的说明
首先需要弄明白C函数与C++函数的区别。在存储时,函数和变量一样,都占有一定的内存空间,而函数名被加以修饰后,可作为函数内存空间的首地址,联想类比数组。C++函数的一个重要特性在于函数的可重载性,一般C函数的修饰的是固定的,例如int Add(int a, int b)函数,可能存储时首地址名为_Add,而C++由于重载的特性,存储时更为负载,有可能是将形参都列入函数名,如_Add_int_int等(这里只是举个例子,具体情况是编译器而定)。而extern “C”的作用是让编译器以C函数的方式编译目标函数,从而实现后期能够找到dll函数地址。
3> 手动加载方式
将dll文件放在系统可循的目录下后,在主程序中按如下代码手动加载dll
#include <stdio.h>
//包含windows相关头文件
#include <winsock2.h>
#include <windows.h>
int main(int argc, char** argv)
{
HINSTANCE handle = LoadLibrary("my.dll");
if(handle) //如果动态库加载成功
{
//定义要找的函数原型,用函数指针的形式
typedef int (*DLL_FUNCTION_ADD) (int, int); DLL_FUNCTION_ADD为关键字,括号必须有
//找到目标函数地址
DLL_FUNCTION_ADD dll_func = (DLL_FUNCTION_ADD)GetProcAddress(handle, "Add");
if(dll_func)
{
//调用该函数
int result = dll_func(10,20);
printf("Result : %d", result);
}
}
FreeLibrary(handle);
}
说明:
-
需要配置属性:配置属性——常规——字符集选择使用”使用多字节字符集“ ,如果使用Unicode则会找不到函数LoadLibrary。
-
不需要h文件和lib文件一样可以使用
-
即时实用即时卸载
8. VC项目的静态编译
下面来解释一下前面用到的MT/MTd编译和MD/MDd编译,并介绍VC静态编译技术。
一个exe文件在发布后,如果拿到一个新的环境的机子上跑很可能会出错,因为他们的电脑环境很可能缺少必要的VS的dll文件,因此为了解决这一问题,VS给出了静态编译的解决方案。
对于VC而言:
-
”静态编译”,/MT、/MTd——是指实用libc和msvc相关的静态库进行编译
-
“动态编译”,/MD、/MDd——是指实用相应的DLL版本编译。
我们在配置属性——C/C++——运行时库即可做相应改变。
相关英文解释:d——debug、M——Multi-threading(多线程)、T——Text、D——Dynamic