动态链接库的编写和使用详解

作者:zieckey(zieckey@yahoo.com.cn)
All Rights Reserved!

首先说下DLL的原理.
自从微软推出第一个版本的Windows操作系统以来,
动态链接库(DLL)一直是Windows操作系统的基础。
动态链接库通常都不能直接运行,也不能接收消息。
它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。
只有在其它模块调用动态链接库中的函数时,它才发挥作用。
Windows API中的所有函数都包含在DLL中。
其中有3个最重要的DLL,
Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;
User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;
GDI32.dll,它包含用于画图和显示文本的各个函数。


静态库:函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。
在使用静态库的情况下,在编译链接可执行文件时,
链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。
在使用动态库的时候,往往提供两个文件:一个引入库和一个DLL。
引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。
在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,
在运行的时候,再去加载DLL,访问DLL中导出的函数。

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

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


新建一个空的dll工程,然后创建一个dll1.cpp文件如下
int  add(int a,int b)
{
 return a+b;
}

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

编译之后,我们看看导出函数
E:/zieckey/CPP-study/Dll1/Debug>dumpbin -exports dll1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll1.dll

File Type DLL

  Summary

        7000 .data
        1000 .idata
        2000 .rdata
        2000 .reloc
       2A000 .text
这里没有任何导出函数,那么我们可以这样,更改源文件
_declspec(dllexport) int  add(int a,int b)
{
 return a+b;
}

_declspec(dllexport) int  subtract(int a,int b)
{
 return a-b;
}

编译之后再去看看

E:/zieckey/CPP-study/Dll1/Debug>dumpbin -exports dll1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll1.dll

File Type DLL

  Section contains the following exports for Dll1.dll

           0 characteristics
    456BD6E2 time date stamp Tue Nov 28 142746 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 0000100A add@@YAHHH@Z
          2    1 00001005 subtract@@YAHHH@Z

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text

好了,这次我们看到了两个我们自己下的函数名字,只是名字似乎被更改过了,这是VC为了函数重载的方便,它以自己的规则修改了我们的源程序的函数名字.
下面我们来测试下刚刚建立的dll1.dll动态链接库.
我们新建一个win32控制台的空工程,新建一个dlltest.cpp ,下面是它的源码
#include iostream.h
extern int add(int,int);   声明这个函数在外部定义
extern int subtract(int,int);
void main(void)
{
 cout3+5 = add(3,5)endl;
 cout5-3 = subtract(5,3)endl;
}
编译一下
Compiling...
dlltest.cpp
Linking...
dlltest.obj  error LNK2001 unresolved external symbol int __cdecl substract(int,int) (substract@@YAHHH@Z)
dlltest.obj  error LNK2001 unresolved external symbol int __cdecl add(int,int) (add@@YAHHH@Z)
DebugDllTest.exe  fatal error LNK1120 2 unresolved externals
执行 link.exe 时出错.
DllTest.exe - 1 error(s), 0 warning(s)

可以发现是在连接的时候出错了,则说明编译是没有错,编译器找到了这两个函数,因为这两个函数已经声明了.
但是在连接的时候没有找到.我们可以这样做;
将刚刚生成动态链接库文件的同一目录下,找到dll1.lib输入库文件,拷贝到我们的工程目录下,
最后 Project-Settings  在Link选项卡找到Objectlibrary modules  在最后填入 dll1.lib 。
如果原来就有链接,请使用空格分隔。

好的我们再编译下
--------------------Configuration DllTest - Win32 Debug--------------------
DllTest.exe - 0 error(s), 0 warning(s)
没有错误,执行时又出现错误,哦,我们的程序在执行时没有找到dll1.dll这个文件,好说,将它也拷贝到当前工程的目录下.
再执行,ye,这次就好了,输出信息如下
3+5 = 8
5-3 = 2
Press any key to continue

我们可以查看以查看一下这个可执行程序的输入信息
E:/zieckey/CPP-study/DllTest/Debug>dumpbin -imports DllTest.exe
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file DllTest.exe

File Type EXECUTABLE IMAGE

  Section contains the following imports

    Dll1.dll
                43019C Import Address Table
                43003C Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                   0  add@@YAHHH@Z
                   1  subtract@@YAHHH@Z

    KERNEL32.dll
                4301D0 Import Address Table
                430070 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                  CA  GetCommandLineA
                 174  GetVersion
                  7D  ExitProcess
                 22F  RtlUnwind
                 11A  GetLastError
                  1B  CloseHandle
                 2DF  WriteFile
                 218  ReadFile
                 26A  SetFilePointer
                 26D  SetHandleCount
                 152  GetStdHandle
                 115  GetFileType
                 150  GetStartupInfoA
                  51  DebugBreak
                 1AD  InterlockedDecrement
                 1F5  OutputDebugStringA
                 13E  GetProcAddress
                 1C2  LoadLibraryA
                 1B0  InterlockedIncrement
                 124  GetModuleFileNameA
                 29E  TerminateProcess
                  F7  GetCurrentProcess
                 2AD  UnhandledExceptionFilter
                  B2  FreeEnvironmentStringsA
                  B3  FreeEnvironmentStringsW
                 2D2  WideCharToMultiByte
                 106  GetEnvironmentStrings
                 108  GetEnvironmentStringsW
                 126  GetModuleHandleA
                 109  GetEnvironmentVariableA
                 175  GetVersionExA
                 19D  HeapDestroy
                 19B  HeapCreate
                 19F  HeapFree
                 2BF  VirtualFree
                  AA  FlushFileBuffers
                 1B8  IsBadWritePtr
                 1B5  IsBadReadPtr
                 1A7  HeapValidate
                 27C  SetStdHandle
                 241  SetConsoleCtrlHandler
                  BF  GetCPInfo
                  B9  GetACP
                 131  GetOEMCP
                 199  HeapAlloc
                 2BB  VirtualAlloc
                 1A2  HeapReAlloc
                 28B  SetUnhandledExceptionFilter
                 1B2  IsBadCodePtr
                 1E4  MultiByteToWideChar
                 1BF  LCMapStringA
                 1C0  LCMapStringW
                 153  GetStringTypeA
                 156  GetStringTypeW

  Summary

        6000 .data
        1000 .idata
        2000 .rdata
        2000 .reloc
       27000 .text

 

另外我们可以通过VC提供的图形化工具查看我们的可执行程序依赖的动态链接库文件,
在VC的安装目录下的 DSoftwaresMicrosoft Visual StudioVC98Microsoft Visual C++ 6.0 Tools 目录下有这个工具 Depends .
它可以查看可执行程序依赖的动态链接库文件

我们还可以这样声明动态链接库里的函数接口,源码修改为
#include iostream.h
extern int add(int,int);
extern int subtract(int,int);
_declspec(dllimport) int add(int,int);
_declspec(dllimport) int subtract(int,int);
void main(void)
{
 cout3+5 = add(3,5)endl;
 cout5-3 = subtract(5,3)endl;
}
编译运行.
_declspec(dllimport) 就告诉编译器这个函数是从动态链接库的.lib文件中输入的,编译器就能生成运行效率更高的代码.

我们可以把我们自己做的动态链接库文件的函数声明都放到一个头文件中,以便告诉用户这个动态链接库文件里封装的函数原型.
好的我们在前面动态链接库的工程中添加一个头文件 dll1.h ,其里面的内容是
_declspec(dllimport) int add(int,int);
_declspec(dllimport) int subtract(int,int);

那么现在我们就可以将源码修改为
#include iostream.h
extern int add(int,int);
extern int subtract(int,int);
#include ..Dll1dll1.h
void main(void)
{
 cout3+5 = add(3,5)endl;
 cout5-3 = subtract(5,3)endl;
}
编译运行.

我们到这里可以发现,前面的动态链接库源码文件dll1.cpp
_declspec(dllexport) int  add(int a,int b)
{
 return a+b;
}
_declspec(dllexport) int  subtract(int a,int b)
{
 return a-b;
}
这里是导出形式(dllexport),那么这个动态链接库就不能被它自己的源码程序调用,这时我们可以改改就行了
dll1.cpp
#define DLL1_API _declspec(dllexport)
#include dll1.h
int  add(int a,int b)
{
 return a+b;
}
int  subtract(int a,int b)
{
 return a-b;
}


dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API int subtract(int,int);


现在这个dll库就可以被自己调用也可以被其他程序调用了.
我们再看看怎么将类导出,象这样就好了
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API int subtract(int,int);
class DLL1_API Point
{
public
 void output( int x,int y);
};

如果我们不想将整个类都导出,我们只需将类的部分公有成员函数导出,
我们可以这样做
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API int subtract(int,int);
class DLL1_API Point
{
public
 DLL1_API void output( int x,int y);
 void test();
};
这里的void test();是做一个对比用的.
我们用dumpbin -exports dll1.dll看看导出情况
E:/zieckey/CPP-study/Dll1/Debug>dumpbin -exports dll1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll1.dll

File Type DLL

  Section contains the following exports for Dll1.dll

           0 characteristics
    456BE737 time date stamp Tue Nov 28 153727 2006
        0.00 version
           1 ordinal base
           3 number of functions
           3 number of names

    ordinal hint RVA      name

          1    0 00001019 add@@YAHHH@Z
          2    1 0000101E output@Point@@QAEXHH@Z
          3    2 00001014 subtract@@YAHHH@Z

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       31000 .text

这里就可以发现只有
          2    1 0000101E output@Point@@QAEXHH@Z
而没有test()


上面提到了VC++编译器将函数名字进行了改写,这对于其他编译器来说也许就不是什么好事,因为其他编译器很可能找不到导出的函数,
因为名字已经被改动了.那么我们就希望这个名字不要更改.这里可以加上 extern C 标识
dll1.cpp
#define DLL1_API extern C _declspec(dllexport)
#include dll1.h
int  add(int a,int b)
{
 return a+b;
}
int  subtract(int a,int b)
{
 return a-b;
}

dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern C _declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API int subtract(int,int);
编译下,看看导出情况,
E:/zieckey/CPP-study/Dll1/Debug>dumpbin -exports dll1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll1.dll

File Type DLL

  Section contains the following exports for Dll1.dll

           0 characteristics
    456BE969 time date stamp Tue Nov 28 154649 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 0000100A add
          2    1 00001005 subtract

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text

这里就可以看到名字没有改变,
但是用 extern C 标识也有缺陷,就是它只能表示全局函数,
而对于类说,它不能导出类的成员函数.
如果调用约定被改变了的话,即使加了 extern C 标识,函数名还是会被改变
这里我们举例看看
dll1.cpp
#define DLL1_API extern C _declspec(dllexport)
#include dll1.h
int _stdcall  add(int a,int b)
{
 return a+b;
}
int _stdcall  subtract(int a,int b)
{
 return a-b;
}

dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern C _declspec(dllimport)
#endif
DLL1_API int _stdcall add(int,int);
DLL1_API int _stdcall subtract(int,int);
编译下,看看导出情况
E:/zieckey/CPP-study/Dll1/Debug>dumpbin -exports dll1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll1.dll

File Type DLL

  Section contains the following exports for Dll1.dll

           0 characteristics
    456BEBDB time date stamp Tue Nov 28 155715 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001005 _add@8
          2    1 0000100A _subtract@8

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text
这里就可以看到名字还是发生了改变,
_add@8这里的8表示add函数的参数所占字节数.
我们看看下面的解决方法.

再重新新建一个dll的空工程,命名为Dll2      
再新建一个C++源文件,命名为dll2.cpp,
dll2.cpp
int add(int a,int b)
{
 return a+b;
}
int subtract(int a,int b)
{
 return a-b;
}

然后我们创建一个模块定义文件,命名为dll2.def,然后添加我们的工程中.其内容如下

LIBRARY Dll2

EXPORTS
add
subtract

 

其中 LIBRARY Dll2 这个名字一定要与我们的工程名字匹配,另外这句话也不是必须的.
EXPORTS表明动态链接库中要导出的函数名字,我们可以在 MSDN 中查看一下它的用法.

编译一下,看看导出情况
E:/zieckey/CPP-study/Dll2/Debug>dumpbin -exports dll2.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll2.dll

File Type DLL

  Section contains the following exports for Dll2.dll

           0 characteristics
    456BEECE time date stamp Tue Nov 28 160950 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 0000100A add
          2    1 00001005 subtract

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text
      
这里就可以看到名字没有改变,和原来的一样.


上面我们都是用隐式链接的方式调用动态链接库的,
下面我们看看另一种方法,显示调用
将我们的 DllTest 工程中的 Project-Settings 
在Link选项卡中的 Objectlibrary modules 删除 dll1.lib

这里就要用到一个函数
HINSTANCE LoadLibrary
(
  LPCTSTR lpLibFileName    address of filename of executable module
);
它是映射指定的模块到应用进程的地址空间

获得函数的地址空间用如下函数,
FARPROC GetProcAddress(
  HMODULE hModule,     handle to DLL module
  LPCSTR lpProcName    name of function
);

好的,现在我们看看我们的测试程序怎么写,将测试工程中的 dlltest.cpp 文件改为如下

dlltest.cpp
#include iostream.h
#include windows.h
void main(void)
{
 HINSTANCE hInst;
 hInst=LoadLibrary(Dll2.dll);
 typedef int (FUNCPROC)(int a,int b);
 FUNCPROC ADD=(FUNCPROC)GetProcAddress(hInst,add);
 FUNCPROC SUB=(FUNCPROC)GetProcAddress(hInst,subtract);
 if(!ADD)
 {
  cout获取函数地址失败!endl;
  return;
 }
 cout3+5 = ADD(3,5)endl;
 cout5-3 = SUB(5,3)endl;
 FreeLibrary(hInst);  释放动态链接库 
}

编译运行,哦,别忘了将我们新编译处理的dll文件 dll2.dll 复制到我们的测试程序工程目录下.
运行结果如下
3+5 = 8
5-3 = 2
Press any key to continue

这里我们可以发现我们只拷贝了一个 dll2.dll 文件,
而没有拷贝我们的 dll2.lib ,这里是因为我们是动态加载的.
动态加载的好处是在我们需要的地方加载,这样可以节约内存空间.
如果是用隐式链接的方式,那么在我们启动程序时,多个dll库都要加载到内存中,
这时很浪费内存的,而且程序启动时间也很慢.

 

我们再来看看改变调用的方式,将dll2.cpp文件改变如下
dll2.cpp
int add(int a,int b)
{
 return a+b;
}
int subtract(int a,int b)
{
 return a-b;
}

编译一下,看看导出情况
E:/zieckey/CPP-study/Dll2/Debug>dumpbin -exports dll2.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file dll2.dll

File Type DLL

  Section contains the following exports for Dll2.dll

           0 characteristics
    456BF57D time date stamp Tue Nov 28 163821 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 0000100A add
          2    1 00001005 subtract

  Summary

        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text
这里就可以看到即使改变了调用方式,函数名字没有改变,和原来的一样.
好的,那么我们来看看测试程序,
当然,别忘了将我们新编译处理的dll文件 dll2.dll 复制到我们的测试程序工程目录下.
编译,没有错误,在运行时出现了错误,这是因为我们改变了调用的约定,
当然,在调用的时候也要遵循这样的约定,将测试工程中的 dlltest.cpp 文件改为如下

dlltest.cpp
#include iostream.h
#include windows.h
void main(void)
{
 HINSTANCE hInst;
 hInst=LoadLibrary(Dll2.dll);
 typedef int (_stdcall FUNCPROC)(int a,int b);
 FUNCPROC ADD=(FUNCPROC)GetProcAddress(hInst,add);
 FUNCPROC SUB=(FUNCPROC)GetProcAddress(hInst,subtract);
 if(!ADD)
 {
  cout获取函数地址失败!endl;
  return;
 }
 cout3+5 = ADD(3,5)endl;
 cout5-3 = SUB(5,3)endl;
 FreeLibrary(hInst);  释放动态链接库 
}
好了这样就行了.
我们再来看看现在编译出来的可执行程序 dlltest.exe 的导入信息.
E:/zieckey/CPP-study/DllTest/Debug>dumpbin -imports DllTest.exe
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.


Dump of file DllTest.exe

File Type EXECUTABLE IMAGE

  Section contains the following imports

    KERNEL32.dll
                43015C Import Address Table
                430028 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                  B4  FreeLibrary
                 13E  GetProcAddress
                 1C2  LoadLibraryA
                  CA  GetCommandLineA
                 174  GetVersion
                  7D  ExitProcess
                 22F  RtlUnwind
                 11A  GetLastError
                  1B  CloseHandle
                 2DF  WriteFile
                 218  ReadFile
                 26A  SetFilePointer
                 26D  SetHandleCount
                 152  GetStdHandle
                 115  GetFileType
                 150  GetStartupInfoA
                  51  DebugBreak
                 1AD  InterlockedDecrement
                 1F5  OutputDebugStringA
                 1B0  InterlockedIncrement
                 124  GetModuleFileNameA
                 29E  TerminateProcess
                  F7  GetCurrentProcess
                 2AD  UnhandledExceptionFilter
                  B2  FreeEnvironmentStringsA
                  B3  FreeEnvironmentStringsW
                 2D2  WideCharToMultiByte
                 106  GetEnvironmentStrings
                 108  GetEnvironmentStringsW
                 126  GetModuleHandleA
                 109  GetEnvironmentVariableA
                 175  GetVersionExA
                 19D  HeapDestroy
                 19B  HeapCreate
                 19F  HeapFree
                 2BF  VirtualFree
                  AA  FlushFileBuffers
                 1B8  IsBadWritePtr
                 1B5  IsBadReadPtr
                 1A7  HeapValidate
                 27C  SetStdHandle
                 241  SetConsoleCtrlHandler
                  BF  GetCPInfo
                  B9  GetACP
                 131  GetOEMCP
                 199  HeapAlloc
                 2BB  VirtualAlloc
                 1A2  HeapReAlloc
                 28B  SetUnhandledExceptionFilter
                 1B2  IsBadCodePtr
                 1E4  MultiByteToWideChar
                 1BF  LCMapStringA
                 1C0  LCMapStringW
                 153  GetStringTypeA
                 156  GetStringTypeW

  Summary

        6000 .data
        1000 .idata
        2000 .rdata
        2000 .reloc
       27000 .text
      
这里就看不到我们的 dll2.dll 的信息了.因为我们时动态加载

我们看到dll2.dll库的信息有如下
          1    0 0000100A add
          2    1 00001005 subtract
我们可以通过序号调用这两个函数,这里我们用到了 MAKEINTRESOURCE     
将测试工程中的 dlltest.cpp 文件改为如下

dlltest.cpp
#include iostream.h
#include windows.h
void main(void)
{
 HINSTANCE hInst;
 hInst=LoadLibrary(Dll2.dll);
 typedef int (_stdcall FUNCPROC)(int a,int b);
 FUNCPROC ADD=(FUNCPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));
 FUNCPROC SUB=(FUNCPROC)GetProcAddress(hInst,MAKEINTRESOURCE(2));
 if(!ADD)
 {
  cout获取函数地址失败!endl;
  return;
 }
 cout3+5 = ADD(3,5)endl;
 cout5-3 = SUB(5,3)endl;
 FreeLibrary(hInst);  释放动态链接库 
}
好了这样就行了. 
但是我们应该通过函数名来调用dll库中的函数.
因为这个让程序可读性更好,出错几率也小些

对于动态链接库的可选程序入口点时 DllMain
BOOL WINAPI DllMain(
  HINSTANCE hinstDLL,   handle to DLL module
  DWORD fdwReason,      reason for calling function
  LPVOID lpvReserved    reserved
);

这里我们看到通过模块化生成动态链接库是比较好的选择.


一点小技巧:
VC的bin目录下的 VCVARS32.BAT 批处理程序就是建立VC的开发环境的.
在命令提示符下的粘贴和拷贝
粘贴:点击鼠标右键粘贴就可以了
复制:点击鼠标右键选择标记,然后用鼠标左键标记一段,单击鼠标左键,那么就将相应内容复制到剪贴板下了.

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值