C++学习笔记——动态库与静态库

目录

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 

//使用库
#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文件需放在以下五个目录中的任意一个即可被操作系统找到:

  1. 可执行文件所在目录;

  2. 进程当前目录;

  3. 系统目录,如C:\Windows\System32等;

  4. Windows目录,如C:\Windows;

  5. 环境变量(系统变量)PATH中的目录。(注:环境变量修改后重启生效!

2. DLL的加载、卸载与多进程的数据段

需要明确的是,dll文件并不能独立运行,只有当可执行文件.exe加载它时,他才能运行。在exe中有一些标志性信息,表明了此exe文件需要依赖那些dll文件,操作系统即在上述五个目录开始寻找dll文件。exe被加载到内存,形成一个进程(process),dll也被加载到内存;此时进程可以直接调用dll里的函数。因此一个可执行文件实体可以有多个进程,那么我们就来考虑一个问题,当多个进程同时调用一个dll时,dll的全局数据段是共享的吗?考虑一下代码:

//export.cpp
#include 

static int value = 0;

_declspec(dllexport) void ValueSet(int a)
{
    value = a;
}

_declspec(dlliexport) int ValueGet()
{
    return value;
}

//import.cpp

#include
#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 

#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 
#include 

#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;
}

 

C++学习笔记——动态库与静态库_第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 
MYDLL int Add (int a, int b);

#endif

//mydll.cpp
#define MYDLL_EXPORT                    //预编译规定“输出选项”,用作编译dll工程里的cpp文件
#include 

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

生成dll、lib文件后,将dll、lib和h文件一并拷贝至目标工程文件夹,目标工程cpp调用dll文件中函数如下,用户根据头文件即可知道dll中函数的相关信息,同时也省去了导入函数的麻烦。

#include 
#include                     
//此处不需任何其他宏定义,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         //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 
#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 
#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 

#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 

#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 

//包含windows相关头文件
#include 
#include 

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

 

作者冷风哈,QQ:164989932

码字不易,转载请注明出处。

你可能感兴趣的:(C++)