原文地址:www.tenouk.com/ModuleCC.ht…
原文作者:www.tenouk.com/
发布时间:约2004年前后
我们在这个模块里有什么?
- 动态链接库和Windows APIs 故事和示例
- 动态链接的类型
- 负载时间动态链接
- 运行时动态链接
- DLL和内存管理
- 动态链接的优势
- 动态链接库入口点功能
- 调用进入点函数
- 入门点功能定义
- 输入点函数返回值
- 动态链接库创建
- 创建源文件
- 输出函数
- 使用__declspec(dllexport)从DLL中导出。
- 导出C函数在C或C++语言可执行文件中使用。
- 创建一个导入库
- 动态链接库更新
- 动态链接库重定向
- 动态链接库数据
- 可变范围
- 动态内存分配
- 线程本地存储
我的训练时间:zz小时。在你开始之前,请阅读这里的一些说明。DLL的Windows MFC编程(GUI编程)可参见MFC GUI编程步骤教程。
应该掌握的Win32技能。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的DLL。
- 能够创建DLL的导出和导入函数。
动态链接库与Windows API的故事与实例。
注:本模块中提供的部分信息与上一模块重复。Tenouk在此表示歉意。
动态链接库(DLL)是一个模块,它包含了可以被另一个模块(应用程序或另一个DLL)使用的函数和数据。一个DLL可以定义两种函数。
- Exported - 是为了被其他模块调用,也可以从定义它们的DLL中调用。
- 内部函数。- 通常是为了只从定义它们的DLL内部调用。
虽然一个DLL可以导出数据,但它的数据一般只被它的函数使用。但是,并不能阻止另一个模块对该地址进行读写。DLL提供了一种将应用程序模块化的方法,因此功能可以更容易地更新和重用。当几个应用程序同时使用相同的功能时,它们还有助于减少内存开销,因为虽然每个应用程序都得到自己的数据副本,但它们可以共享代码。
Windows应用程序编程接口(API)是以一组动态链接库的形式实现的,因此任何使用Windows API的进程都会使用动态链接。动态链接允许一个模块在加载时或运行时只包含定位导出的DLL函数所需的信息。动态链接不同于我们更熟悉的静态链接,在静态链接中,链接器将库函数的代码复制到每个调用它的模块中。
动态链接的类型
有两种方法可以调用DLL中的函数。
-
在加载时动态链接中,一个模块(应用程序或其他模块)对导出的DLL函数进行显式调用,就像它们是本地函数一样。这需要将模块与包含函数的DLL的导入库进行链接。导入库为系统提供加载DLL所需的信息,并在应用程序加载时定位导出的DLL函数。
-
在运行时动态链接中,模块(应用程序或另一个模块)在运行时使用LoadLibrary()或LoadLibraryEx()函数加载DLL。DLL加载完毕后,模块调用GetProcAddress()函数来获取导出的DLL函数的地址。模块使用GetProcAddress()返回的函数指针调用导出的DLL函数。这样就不需要导入库了。
加载时动态链接
当系统启动一个使用加载时动态链接的程序时,会利用链接器放在文件中的信息来定位进程所使用的DLL的名称。然后系统依次在以下位置搜索DLLs。
- 应用程序加载的目录
- 当前目录。
- 系统目录。使用GetSystemDirectory()函数来获取该目录的路径。
- 16位系统目录。没有获取该目录路径的函数,但会搜索该目录。对于Windows Me/98/95:该目录不存在。
- Windows目录。使用GetWindowsDirectory()函数来获取该目录的路径。
- PATH环境变量中列出的目录。在命令提示符下输入PATH命令,可确认目录。
对于Windows Server 2003,Windows XP SP1:HKLM/System/CurrentControlSet/Control/Session Manager/SafeDllSearchMode的默认值为1(在系统和Windows目录之后搜索当前目录)。对于Windows XP。 如果HKLMSystem/CurrentControlSet/Control/Session Manager/SafeDllSearchMode为1,则在系统和Windows目录之后搜索当前目录,但在PATH环境变量中的目录之前搜索。默认值是0(在系统和Windows目录之前搜索当前目录)。请注意,这个值在加载时是以每个进程为基础进行缓存的。
如果系统无法找到所需的DLL,则会终止进程,并显示一个向用户报告错误的对话框。否则,系统会将DLL映射到进程的虚拟地址空间,并递增DLL引用计数。系统调用入口点函数。该函数接收到一个代码,表示进程正在加载DLL。如果切入点函数没有返回TRUE,系统将终止进程并报告错误。最后,系统用导入的DLL函数的起始地址修改函数地址表。DLL在初始化过程中被映射到进程的虚拟地址空间中,只有在需要时才加载到物理内存中。
运行时动态链接
当应用程序调用LoadLibrary()或LoadLibraryEx()函数时,系统尝试使用加载时动态链接中使用的相同搜索序列来定位DLL。如果搜索成功,系统将DLL模块映射到进程的虚拟地址空间,并递增引用计数。如果对LoadLibrary()或LoadLibraryEx()的调用指定了一个DLL,其代码已经映射到调用进程的虚拟地址空间中,该函数只是返回一个DLL的句柄,并递增DLL的引用计数。请注意,两个基本文件名和扩展名相同但在不同目录下的DLL不被认为是同一个DLL。
系统在调用LoadLibrary()或LoadLibraryEx()的线程的上下文中调用入口点函数。如果DLL已经被进程通过调用LoadLibrary()或LoadLibraryEx()加载,而没有相应的FreeLibrary()函数的调用,则不调用入口点函数。
如果系统找不到DLL,或者入口点函数返回FALSE,LoadLibrary()或LoadLibraryEx()返回NULL。如果LoadLibrary()或LoadLibraryEx()成功,它将返回一个DLL模块的句柄。进程可以在调用GetProcAddress()、FreeLibrary()或FreeLibraryAndExitThread()函数时使用这个句柄来识别DLL。
GetModuleHandle()函数返回一个在GetProcAddress()、FreeLibrary()或FreeLibraryAndExitThread()中使用的句柄。GetModuleHandle()函数只有在DLL模块已经通过加载时链接或通过之前对LoadLibrary()或LoadLibraryEx()的调用映射到进程的地址空间时才会成功。与LoadLibrary()或LoadLibraryEx()不同的是,GetModuleHandle()不会递增模块引用计数。GetModuleFileName()函数检索与GetModuleHandle()、LoadLibrary()或LoadLibraryEx()返回的句柄相关联的模块的完整路径。
进程可以使用GetProcAddress()来获取DLL中使用LoadLibrary()或LoadLibraryEx()、GetModuleHandle()返回的DLL模块句柄的导出函数的地址。当不再需要DLL模块时,进程可以调用FreeLibrary()或FreeLibraryAndExitThread()。这些函数会递减模块引用计数,如果引用计数为零,则从进程的虚拟地址空间中解映射DLL代码。运行时动态链接使进程能够继续运行,即使DLL不可用。然后,进程可以使用另一种方法来完成其目标。例如,如果一个进程无法找到一个DLL,它可以尝试使用另一个DLL,或者它可以通知用户一个错误。如果用户能够提供缺失的DLL的完整路径,即使DLL不在正常的搜索路径中,进程也可以利用这个信息来加载该DLL。这种情况与加载时链接形成了鲜明的对比,在加载时链接中,如果系统找不到DLL,就会简单地终止进程。如果DLL使用DllMain()函数为进程的每个线程执行初始化,运行时动态链接可能会引起问题,因为在调用LoadLibrary()或LoadLibraryEx()之前存在的线程不会调用入口点。
DLLs和内存管理
每个加载DLL的进程都会将其映射到它的虚拟地址空间中。进程将DLL加载到其虚拟地址后,就可以调用导出的DLL函数。系统为每个DLL维护一个每线程的引用计数。当一个线程加载DLL时,引用计数会递增1。当进程终止时,或者当参考计数变为零时(仅运行时动态链接),DLL将从进程的虚拟地址空间中卸载。像任何其他函数一样,导出的DLL函数在调用它的线程的上下文中运行。因此,以下条件适用。
- 调用DLL的进程的线程可以使用DLL函数打开的句柄。同样,调用进程的任何线程打开的句柄也可以在DLL函数中使用。
- DLL使用调用线程的栈和调用进程的虚拟地址空间。
- DLL从调用进程的虚拟地址空间中分配内存。
动态链接的优点
与静态链接相比,动态链接具有以下优点。
- 在同一基址加载同一DLL的多个进程在物理内存中共享一个DLL的副本。这样做可以节省系统内存,减少交换。
- 当DLL中的函数发生变化时,只要函数参数、调用约定和返回值不发生变化,使用这些函数的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- DLL可以提供后市场支持。例如,一个显示驱动DLL可以被修改为支持应用程序最初出厂时不可用的显示器。
- 用不同编程语言编写的程序可以调用相同的DLL函数,只要这些程序遵循函数使用的相同调用约定。调用惯例(如C、Pascal或标准调用)控制了调用函数必须将参数推到堆栈上的顺序,是函数还是调用函数负责清理堆栈,以及是否有任何参数在寄存器中传递。更多的信息,请参见编译器附带的文档。
使用DLL的一个潜在的缺点是应用程序不是自足的,它依赖于一个单独的DLL模块的存在。如果使用加载时动态链接的进程需要一个在进程启动时没有找到的DLL,系统就会终止该进程,并给用户一个错误信息。在这种情况下,系统不会终止使用运行时动态链接的进程,但缺失的DLL所导出的函数对程序不可用。
动态链接库入口点函数
DLL可以选择性地指定一个切入点函数,如果存在,系统会在进程或线程加载或卸载时调用切入点函数。如果存在,每当一个进程或线程加载或卸载DLL时,系统就会调用该入口点函数。它可以用来执行简单的初始化和清理任务。例如,它可以在创建新线程时设置线程本地存储,并在线程终止时清理它。
如果你将你的DLL与C运行时库链接,它可能会为你提供一个入口点函数,并允许你提供一个单独的初始化函数。查看你的运行时库的文档以了解更多信息。如果您提供了自己的入口点,请参见下一节中的DllMain()函数。DllMain()这个名字是用户定义函数的占位符。您必须在构建 DLL 时指定实际使用的名称。
调用入口点函数
每当发生以下任何一个事件时,系统就会调用入口点函数。
- 进程加载DLL。对于使用加载时动态链接的进程,DLL在进程初始化期间被加载。对于使用运行时链接的进程,DLL在LoadLibrary()或LoadLibraryEx()返回之前被加载。
- 进程会卸载DLL。当进程终止或调用FreeLibrary()函数,并且引用计数为零时,DLL被卸载。如果进程因TerminateProcess()或TerminateThread()函数而终止,系统不会调用DLL入口点函数。
- 在加载了DLL的进程中会创建一个新的线程。你可以使用DisableThreadLibraryCalls()函数来禁用线程创建时的通知。
- 已加载DLL的进程的线程正常终止,不使用TerminateThread()或TerminateProcess()。当进程卸载DLL时,入口点函数只对整个进程调用一次,而不是对进程的每个现有线程调用一次。你可以使用DisableThreadLibraryCalls()来禁止线程终止时的通知。
每次只有一个线程可以调用入口点函数。系统在引起调用函数的进程或线程的上下文中调用切入点函数,这允许DLL使用其切入点函数在调用进程的虚拟地址空间中分配内存或打开进程可访问的句柄。这使得DLL可以使用它的切入点函数在调用进程的虚拟地址空间中分配内存,或者打开进程可以访问的句柄。切入点函数还可以通过使用线程本地存储(TLS)为新线程分配私有的内存。
入门点函数定义
DLL入口点函数必须用标准调用约定(__stdcall
)来声明。如果没有正确声明 DLL 入口点,则 DLL 不会被加载,系统会显示一条消息,表明必须用 WINAPI 声明 DLL 入口点。对于Windows Me/98/95:如果DLL入口点声明不正确,则DLL未被加载,系统显示一条名为 "Error starting program "的消息,指示用户检查文件以确定问题所在。在函数的主体中,可以处理以下DLL入口点被调用的任意组合情况。
- 一个进程加载DLL(DLL_PROCESS_ATTACH)。
- 当前进程创建了一个新的线程(DLL_THREAD_ATTACH)。
- 一个线程正常退出(DLL_THREAD_DETACH)。
- 一个进程卸载DLL(DLL_PROCESS_DETACH)。
入口点函数应该只执行简单的初始化任务。它不能调用LoadLibrary()或LoadLibraryEx()函数(或调用这些函数的函数),因为这可能会在DLL加载顺序中产生依赖性循环。这可能导致DLL在系统执行其初始化代码之前就被使用。同样,入口点函数不能调用FreeLibrary()函数(或调用FreeLibrary()的函数),因为这可能会导致在系统执行其终止代码后使用DLL。
调用Kernel32.dll中的其他函数是安全的,因为当调用切入点函数时,保证这个DLL会被加载到进程地址空间中。通常情况下,entry-point函数会创建同步对象,如关键部分和mutexes,并使用TLS。不要调用注册表函数,因为它们位于Advapi32.dll中。如果你正在与C运行时库进行动态链接,不要调用malloc(),而是调用HeapAlloc()。
调用Kernel32.dll以外的导入函数可能会导致难以诊断的问题。例如,调用User、Shell和COM函数可能会导致访问违规错误,因为它们的DLL中的一些函数会调用LoadLibrary()来加载其他系统组件。下面的例子演示了如何构造DLL入口点函数。
#include <windows.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved) // reserved
{
// Perform actions based on the reason for calling.
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
// Successful DLL_PROCESS_ATTACH.
return TRUE;
}
输入点函数返回值
当因为进程正在加载而调用DLL入口点函数时,该函数返回TRUE表示成功。对于使用加载时链接的进程,返回值为FALSE会导致进程初始化失败,进程终止。对于使用运行时链接的进程,返回值为FALSE会导致LoadLibrary()或LoadLibraryEx()函数返回NULL,表示失败。系统会立即用DLL_PROCESS_DETACH调用你的入口点函数,并卸载DLL。当该函数因其他原因被调用时,入口点函数的返回值将被忽略。
动态链接库的创建
要创建一个动态链接库(DLL),你必须创建一个或多个源代码文件,也可能创建一个用于导出函数的链接器文件。如果您计划允许使用您的 DLL 的应用程序使用加载时动态链接,您还必须创建一个导入库。
创建源代码文件
DLL的源文件包含导出的函数和数据,内部函数和数据,以及DLL的可选入口点函数。您可以使用任何支持创建基于Windows的DLL的开发工具。如果你的DLL可能被一个多线程的应用程序使用,你应该使你的DLL成为 "线程安全"。你必须同步访问DLL的所有全局数据以避免数据损坏。你还必须确保你只链接到线程安全的库。例如,Microsoft速Visual C++速包含了多个版本的C运行时库,一个是不线程安全的,两个是线程安全的。更多细节请参考模块A。
导出函数
如何指定DLL中的哪些函数应该被导出取决于你所使用的开发工具。一些编译器允许你通过在函数声明中使用修饰符直接在源代码中导出函数。其他时候,你必须在传递给链接器的文件中指定导出。例如,使用Visual C++,有两种可能的方法来导出DLL函数。
- 用
__declspec
修饰符或者是... - 用一个.def文件。
如果你使用 __declspec
修饰符,就没有必要使用 .def 文件。
使用__declspec(dllexport)
从DLL中导出。
.DLL文件的布局与.EXE文件非常相似,但有一个重要的区别:DLL文件包含一个导出表。导出表包含DLL向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL中的任何其他函数都是DLL的私有函数。可以通过DUMPBIN工具的/EXPORTS选项来查看DLL的导出表。您可以使用两种方法从DLL中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建DLL时使用该.DEF文件。如果你想从 DLL 中按序号而不是按名称导出函数,请使用这种方法。
- 在函数的定义中使用关键字__declspec(dllexport)。
当用这两种方法导出函数时,一定要使用 __stdcall
的调用约定。微软在Visual C++的16位编译器版本中引入了__export
,允许编译器自动生成导出名,并将它们放在一个.LIB文件中。然后,这个.LIB文件可以像静态.LIB一样与DLL链接使用。
在32位编译器版本中,你可以使用__declspec(dllexport)
关键字从DLL中导出数据、函数、类或类成员函数。__declspec(dllexport)
将导出指令添加到对象文件中,因此你不需要使用一个.DEF 文件。
这种便利性在试图导出装饰的 C++ 函数名时最为明显。没有标准的名称装饰规范,所以导出的函数名可能会在不同的编译器版本之间发生变化。如果你使用 __declspec(dllexport)
,重新编译 DLL 和依赖的 .EXE 文件是必要的,只是为了考虑到任何命名约定的变化。
许多导出指令,例如 ordinals、NONAME 和 PRIVATE,只能在 .DEF 文件中进行,没有 .DEF 文件就无法指定这些属性。然而,除了使用 .DEF 文件之外,使用 __declspec(dllexport)
不会导致构建错误。要导出函数,__declspec(dllexport)
关键字必须出现在调用约定关键字的左边,如果有指定关键字的话。例如:
__declspec(dllexport) void __cdecl FunctionName(void);
要导出一个类中所有的公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示。
class __declspec(dllexport) CExampleExport : public CObject
{ ... class definition ... };
当构建你的 DLL 时,你通常会创建一个头文件,其中包含你要导出的函数原型和/或类,并在头文件的声明中添加 __declspec(dllexport)
。为了使你的代码更易读,为 __declspec(dllexport)
定义一个宏,并对你要导出的每个符号使用这个宏。例如:
#define DllExport __declspec(dllexport)
__declspec(dllexport)
在 DLL 的导出表中存储函数名。当把 DLL 源代码从 Win16 移植到 Win32 时,用 __declspec(dllexport)
替换 __export
的每个实例。作为参考,在Win32 WINBASE.H头文件中搜索__declspec(dllimport)
的使用实例。
导出C函数在C或C++语言可执行文件中的用途
如果你在用 C 语言编写的 DLL 中有一些函数,而你想从 C 语言或 C++ 语言的模块中访问这些函数,你应该使用 __cplusplus
预处理程序宏来确定正在编译哪种语言,如果是从 C++ 语言的模块中使用这些函数,则用 C 链接来声明这些函数。如果你使用这种技术,并为你的DLL提供头文件,这些函数可以被C和C++用户使用,而不会有任何改变。
下面的代码显示了一个可以被C和C++客户端应用程序使用的头文件。
// MyCFuncs.h
#ifdef __cplusplus
extern "C" { // only need to export C interface if used by C++ source code
#endif
__declspec(dllimport) void MyCFunc();
__declspec(dllimport) void AnotherCFunc();
#ifdef __cplusplus
}
#endif
如果你需要将C函数链接到你的C++可执行文件中,而函数声明头文件没有使用上述技术,在C++源文件中,做如下操作,以防止编译器装饰C函数名。
extern "C" {
#include "MyCHeader.h"
}
创建一个导入库
一个导入库(.lib)文件包含了链接器所需的信息,以解析对导出的DLL函数的外部引用,因此系统可以在运行时定位指定的DLL和导出的DLL函数。例如,要调用CreateWindow()函数,你必须将你的代码与导入库User32.lib链接。原因是CreateWindow()驻留在一个名为User32.dll的系统DLL中,而User32.lib是用于解析你的代码中对User32.lib中导出函数的调用的导入库。链接器会创建一个包含每个函数调用地址的表。当加载DLL时,对DLL中函数的调用将被固定起来。当系统在初始化进程时,它会加载User32.dll,因为进程依赖于该DLL中的导出函数,它更新函数地址表中的条目。所有对CreateWindow()的调用都会调用User32.dll中导出的函数。警告。 在DLL中调用ExitProcess()函数可能导致意外的应用程序或系统错误。只有当您知道哪些应用程序或系统组件将加载 DLL,并且在此上下文中调用ExitProcess()是安全的,才能确保从 DLL 中调用 ExitProcess()。
动态链接库更新
有时需要用较新的版本替换 DLL。在替换 DLL 之前,请执行版本检查,以确保您是在用较新的版本替换旧版本。可以替换正在使用的 DLL。替换正在使用的 DLL 的方法取决于您使用的操作系统。在 Windows XP 和更高版本上,应用程序应使用隔离应用程序和并排装配体。如果执行以下步骤,则无需重新启动计算机。
- 使用MoveFileEx()函数重命名被替换的DLL。不要指定MOVEFILE_ALLOWED,并确保重命名的文件在包含原始文件的同一卷上。你也可以简单地重命名同一目录下的文件,给它一个不同的扩展名。
- 将新的 DLL 复制到包含重命名 DLL 的目录中。现在所有的应用程序都将使用新的DLL。
- 使用带有MOVEFILE_DELAY_UNTIL_REBOOT的MoveFileEx()来删除重命名的DLL。
在你进行这个替换之前,应用程序将使用原来的DLL,直到它被卸载。在你进行替换之后,应用程序将使用新的DLL。当你编写一个DLL时,你必须注意确保它已经为这种情况做好了准备,特别是当DLL维护全局状态信息或与其他服务通信时。如果 DLL 没有为全局状态信息或通信协议的更改做好准备,更新 DLL 将需要您重新启动计算机,以确保所有应用程序都使用相同版本的 DLL。对于Windows Me/98/95:因为不支持MoveFileEx(),所以需要重新启动计算机。
动态链接库重定向
当应用程序加载的DLL版本与出厂时的版本不同时,可能会出现问题。从Windows 2000开始,您可以通过创建一个重定向文件来确保您的应用程序使用DLL的正确版本。重定向文件的内容会被忽略,但它的存在会强制应用程序目录中的所有DLL从该目录加载。
重定向文件必须命名为:appname.local。
例如,如果应用程序的名称是editor.exe,重定向文件就命名为editor.exe.local。你必须将itor.exe.local安装在包含editor.exe的同一目录中。你也必须在同一目录下安装DLLs。如果存在重定向文件,LoadLibrary()和LoadLibraryEx()函数会改变其搜索顺序。如果指定了路径,并且存在应用程序的重定向文件,这些函数就会在应用程序的目录中搜索DLL。如果DLL存在于应用程序的目录中,这些函数忽略指定的路径,从应用程序的目录中加载DLL。如果模块不在应用程序的目录中,这些函数从指定的目录中加载DLL。例如,一个应用程序c:\myapp\myapp.exe使用以下路径调用LoadLibrary()。
c:program files/common files/system/mydll.dll
如果c:\myapp\myapp.exe.local和c:\myapp\mydll.dll存在,LoadLibrary()将加载c:\myapp\mydll.dll。否则,LoadLibrary()将加载c:program files\common files\system\mydll.dll。在包含应用程序的同一目录下安装应用程序的DLLs是一个很好的做法,即使你不使用重定向。它可以确保安装你的应用程序不会覆盖DLL的其他副本并导致其他应用程序失败。此外,其他应用程序也不会覆盖您的 DLL 副本而导致应用程序失败。
动态链接库数据
动态链接库(DLL)可以包含全局数据或本地数据。
变量范围
DLL变量的默认范围与应用程序中声明的变量的范围相同。DLL源代码文件中的全局变量对使用DLL的每个进程都是全局的。静态变量的作用域限于它们被声明的块。因此,默认情况下,每个进程都有自己的DLL全局变量和静态变量实例。您的开发工具可能允许您覆盖全局变量和静态变量的默认范围。
动态内存分配
当DLL使用任何一个内存分配函数(GlobalAlloc()、LocalAlloc()、HeapAlloc()和VirtualAlloc())分配内存时,内存是在调用进程的虚拟地址空间中分配的,并且只有该进程的线程可以访问。DLL可以使用文件映射来分配可以在进程间共享的内存。
线程本地存储
线程本地存储(TLS)函数使DLL能够为多线程进程的每个线程分配一个索引,用于存储和检索不同的值。例如,一个电子表格应用程序可以在用户每次打开一个新的电子表格时创建一个新的同一线程实例。为各种电子表格操作提供函数的DLL可以使用TLS来保存每个电子表格的当前状态信息(行、列等)。
警告。 Visual C++编译器支持一种语法,可以让你声明线程本地变量。__declspec(thread)
。如果你在一个DLL中使用这个语法,你将不能使用LoadLibrary()或LoadLibraryEx()显式加载DLL。如果你的DLL将被显式加载,你必须使用线程本地存储函数来代替__declspec(thread)
。
进一步的阅读和挖掘。
-
Microsoft Visual C++,在线MSDN。
-
结构、枚举、联合和typedef故事可以参考C/C++结构、枚举、联合和typedef。
-
多字节、Unicode字符和本地化请参考Locale、宽字符和Unicode(Story)和Windows用户与组编程教程(Implementation)。
-
Windows数据类型是Windows数据类型。
通过www.DeepL.com/Translator(免费版)翻译