关于Windows API、CRT和STL二三事

时间:2021-06-10 16:11:11

1.本文编写目的
    本文是为了帮助一些人弄清一些关于Windows API, C运行时程序库(CRT), 和标准C++库(STL)的基本概念。有很多人甚至是有经验的程序员在这些概念上是含糊不清的甚至是有错误观点。如果你想知道他们是在什么基础上实现的并且一直没时间弄清楚这些概念,请花费点时间阅读下本文。
2. 基本概念
   下面这幅图代表了WinAPI、CRT和STL三者之间的关系。
    表 1: Windows API、CRT和C++标准库之间的关系图
关于Windows API、CRT和STL二三事 
    相邻的模块之间可以相互通讯。为什么这么说呢?下面依照这幅图从下到上依次说说明。
2.2. 硬件层
    每个硬件部分都有自己的命令集,操作系统通过这些命令集控制硬件并与硬件通信。不同硬件部分的命令集数量和复杂度差别很大。通常,同样的硬件如果由不同的硬件厂商实现,它们通常会在标准的命令集外提供一些扩展,使用这些扩展通常可以发挥这些硬件的特性。如果每个程序编写者都针对这些硬件重写一遍程序,每个硬件的生产厂家都有很多,那会把码农们累死滴!于是上帝说,要有操作系统,于是人类 有了一个方便的访问硬件的统一平台。
2.3 操作系统
    操作系统的目的之一就是把底层硬件的特性给封装起来,然后提供一个统一的接口,以便于计算机操作人员控制硬件。现代版本的操作系统不再允许应用程序直接访问硬件,操作系统能直接访问硬件层时,称操作系统处于核心态(kernel mode)。
    一些老版本的操作系统,如MS-DOS,允许编程人员直接访问硬件。虽然这样做可以使他们写出来的程序能够在一段短的时间内应用硬件的某些高级特性,但是长远来说这样做对这个程序是有害的,因为这个程序将不能在这个硬件的新版本上运行。
2.4 应用程序编程接口(Application Programming Interface)
    现代的应用程序如果想访问硬件,就必须通过操作系统,具体来说就是使用操作系统提供的API。一个API就是一些功能的统一接口,它能把硬件的这些功能抽象地统一起来,让程序员集中精力实现他们想要的功能。应用程序已经不可能绕过现代的操作系统直接访问硬件,一种更普遍的说法就是应用程序以用户态(user mode)运行。MS Windows提供的API是一群C函数的集合。Windows平台的最低层次的开发语言就是C语言。
2.4.1 平台软件开发包(Platform Software Development Kit)
    MS发布了一个免费的平台软件开发包(简称之为Platform SDK或PSDK)以促使软件开发员在windows平台上开发软件。PSDK包含如下东东:
   1. 包含这些API生命的头文件
   2. 用于连接的相应的库文件(通过它们能够在相应的DLL中定位到这些API函数)
   3. 相关的说明文档
   4. 各种帮助工具
    例如,如果要打开或创建一个文件,我们要调用函数CreateFile,它在头文件"WinBase.h"中声明,通过库文件"Kernel32.lib"在dll文件中进行函数定位。
    Windows API的函数名称依照骆驼命名法命名,因此不同的函数很容易区分。常量和宏的命名一般是大写。在相应的文档里,会详细的说明每个函数所需要的头文件、库文件以及能够运行的相关平台。
    一个Windows应用程序能够调用任何windows的API函数,前提是应用程序能够正确的找到这个函数的签名并且能够链接到相应的库文件,或者通过函数GetProcAddress和函数签名(一般就是函数的名称)直接连接到它在dll库文件中的地址。
2.5 C运行时程序库
    基于操作系统的API函数,软件厂商实现了C运行时程序库(CRT)。CRT由一些头文件和相应的源文件构成,这些文件实现了一些基础的公共操作,如字符串操作、一些数学运算函数和基本的输入/输出等操作。通常的,如果一家厂商发布了一个C编译器,它会附带一些CRT库。一些国际标准化组织负责制定C语言的标准并实现一些运行时程序库。
2.5.1 标准和扩展
    理论上,如果一个程序使用标准C语言开发的,并且有一个平台支持其相应的标准C编译器和动态库,那么这个程序就能在这个平台上运行。但是实际上每个C编译器开发厂家都会对C语言做一些有利于自己的扩展,程序员可以很方便的利用这些扩展开发程序,但是付出的代价就是这个程序不再具有可移动性。
    CRT的函数名称一般都是小写的,宏和常数是大写的,而一些扩展则通常具有一定的以下划线作为开头的标识,例如函数_mkdir。针对这些扩展,相应的文档都会详细的予以说明。
2.6 关于Unicode的认知
2.6.1 PSDK支持Unicode
    实际上,以上提到的Win32 API的名称并不是它们的真实名称。这些名称仅仅是一些宏,你可以在PSDK的头文件中找到这些宏对用的函数名称。所以,如果PSDK的文档提到一个函数,如CreateFile,开发人员应该意识到它仅仅是一个宏。它的真实名称是CreateFileA和CreateFileW。是的,它代表了“两个”函数名,而不是一个,是同一个函数在不同Win32函数的两个不同的版本。以'A'结尾的函数接受ANSI字符串,即Unicode字符串,即wchar_ts型字符串。两种版本的函数都在模块kernel32.dll中实现,如果你的编程环境是Unicode则,则宏CreateFile在编译是会被CreateFileW代替,否则用CreateFileA代替。
    Windows操作系统有三大家族:MS-DOS/9x-based,Windows CE,Windows NT。
       1. MS-DOS/9x-based系列,包括了Windows 1.0-3.11,95,98和Windows ME都给予MS-DOS操作系统。Windows操作系统的早期版本:1.0-2.0和16-bit操作系统。而其后的操作系统,3.0,95,98和ME则是16位和32位操作系统的混合体。它们实际最低支持16位的操作环境,也能运行一些受限制的32位应用程序。之所以受限制,就是它们只支持ANSI版本的win32函数。现代这个系列的操作系统已经绝迹了,并且微软不再维护这些系统。
       2. Windows NT系列始自于上世界90年代的Window NT 3.1,并且包括了以后的Windows NT 4,Windows 2000,Windows XP,Window Vista,以及这些系统的server版本。Windows NT系列是真正的32位操作系统。它们既支持Unicode版本的win32函数,也支持ANSI版本的win32函数。NT系列的操作系统内部的字符串是Unicode型字符,ANSI型的Win32 API函数实际上是Unicode版本的函数的包装。
       3. Windows CE系列则是针对移动和嵌入式设备开发的系统。它们是32位操作系统。Windows CE只支持Unicode版Win32 API。
2.6.2 PSDK的字符串解决方案:TCHARs
    为了避免为不同的windows操作系统开发不同版本的PSDK,微软制订了一个统一的字符串类型TCHARs。TCHAR以及其他的相应的宏在头文件WinNT.h中有定义。程序员在程序中不需要为使用char还是wchar_t而纠结,只需要使用宏TCHAR就可以了。根据Unicode环境是否存在,编译器会自动进行相应的转换。同样道理,程序员不需要为使用'A'还是'W'型Win32 API函数纠结。
// Generic code
//
LPCTSTR psz = TEXT("Hello World!");
TCHAR szDir[MAX_PATH] = { 0 };
GetCurrentDirectory(MAX_PATH, szDir);

// 如果UNICODE符号没有被定义
//
const char* psz = "Hello World!";
char szDir[MAX_PATH] = { 0 };
GetCurrentDirectoryA(MAX_PATH, szDir);

//在纯32位操作系统上GetCurrentDirectoryA只是一个外包装,实际工作流程如下:
// 1. 分配一个临时的固定大小的wchar_t缓冲区。
// 2. 调用实际的工作者:GetCurrentDirectoryW.
// 3. 根据调用线程的active code page ,调用函数WideCharToMultiByte把wchar_t变为char字符串。
//    如果一个字符串不能找到对应的符号,则用符号'?'代替。

// 如果宏UNICODE被定义
//
const wchar_t* psz = L"Hello World!";
wchar_t szDir[MAX_PATH] = { 0 };
GetCurrentDirectoryW(MAX_PATH, szDir);
//直接调用实际的干活的,不需要先找包工头这个中间人
    使用TCHAR可以方便程序员为ANSI和Unicode builds两种编译环境写一份代码。在今天这个时代,你不可能为旧有的操作系统Windows 9x/Me编写程序,你可以安全的使用Unicode字符串。这样的一个好处就是Unicode应用程序可以忘记code pages hustle。
    一个记住PSDK字符串声明的简单办法如下:
                L P C T STR = const TCHAR* 
                ^ ^ ^ ^ ^ 
                | | | | | 
    Long -------+ | | | | 
    Pointer to ---+ | | | 
    Constant -------+ | | 
    TCHAR ------------+ | 
    STRing -------------+

有时候L(即"Long")被忽略掉,因为long和short型指针在Win32平台上的区别是过时的。所以PTSTR = "pointer to TCHAR string",也即TCHAR*。
    下面是同一个程序在两种不同运行环境下的运行结果。第一个是在ANSI环境下的运行结果,第二个则是在Unicode环境下的运行结果。

关于Windows API、CRT和STL二三事 
    20世纪的幼稚的ANSI程序,把一切非英语字符都转换为'?'。

关于Windows API、CRT和STL二三事 
    现代的Unicode程序则可以识别其他语言的字符串。
2.6.3 CRT的字符串解决方案:_TCHARs
    PSDK采用了一种泛型文字转换技术,它能够根据具体的CRT环境实现文字类型转换。CRT用了一个额外的头文件"tchar.h"来实现这种泛型技术。为了和C语言标准兼容,所有非标准的名称都已下划线开头。所以,CRT使用宏_T来代替在文件"WinNT.h"中定义的宏TEXT()。CRT的开发者为了使得这个库能够广泛的应用开来,所以目前CRT能识别三种字符集:
    * SBCS - 单字节字符集。这类字符集的字符类型是char。一个char元素存储一个ASCII字符。不必为每个单独的工程定义一个字符集。这个字符集来源于上世纪70年代的C语言,0x00 - 0x7F留给英语字符集合,而剩余的0x80 - 0xFF就留给非英语字符集合使用,具体的这个集合中每个字符代表什么含义则由当前的active code page决定。
    * _MBCS - 多字节字符集。多字节string字符集也依赖char型字符类型。 一个多字节字符可能占用一个或两个char型元素空间存储。 如果要用多字节字符集,就需要在工程中定义宏_MBCS。宏_MBCS向后兼容SBCS模式,并且MS Visual C++ 8.0(2005)以前的版本都默认使用这种字符集。东亚语言,如日语、韩语和中文,一般采用这种字符集。现在,多字节字符集已经被Unicode字符集代替。过去在Windows 9x/Me平台上,只有多字节字符集才能处理东亚语言。
    * _UNICODE -统一编码字符集。这种字符集以来wchar_t字符类型。一个Unicode字符占用一个wchar_t类型空间。windows上的wchar_t型一般占用16比特大小,所以在windows上可以使用使用大约65535个不同的字符。从MS Visual C++ 8.0 (2005)以后默认使用这种字符集。
    CRT使用_MBCS和_UNICODE两个宏来区分多字节字符集和Unicode字符集。
Diagram #2: The Generic Text Mapping in CRT
// Generic code; names are not standard, hence the leading underscore.
//
_TCHAR message[128] = _T("The time is: ");
_TCHAR* now = _tasctime(&tm);
_tcscat(message, now);
_putts(message);

// What happens if no symbol is defined at all (SBCS).
//
char message[128] = "The time is: ";
char* now = asctime(&tm);
strcat(message, now);
puts(message);

// What happens if _MBCS symbol is defined (Multi-byte Character Set);
// non-standard names are with the leading underscore.
//
char message[128] = "The time is: ";
char* now = asctime(&tm);
_mbscat(message, now);
puts(message);

// What happens if _UNICODE symbol is defined (Unicode Character Set);
// non-standard names are with the leading underscore.
//
wchar_t message[128] = L"The time is: ";
wchar_t* now = _wasctime(&tm);
wcscat(message, now);
_putws(message);
2.7 C++标准库
    C++编程语言有其自己的一套标准库。这些库包含了一系列的类和函数,可以在编程中使用。
    通常,人们一提起C++标准库就下意识地直接联想到STL。STL是标准模板库(Standard Template Library)的缩写。STL最近的版本是作为C++标准库的一个子集发布的。但是现实中STL已经无处不在了,成了C++标准库的同义语。
    国际标准化组织(IOS,International Organization for Standardization)负责定义C++语言的标准以及其标准库。
2.7.1 C++标准库的内容
    C++标准库大概可以划分为以下几个部分:
   1.容器。一般都是一些普通的数据结构,如vector,set,list,map.
   2.迭代器。提供了一种统一的访问标准容器的方法。
   3. 算法。实现了一些常用的算法,这些算法一般都通过迭代器访问容器,而不是直接访问容器,所以一种算法可以作用于不同的容器。
   4. 分配器。负责容器内每个元素的内存空间的分配与回收。
   5. 函数对象以及Utilities。利用他们便于算法作用于各种容器。
   6. 字符流。把输入/输出流作为一个统一的对象处理。
   7. C动态运行库。由于要向后兼容C,所以CRT要作为C++标准库的一部分出现。
2.8 多平台方面的进展
     有时候客观需求要求一个软件能在多个计算机平台上运行。程序员可能需要为某个特定的平台开发特定的软件版本,这种方法不仅过时,而且容易出错。因为相同的函数模块可能需要真多多种不同的平台实现不同的版本,多以不仅浪费时间而且非常浪费开发资源。
    所以一般的方法就是,编写的软件所基于的平台可以在不同的平台运行,它不能调用特定平台的API函数,而且不能使用特定的标准库开发商提供的标准库的扩展。这就使得开发非常困难,但是从长远来看,所有的平台能从软件的新特性以及改正的bug中受益。
3. 代码重利用
    有两种方式可以实现把CRT和C++库的代码嵌入一个项目中:静态链接和动态链接。以下针对这两种方式的讨论,只针对CRT介绍开来,但是这些概念在C++标准库上也是同义的或者说是近似的。
3.1 静态链接
    如果静态链接CRT/C++库,则程序启动时这些库的代码也一并会被加载紧内存中,这种方法的优点和缺点并存。
    优点:
   1. 使用方便。通过这种方式很容易把程序拷贝到目标计算机,并使它运行起来。不用为CRT/C++复杂的使用步骤担心。
   2. 不需要额外的文件。通过这种方式,一些小的应用程序可以很方便地把这些库放进一个可执行文件当中。这种程序可以很容易的从网上下载下来,不用担心它的完整性遭到破坏。
    缺点:
   1. 不耐用。通过静态链接方式实现的程序不能区分一个库的版本,即旧版本和新版本对它来说都是一样的。
   2. 静态链接容易导致多米诺效应。当下,一个程序不可能完全由一个组织去实现。现代的软件项目已经非常复杂,并且开始严重依赖第三方的构件和库。所以一个软件一般被划分为若干个耦合关系非常松散的模块。如果静态链接到CRT中的某一部分,将严重影响到这些模块之间的互操作性,这也将迫使程序员依赖于这些模块之间的最大的公共部分。下面这一章节将详细的讨论相关的细节。
3.1.1 把CRT当做一个黑盒子
    一个问题就是不同的CRT实例之间很难共享一个内置的CRT对象,一个CRT实例所分配的内存空间还必须由这个实例来释放,一个CRT实例所打开的文件也只能有这个实例来操作并完成关闭,等等。这是因为此时CRT只能内部操作这些获取的资源。任何一个CRT实例如果想释放另一个CRT实例的内存活通过FILE*指针访问另一个实例打开的文件都将改变被访问者的状态甚至导致被访问者的程序的崩溃。
    所以静态地使用CRT的程序员一般都要提供额外的函数来释放他所开发的模块所使用的资源,并且要求调用这个模块的人一定要记住通过这些函数来对这些资源进行释放,否则将造成内存泄露。如果不同的模块通过静态方式连接到CRT,则这些模块之间就不能共享STL的容器对象或C++对象。下表说明了通过调用malloc函数来使用一段内存缓存。

关于Windows API、CRT和STL二三事 
     上面这个表中,模块1以静态的方式连接到CRT,而模块2和3则以动态的方式连接到CRT,模块2和3之间可以相互传递CRT所用有的对象。例如,模块3使用malloc所分配的一段内存空间可以由模块2释放,因为对malloc和free的调用都是由同一个CRT对象实现的。
    但是,模块1的资源就很难由其他模块释放。模块1所占有的每个资源都应该由它自己来释放。因为模块1是以静态的方式链接到CRT的一个实例。上面图中的模块2就必须通过调用模块1所提供的函数来释放它通过模块1所获取的内存空间。
3.2 动态链接
    如果通过动态链接的方式链接到CRT/C++库,则程序运行时只需要把很小一部分需要的导入库链接到程序中。这些导入库包含了说明CRT/C++的相应的函数的具体的实现的地址所在。程序启动时,程序会根据这些指令的说明把一些相关的dll加载进程序的内存空间中来。
    优点:
   1.容易实现模块化。就像前面所述的那样,一个程序可以通过动态链接很方便的实现模块化。一个程序可以很方便的划分为若干个模块,并且各个模块之间可以很方便的共享比较高级的对象数据。
   2. 更快的启动。系统启动时已经加载了这些CRT DLL,所以当使用这些dll的程序启动时就无需在加载这些DLL了,而且这不仅节省了内存空间,更节省了页交换的时间。
    缺点:
   1.部署起来比较复杂。这些CRT库必须被重新被分配一遍,而且为了一个程序能够工作,这些库在内存中必须有序地放置。这就需要一个额外的启动项目,并且部署的时候要小心谨慎。
4. 总结
    本文大体上介绍了Windows API、CRT和STL相互之间的关系和依赖顺序。对于用户态的程序来说,Windows API是可以用到的计算机的最低一层。处于Windows API之上的则是C的动态运行库,它对操作系统进行了封装,并隐藏了不同的操作系统之间的差异。标准C++库则提供了更多的功能,并且把CRT作为它的一部分。通过标准的函数以及相关类可以写出跨平台的程序,这个程序只需在新的平台上重新编译一次发布出来,代码不需要改动。
    根据应用程序的客观需要,它可以静态活动态的链接到C运行时程序库和标准C++程序库。每种方法都有它自己的优缺点。