什么是DLL(动态链接库)?

DLL是一个包含可由多个程序同时使用的代码和数据的库。例如:在Windows操作系统中,Comdlg32 DLL执行与对话框有关的常见函数。因此,每个程序都可以使用该DLL中包含的功能来实现“打开”对话框。这有助于促进代码重用和内存的有效使用。这篇文章的目的就是让你一次性就能了解和掌握DLL。

为什么要使用DLL(动态链接库)?

代码复用是提高软件开发效率的重要途径。一般而言,只要某部分代码具有通用性,就可以将它构造成相对独立的功能模块并在之后的项目中重复使用。比较常见的例子是各种应用程序框架,它们都以源代码的形式发布。由于这种复用是源代码级别的,源代码完全暴露给了程序员,因而称之为“白盒复用”。白盒复用有以下三个缺点:

  1. 暴露源代码,多份拷贝,造成存储浪费;
  2. 容易与程序员的本地代码发生命名冲突;
  3. 更新模块功能比较困难,不利于问题的模块化实现;

为了弥补这些不足,就提出了“二进制级别”的代码复用了。使用二进制级别的代码复用一定程度上隐藏了源代码,对于“黑盒复用”的途径不只DLL一种,静态链接库,甚至更高级的COM组件都是。

使用DLL主要有以下优点:

  1. 使用较少的资源;当多个程序使用同一函数库时,DLL可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其它在Windows操作系统上运行的程序;
  2. 推广模块式体系结构;
  3. 简化部署与安装。

创建DLL

打开Visual Studio 2012,创建如下图的工程:


输入工程名字,单击[OK];

单击[Finish],工程创建完毕了。现在,我们就可以在工程中加入我们的代码了。加入MyCode.h和MyCode.cpp两个文件;在MyCode.h中输入以下代码:

#ifndef _MYCODE_H_
#define _MYCODE_H_
#ifdef DLLDEMO1_EXPORTS
#define EXPORTS_DEMO _declspec( dllexport )
#else
#define EXPORTS_DEMO _declspec(dllimport)
#endif
extern "C" EXPORTS_DEMO int Add (int a , int b);
#endif

在MyCode.cpp中输入以下代码:

#include "stdafx.h"
#include "MyCode.h"
int Add ( int a , int b )
{return ( a + b );
}

编译工程,就会生成DLLDemo1.dll文件。在代码中,很多细节的地方,我稍后进行详细的讲解。

使用DLL

当我们的程序需要使用DLL时,就需要去加载DLL,在程序中加载DLL有两种方法,分别为加载时动态链接和运行时动态链接。

  1. 在加载时动态链接中,应用程序像调用本地函数一样对导出的DLL函数进行显示调用。要使用加载时动态链接,需要在编译和链接应用程序时提供头文件和导入库文件(.lib)。当这样做的时候,链接器将向系统提供加载DLL所需的信息,并在加载时解析导出的DLL函数的位置;
  2. 在运行时动态链接中,应用程序调用LoadLibrary函数或LoadLibraryEx函数以在运行时加载DLL。成功加载DLL后,可以使用GetProcAddress函数获得要调用的导出的DLL函数的地址。在使用运行时动态链接时,不需要使用导入库文件。

在实际编程时有两种使用DLL的方法,那么到底应该使用那一种呢?在实际开发时,是基于以下几点进行考虑的:

  1. 启动性能如果应用程序的初始启动性能很重要,则应使用运行时动态链接;
  2. 易用性在加载时动态链接中,导出的DLL函数类似于本地函数,我们可以方便地进行这些函数的调用;
  3. 应用程序逻辑在运行时动态链接中,应用程序可以分支,以便按照需要加载不同的模块。

下面,我将分别使用两种方法调用DLL动态链接库。

加载时动态链接:

#include <windows.h>
#include <iostream>
//#include "..\\DLLDemo1\\MyCode.h"
using namespace std;
#pragma comment(lib, "..\\debug\\DLLDemo1.lib")
extern "C" _declspec(dllimport) int Add(int a, int b);
int main(int argc, char *argv[])
{cout<<Add(2, 3)<<endl;return 0;
}

运行时动态链接:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{HMODULE hDll = LoadLibrary("DLLDemo1.dll");if (hDll != NULL){AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");if (add != NULL){cout<<add(2, 3)<<endl;}FreeLibrary(hDll);}
}

上述代码都在DLLDemo1工程中。(工程下载)。

DllMain函数

Windows在加载DLL时,需要一个入口函数,就像控制台程序需要main函数一样。有的时候,DLL并没有提供DllMain函数,应用程序也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的默认DllMain函数版本,并不意味着DLL可以抛弃DllMain函数。

根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数,这就说明不能在客户端直接调用DllMain函数,DllMain函数是自动被调用的。

DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DllMain函数也被调用。参数ul_reason_for_call指明了调用DllMain的原因,有以下四种情况:

DLL_PROCESS_ATTACH:当一个DLL被首次载入进程地址空间时,系统会调用该DLL的DllMain函数,传递的ul_reason_for_call参数值为DLL_PROCESS_ATTACH。这种情况只有首次映射DLL时才发生;

DLL_THREAD_ATTACH:该通知告诉所有的DLL执行线程的初始化。当进程创建一个新的线程时,系统会查看进程地址空间中所有的DLL文件映射,之后用DLL_THREAD_ATTACH来调用DLL中的DllMain函数。要注意的是,系统不会为进程的主线程使用值DLL_THREAD_ATTACH来调用DLL中的DllMain函数;

DLL_PROCESS_DETACH:当DLL从进程的地址空间解除映射时,参数ul_reason_for_call参数值为DLL_PROCESS_DETACH。当DLL处理DLL_PROCESS_DETACH时,DLL应该处理与进程相关的清理操作。如果进程的终结是因为系统中有某个线程调用了TerminateProcess来终结的,那么系统就不会用DLL_PROCESS_DETACH来调用DLL中的DllMain函数来执行进程的清理工作。这样就会造成数据丢失;

DLL_THREAD_DETACH:该通知告诉所有的DLL执行线程的清理工作。注意的是如果线程的终结是使用TerminateThread来完成的,那么系统将不会使用值DLL_THREAD_DETACH来执行线程的清理工作,这也就是说可能会造成数据丢失,所以不要使用TerminateThread来终结线程。以上所有讲解在工程DLLMainDemo(工程下载)都有体现。

函数导出方式

在DLL的创建过程中,我使用的是_declspec( dllexport )方式导出函数的,其实还有另一种导出函数的方式,那就是使用导出文件(.def)。你可以在DLL工程中,添加一个Module-Definition File(.def)文件。.def文件为链接器提供了有关被链接器程序的导出、属性及其它方面的信息。

对于上面的例子,.def可以是这样的:

LIBRARY     "DLLDemo2"
EXPORTS
Add @ 1 ;Export the Add function

Module-Definition File(.def)文件的格式如下:

  1. LIBRARY语句说明.def文件对应的DLL;
  2. EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号有一定的作用)。

使用def文件,生成了DLL,客户端调用代码如下:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{HMODULE hDll = LoadLibrary("DLLDemo2.dll");if (hDll != NULL){AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));if (add != NULL){cout<<add(2, 3)<<endl;}FreeLibrary(hDll);}
}

可以看到,在调用GetProcAddress函数时,传入的第二个参数是MAKEINTRESOURCE(1),这里面的1就是def文件中对应函数的序号。(工程下载)

extern “C”

为什么要使用extern “C”呢?C++之父在设计C++时,考虑到当时已经存在了大量的C代码,为了支持原来的C代码和已经写好的C库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。在声明函数时,注意到我也使用了extern “C”,这里要详细的说说extern “C”。

extern “C”包含两层含义,首先是它修饰的目标是”extern”的;其次,被它修饰的目标才是”C”的。先来说说extern;在C/C++中,extern用来表明函数和变量作用范围(可见性)的关键字,这个关键字告诉编译器,它申明的函数和变量可以在本模块或其它模块中使用。extern的作用总结起来就是以下几点:

  1. 在一个文件内,如果外部变量不在文件的开头定义,其有效范围只限定在从定义开始到文件的结束处。如果在定义前需要引用该变量,则要在引用之前用关键字”extern”对该变量做”外部变量声明”,表示该变量是一个已经定义的外部变量。有了这个声明,就可以从声明处起合理地使用该变量了,例如:
/*
** FileName     : Extern Demo
** Author       : Jelly Young
** Date         : 2013/11/18
** Description  : More information, please go to http://www.jellythink.com
*/
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{extern int a;cout<<a<<endl;
}
int a = 100;
  1. 在多文件的程序中,如果多个文件都要使用同一个外部变量,不能在各个文件中各定义一个外部变量,否则会出现“重复定义”的错误。正确的做法是在任意一个文件中定义外部变量,其它文件用extern对变量做“外部变量声明”。在编译和链接时,系统会知道该变量是一个已经在别处定义的外部变量,并把另一文件中外部变量的作用域扩展到本文件,这样在本文件就可以合法地使用该外部变量了。写过MFC程序的人都知道,在在CXXXApp类的头文件中,就使用extern声明了一个该类的变量,而该变量的实际定义是在CXXXApp类的实现文件中完成的;
  2. 外部函数,在定义函数时,如果在最左端加关键字extern,表示此函数是外部函数。C语言规定,如果在定义时省略extern,则隐含为外部函数。而内部函数必须在前面加static关键字。在需要调用此函数的文件中,用extern对函数作声明,表明该函数是在其它文件中定义的外部函数。

接着说”C”的含义。我们都知道C++通过函数参数的不同类型支持重载机制,编译器根据参数为每个重载函数产生不同的内部标识符;但是,如果遇到了C++程序要调用已经被编译后的C函数,那该怎么办呢?比如上面的int Add ( int a , int b )函数。该函数被C编译器后在库中的名字为_Add,而C++编译器则会生成像_Add_int_int之类的名字用来支持函数重载和类型安全。由于编译后的名字不同,C++程序不能直接调用C函数,所以C++提供了一个C连接交换指定符号extern “C”来解决这个问题;所以,在上面的DLL中,Add函数的声明格式为:extern “C” EXPORTS_DEMO int Add (int a , int b)。这样就告诉了C++编译器,函数Add是个C连接的函数,应该到库中找名字_Add,而不是找_Add_int_int。当我们将上面DLL中的”C”去掉,编译生成新的DLL,使用Dependency Walker工具查看该DLL,如图:


请注意导出方式为C++,而且导出的Add函数的名字添加了很多的东西,当使用这种方式导出时,客户端调用时,代码就是下面这样:

#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int main(int argc, char *argv[])
{HMODULE hDll = LoadLibrary("DLLDemo1.dll");if (hDll != NULL){AddFunc add = (AddFunc)GetProcAddress(hDll, "?Add@@YAHHH@Z");if (add != NULL){cout<<add(2, 3)<<endl;}FreeLibrary(hDll);}
}

请注意GetProcAddress函数的第二个参数,该参数名就是导出的函数名,在编码时,写这样一个名字是不是很奇怪啊。当我们使用extern “C”方式导出时,截图如下:

注意导出方式为C,而且函数名现在就是普通的Add了。我们再使用GetProcAddress时,就可以直接指定Add了,而不用再加那一长串奇怪的名字了。

DLL导出变量

DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据。

DLL导出类

DLL中定义的类,也可以被导出。详细工程代码,请参见(工程下载)

总结

对DLL的讲解就到此结束,由于MFC在现在的环境下使用较少,此处不予讲解,如果以后做项目遇到了MFC的DLL相关知识,我再做总结。最后,希望大家给我的博客提出一些中肯的建议。

本文版权归果冻说所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。
如果这篇文章对你有帮助,你可以
请我喝杯咖啡。

» 本文链接:http://www.jellythink.com/archives/111
» 订阅本站:http://www.jellythink.com/feed

» 转载请注明来源:果冻想 » <a rel="bookmark" title="在Visual Studio中使用C++创建和使用DLL"《在Visual Studio中使用C++创建和使用DLL》

******************************************************************************************************

参考上面的内容,自己在VS2010开发环境上测试了一遍,测试步骤如下:

1.将所需要的函数封装成DLL.

  首先创建DLL工程项目,命名为DllDemo,如下图:     
然后创建头文件(MyCode.h)和.cpp文件(MyCode.cpp),并分别添加代码:
MyCode.h头文件:
#ifndef _MYCODE_H_
#define _MYCODE_H_
#ifdef DLLDEMO1_EXPORTS
#define EXPORTS_DEMO _declspec( dllexport )
#else
#define EXPORTS_DEMO _declspec(dllimport)
#endif
extern "C" EXPORTS_DEMO int Add (int a , int b);
#endif

MyCode.cpp文件:

#include "MyCode.h"
int Add ( int a , int b )
{return ( a + b );
}

编译工程,就会在Debug文件下生成DllDemo.dll文件。

2.加载时动态链接方式调用DLL.

    首先创建控制台应用程序,命名为DllTest,如下图所示:
``
 然后添加代码:
// DllTest.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
//#include "..\\DLLDemo1\\MyCode.h"
using namespace std;
#pragma comment(lib, "..\\debug\\DllDemo.lib")         //***********************************************************************问题1
extern "C" _declspec(dllimport) int Add(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{cout<<Add(2, 3)<<endl;while(1);//程序运行到这,方便看运行结果return 0;
}

运行结果如下图:

注意:导入库文件的目录必须在本工程的目录下,也就是说要把生成的dll和lib文件都要拷贝到该工程的目录下,因为不再该目录下,尽管修改了路径,仍然提示找不到DllDemo.dll,不知道为什么?

3.运行时动态链接方式调用DLL.

 和第二步一样,创建控制台应用程序,命名为DllTest1,添加代码如下:
// DllTest1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <windows.h>
using namespace std;
typedef int (*AddFunc)(int a, int b);int _tmain(int argc, _TCHAR* argv[])
{HMODULE hDll = LoadLibrary(_T("DllDemo.dll"));if (hDll != NULL){AddFunc add = (AddFunc)GetProcAddress(hDll, "Add");if (add != NULL){cout<<add(2, 3)<<endl;}FreeLibrary(hDll);}while(1);
}
运行结果如下图:

4.以.def文件(模块定义文件)方式导出函数(非_declspec(dllexport)方式导出函数):

 首先创建DLL工程项目,命名为DllDemo,如下图:  
然后创建头文件(MyCode.h)和.cpp文件(MyCode.cpp),并分别添加代码:
MyCode.h头文件:
#ifndef _MYCODE_H_
#define _MYCODE_H_
extern "C" int Add (int a , int b);
#endif

MyCode.cpp文件:

#include "MyCode.h"
int Add ( int a , int b )
{return ( a + b );
}

然后添加模块定义文件(.def文件):



添加代码:
LIBRARY  "DllDemo"   //这里的字符串名和工程名要一致
EXPORTS
Add @1;Export the Add function

编译工程,即刻生成DllDemo.dll文件。

使用def文件,生成了DLL,客户端调用代码如下:
// DllTest2.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include <iostream>
using namespace std;
typedef int (*AddFunc)(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{HMODULE hDll = LoadLibrary("DllDemo.dll");if (hDll != NULL){AddFunc add = (AddFunc)GetProcAddress(hDll, MAKEINTRESOURCE(1));if (add != NULL){cout<<add(2, 3)<<endl;}FreeLibrary(hDll);}while(1);
}

工程代码下载:

1.生成动态链接库(_declspec(dllexport)方式导出函数)
2.生成动态链接库(以.def文件(模块定义文件)方式导出函数)
3.以加载时动态链接方式调用DLL
4.以运行时动态链接方式调用DLL
5.以模块定义方式(.def文件)建立的动态链接库的调用

遇到的问题:

1.库导入的时候目录的问题。对应文中的问题1,后面有解释。
2.字符集的问题(是Unicode字符集还是多字节集),两种方案,一种修改字符集为多字节集,二是将字符串前面加 _T(""),文中问题2
3.不知道怎么通过模块定义文件方式生成DLL,通过看参考博客的代码找到了答案,主要修改头文件,和添加模块定义文件。
4.模块定义文件中的库文件名应和工程名一致。

【VS2010学习笔记】【编程实例】 (在Visual Studio中使用C++创建和使用DLL)相关推荐

  1. powershell 编程_如何使用PowerShell以编程方式更改Visual Studio中的默认浏览器,并可能使自己陷入困境...

    powershell 编程 UPDATE: Why my own MacGyver solution was brilliant in its horrible way, the folks over ...

  2. 在Visual Studio中利用NTVS创建Pomelo项目

    刚看新闻,才知道微软发布了Node.js Tools for Visual Studio(NTVS),受够了WebStorm输入法Bug的困扰,这下终于可以解脱了.以Pomelo为例,运行命令:pom ...

  3. OpenCV学习笔记04:在Visual Studio上使用OpenCV4.5.5

    文章目录 一.下载OpenCV 二.安装OpenCV 三.配置OpenCV (一)改成x64调试模式 (二)配置包含目录 (三)配置库目录 (四)配置链接器 (五)配置环境变量 四.显示图像 (一)编 ...

  4. 【Unity学习历程之一】给Visual Studio中的C#脚本编辑添加自动补全

    前言 在咸鱼划水了两年之后,要找工作的如今终于是食得了恶果,经历了"深圳四日游"的求职无果,和各种眼高手低的状况(西山居一面被刷,莉莉丝筛选结果石沉大海,唉),我现在已经在粤嵌参加 ...

  5. C#笔记24:善用Visual Studio

    C#笔记24:善用Visual Studio 1:调试的四个窗口 2:加快编码速度 3:加快编码速度 4:使用第三方工具丰富VS 5:VS2010中的工具 6:学会调试 1:调试的四个窗口 立即窗口: ...

  6. 学习Azure Functions:在Visual Studio 2017中创建Azure Functions

    目录 介绍 Azure Azure帐户设置 设置开发环境 案例分析 在Visual Studio 2017中创建Azure Functions 添加Azure Functions项目 添加HTTP触发 ...

  7. 在 Visual Studio 中使用 Q# 进行量子编程

    1 量子计算机与量子编程 1.1 量子计算机 Quantum computing is computing using quantum-mechanical phenomena, such as su ...

  8. IDE之VS:利用 Visual Studio中的IDE配置python语言进行编程

    IDE之VS:利用 Visual Studio中的IDE配置python语言进行编程 目录 第一步,先安装python环境 第二步,加载本地已有的python 第一步,先安装python环境

  9. IDE之VS:利用 Visual Studio中的IDE配置C++语言进行编程

    IDE之VS:利用 Visual Studio中的IDE配置C++语言进行编程 目录 C++编译器之VS2015 1.新建项目,VisualC++,空项目,确定 2.右键项目文件夹,添加,新建 3.添 ...

  10. expect学习笔记及实例详解【转】

    1. expect是基于tcl演变而来的,所以很多语法和tcl类似,基本的语法如下所示: 1.1 首行加上/usr/bin/expect 1.2 spawn: 后面加上需要执行的shell命令,比如说 ...

最新文章

  1. 的union_C语言“隐秘的角落”——union没那么简单
  2. CF1000G Two-Paths
  3. Django中提供了6种缓存方式
  4. C1. 组队活动 Small(BNUOJ)
  5. python中提供怎样的内置库、可以用来创建用户界面_使用外部GUI库在Autodesk中创建用户界面可能会...
  6. Rust——Macos安装使用
  7. lucene-5.3.1配置(win7x64)
  8. jqueryForm 异步上传图片文件
  9. 算法与数据中台:网约车业务实践
  10. c 生成html的div,createElement动态创建HTML对象脚本代码
  11. sun键盘没有stop键_【转帖】SUN基础知识
  12. Power Query For Excel数据处理利器
  13. javaFX,Scene Builder引入Jfoenix
  14. unity制作子弹击砖块过程分析
  15. AI智能视频分析系统提升水泥厂安全监管解决方案
  16. 无线传感网络复习重点
  17. 2021-2027全球与中国冰箱稳定器市场现状及未来发展趋势
  18. 伪原创视频需要改什么 改视频md5的软件
  19. Teamcity的安装与使用
  20. 手把手教你设计:拼团活动

热门文章

  1. java将分布式打包_java版 电子商务Springcloud分布式微服务多用户商城系统-Springboot项目打包...
  2. 短信转移到另一个手机接收_如何将iPhone手机接收短信同步到另外一台设备上?...
  3. maven打包失败:自定义项目工具类打包给其他微服务使用
  4. 使用python往数据库中添加数据
  5. linux根目录9个g,linux根目录下5个主要的目录,及目录的功能
  6. 设计模式_单例模式回顾_C#版不使用锁保证多线程安全
  7. karto探秘之open_karto 第五章 --- 栅格地图的生成
  8. 在手机上实现实时的单眼3D重建
  9. 结构光相移法-多频外差原理+实践(上)
  10. 导师说,再招女生,他就是孙子