动态链接库

windows API中的所有函数都包含在dll中。
其中三个最重要的dll:
1、Kernel32.dll:包含用于管理内存、进程和线程的各个函数。
2、User32.dll:包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数。
3、GDI32.dll:包含用于画图和显示文本的各个函数。

静态库:lib
函数和数据被编译进一个二进制文件中,在使用静态库的情况下,编译器在链接可执行程文件时,从静态库中复制这些函数和数据,并把他们与最终的应用程序的其他模块组合起来创建最终可执行的exe。

动态链接库的使用:
在使用动态链接库的时候,一般提供两个文件,一个dll,一个lib,该lib不是静态库,而是一个引入库。引入库中包含被dll导出的函数和变量的符号名,dll包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库即可,dll中的函数和数据并不复制到可执行文件中,在运行的时候,再去加载dll,访问dll中导出的函数。

使用动态链接库的好处:
1、使用多种编程语言来编写
2、增强产品的功能:开发规范。
3、提供二次开发的平台
4、简化项目的管理
5、可以节省磁盘空间和内存
6、有助于资源的共享
7、有助于实现应用程序的本地化

动态链接库加载的两种方式:
1.隐式链接
2.显示加载

////////////mydll.dll///////////////////////
_declspec(dllexport) int add(int a ,int b)
{
    return a+b;
}

_declspec(dllexport) int sub(int a ,int b)
{
    return a-b;
}
//////////////////////////////////////////
//VS环境下,查看dll导出的函数

dumpbin -exports mydll.dll

ordinal hint RVA      name

1    0 0001107D ?add@@YAHHH@Z = @ILT+120(?add@@YAHHH@Z)
2    1 00011127 ?sub@@YAHHH@Z = @ILT+290(?sub@@YAHHH@Z)

为了支持函数重载,C++编译器在编译函数的时候,将函数名字都改了。
不同的C++编译改函数名字的方法不同,怎么办?后面再讲。

测试dll:
新建MFC程序,添加两个按钮:

//告诉编译器,这两个函数在外部定义。
extern int add(int a ,int b);
extern int sub(int a ,int b);

void Clesson19_dll_testDlg::OnBnClickedBtnAdd()
{
    // TODO: 在此添加控件通知处理程序代码
    CString str;
    str.Format (_T("5+3=%d"),add(5,3));
    MessageBox(str);
}


void Clesson19_dll_testDlg::OnBnClickedBtnSub()
{
    // TODO: 在此添加控件通知处理程序代码
    CString str;
    str.Format(_T("5-3=%d"),sub(5,3));
    MessageBox(str);
}

编译出错,提示找不到xxx符号。因为没有提供引入库。

将前面生成的lib(引入库)引入进来即可。最简单的方法就是将该lib文件拷贝到解决方案所在的目录下,再在[输入|附加依赖项]中填写该lib库的名字即可。这样编译与链接通过。

可执行程序的输入信息:

dumpbin -imports xx.exe

优化

extern int add(int a ,int b);
extern int sub(int a ,int b);
//替换为:
_declspec(dllimport) int add(int a ,int b);
_declspec(dllimport) int sub(int a ,int b);

明确告诉编译器,我们声明的函数是一个dll中的引入库lib中,这样编译器可以生成执行效率更高的代码。

使用dll的时候,一般提供头文件,以告诉使用者,该dll导出了哪些函数。

即将:

_declspec(dllimport) int add(int a ,int b);
_declspec(dllimport) int sub(int a ,int b);

写入到一个头文件中即可。

下面我们来改造头文件,使头文件既可以为dll的使用者使用,也可以为dll本身所使用。

在dll工程中,写头文件如下:

//dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif

DLL1_API int add(int a ,int b);
DLL1_API int sub(int a ,int b);

改造dll源文件:

//dll1.cpp

#define DLL1_API _declspec(dllexport)
#include "dll1.h"

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

int sub(int a ,int b)
{
    return a-b;
}

分析:编译的时候,头文件不参与编译。当编译dll源文件时,定义了DLL1_API,然后再包含dll.h的时候,发现已经定义了DLL1_API,所以什么也不做,此时,dll1.h中的DLL1_API表示的是,_declspec(dllexport)表示要导出的函数。

当dll的使用者包含该头文件的时候,只要他的项目中没有定义DLL1_API,则#define DLL1_API _declspec(dllimport) ,所以DLL1_API代表的是_declspec(dllimport)

导出C++中的类:

//dll1.h:

class DLL1_API Point
{
public:  
    void output(int x,int y);
};
//dll1.cpp:

#include <Windows.h>
#include <stdio.h>
#include "dll1.h"

void Point::output(int x,int y)
{
    //获取前景窗口的句柄,就是用户当前工作线程中的工作窗口句柄
    HWND hwnd = GetForegroundWindow();
    HDC hdc  = GetDC(hwnd);

    char buf[20];
    memset(buf,0,20);
    sprintf(buf,"x=%d,y=%d",x,y);
    //VS2010将项目的编码由使用Unicode字符集改为多字节字符集,第三个参数才不会报错。
    TextOut(hdc,0,0, buf,strlen(buf));
    ReleaseDC(hwnd,hdc);
}

如果我们只想导出某个类中的某个成员函数:
只需要将类后面的DLL1_API加到要导出的函数之前即可。

class Point
{
public:  
    DLL1_API void output(int x,int y);
    void test();
};

如果是导出类,则类中所有的公开的成员函数都会被导出,如果只是导出了公开的成员函数,则只有该被导出的公开的成员函数才能够被访问。

//在MFC中测试dll

void Clesson19_dll_testDlg::OnBnClickedBtnOut()
{
    // TODO: 在此添加控件通知处理程序代码

    Point pt;
    pt.output(5,3);
}

不管是导出类还是导出成员函数,类成员函数的访问方式是一样的,都是通过类的实例.成员函数才访问。

C++编译器在导出函数的时候会对函数的名称进行改变,以支持重载,这样,一种C++编译器导出的dll如果在另一种C++ 编译器环境下可能就不能使用,因为编译规则不同,可能找不到对应的函数名。
此时,我们最好手动加上extern “C” 限制编译器在编译函数的时候不要修改函数的名字。

在dll1.h中

//dll1.h
#ifndef DLL1_API
#else 
#define DLL1_API extern "C" _declspec(dllimport)
#endif

这个头文件是在dll工程中的,同时也是引用dll使用的,所以也要加上extern “C”告诉客户端dll中的导出函数的名字没有发生变化,所以在客户端编译环境下(不管是C编译器还是C++编译器)就会按照函数本来的名字去dll中找相应的函数。

在dll1.cpp中

//dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport)

extern “C”的缺点:
1.不能导出类的一个成员函数,只能导出全局函数。
2.如果函数的调用约定发生改变的话,即使你使用extern “C”,函数的名字也会发生改变。

//dll1.h

#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif


DLL1_API int _stdcall add(int a ,int b);
DLL1_API int _stdcall sub(int a ,int b);
//dll1.cpp

#define DLL1_API extern "C" _declspec(dllexport)
#include "dll1.h"
#include <Windows.h>
#include <stdio.h>

int _stdcall add(int a ,int b)
{
    return a+b;
}

int _stdcall sub(int a ,int b)
{
    return a-b;
}

_stdcall,即所谓的标准调用约定,也就是WINAPI,是Delphi调用约定(Pascal调用约定),即使定义了extern “C”,C++ 编译器在编译函数的时候也会改变函数的名字。
如果没有加_stdcall,则默认使用的是C语言的函数调用约定。

所以,如果我们写的dll要给Delphi客户端使用的时候,需要指定函数的调用约定为_stdcall。但是函数的名字终究是改变了。

解决方案:
模块定义文件,解决函数名字改变的问题:

//dll2.cpp

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

int sub(int a,int b)
{
    return a-b;
}
//dll2.def

LIBRARY Dll2

EXPORTS   //导出的函数的名字
add
sub 
//如果还有muil()   可以这么写    multiply = muil     表示muil函数将会以multiply函数名导出。

这样的话,导出的函数的名字是不会发生改变的。

我们现在使用动态加载链接库的方式测试一下:

LoadLibrary();加载一个可执行的模块或者加载一个可执行程序。返回一个模块句柄。

得到导出的dll中的函数的地址
GetProcAddress();

动态加载dll,不需要头文件,也不需要lib文件。
优点:
在需要的时候加载dll,提高性能。

//lesson19_dll_testDlg.cpp

void Clesson19_dll_testDlg::OnBnClickedBtnAdd()
{
    HINSTANCE hInst;
    hInst = LoadLibrary(_T("G:\\Study\\Code\\孙鑫CPP\\Debug\\lesson19_dll2.dll"));
    typedef int (*ADDPROC)(int a,int b);
    ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,"add");
    if(!ADD)
    {
        MessageBox(_T("获取函数地址失败"));
    }

    CString str;
    str.Format (_T("5+3=%d"),ADD(5,3));
    MessageBox(str);
    FreeLibrary(hInst);
}

此时,即使在dll中函数的调用约定改了,也不会影响函数名。

//dll2.cpp

int _stdcall add(int a,int b)
{
    return a+b;
}

int _stdcall sub(int a,int b)
{
    return a-b;
}

现在,我们的dll2.cpp中函数的调用约定改为_stdcall,此时生成的dll中的函数名仍为在def中指定的函数名。

但是在客户端调用的时候,一定要指定使用什么调用约定调用函数。

HINSTANCE hInst;
hInst = LoadLibrary(_T("G:\\Study\\Code\\孙鑫CPP\\Debug\\lesson19_dll2.dll"));
typedef int (_stdcall *ADDPROC)(int a,int b);
ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,"add");
if(!ADD)
{
    MessageBox(_T("获取函数地址失败"));
}

CString str;
str.Format (_T("5+3=%d"),ADD(5,3));
MessageBox(str);
FreeLibrary(hInst);

即在定义函数指针前面加上_stdcall。

使用动态加载dll的方式,使用dumpbin -imports xx.exe是看不出dll中的信息的。

我们还可以使用序号加载dll中的导出函数。

ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCEA(1));

一般情况下,我们还是使用函数名访问dll中的导出函数。虽然,使用序号可以在函数名即使发生改变的情况下也能够使用函数,但是写出的代码不易于阅读。


DllMain()函数,可选的入口点。

BOOL WINAPI DllMain(HANDLE hinstDLL, DWORD dwReason, LPVOID lpvReserved);

只要拷贝到cpp程序中即可,程序自动调用。

动态加载dll时,不使用时记得释放。

BOOL FreeLibrary( HMODULE hLibModule); 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值