深入浅出Windows API程序设计:编程基础篇

978-7-115-56948-6
作者: 王端明
译者:
编辑: 陈聪聪杨海玲

图书目录:

详情

本书是Windows API程序设计的入门图书,提供大量的示例程序,主要介绍学习Windows程序设计必备的基础知识,以及一个程序界面所需的菜单、图标光标、子窗口控件、其他资源和对话框等相关内容,并通过Photoshop切片和自绘技术实现一个优雅的程序界面。通过阅读本书,读者可以对Windows程序设计有更加深入的认识,并将其应用到实际场景中。

图书摘要

版权信息

书名:深入浅出Windows API程序设计:编程基础篇

ISBN:978-7-115-56948-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    王端明

责任编辑 陈聪聪

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e56948”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

本书是Windows API程序设计的入门图书,提供大量的示例程序,主要介绍学习Windows程序设计必备的基础知识,以及一个程序界面所需的菜单、图标光标、子窗口控件、其他资源和对话框等相关内容,并通过Photoshop切片和自绘技术实现一个优雅的程序界面。通过阅读本书,读者可以对Windows程序设计有更加深入的认识,并将其应用到实际场景中。

本书适合对Windows API程序设计感兴趣的初学者以及Windows API技术开发人员阅读,也可以作为培训学校的教材使用。


2015年7月,Windows 10操作系统正式发行,新版本的操作系统在UI界面、安全性和易用性等方面都有了大幅提升。64位操作系统已经普及,但传统的Win32 API也属于Windows API。因为不管编译为32位还是64位的应用程序,使用的都是相同的API,只不过是扩展了一些64位数据类型。目前Microsoft Windows在操作系统市场中占据相当大的份额,读者学习Windows程序设计的需求非常迫切。但是遗憾的是,近年来国内可选的关于Windows API的图书较少。

使用Windows API是编写程序的一种经典方式,这一方式为Windows程序提供了优秀的性能、强大的功能和较好的灵活性,生成的执行代码量相对比较小,不需要外部程序库就可以运行。更重要的是,无论将来读者用什么编程语言来编写Windows程序,只要熟悉Windows API,就能对Windows的内部机理有更深刻、更独到的理解。

热爱逆向研究的读者都应该先学好Windows API程序设计,而初学Windows程序设计的读者可能会非常困惑。于是,在2018年年初,我产生了一个想法:总结我这10年的程序设计经验,为Windows开发人员写一本深入浅出的符合国内市场需求的图书。本来我计划用一年的时间撰写本书,可是没想到一写就是3年!

本书面向没有任何Windows API程序设计经验的读者,因此尽量做到通俗易懂。为了确保本书内容的时效性,MSDN是最主要的参考对象。我的初心就是把这10年的程序设计经验毫无保留地分享给读者,并帮助读者学会调试技术。另外,为了精简篇幅,大部分程序的完整源代码并没有写入书中。读者通过本书可以全面掌握Windows程序设计,对于没有涉及的问题也可以通过使用MSDN自行解决。

本书基于Windows 10和Visual Studio 2019(VS 2019)编写,并提供了大量的示例程序。首先介绍学习Windows程序设计必备的基础知识,并对可能用到的字符串处理函数做详细讲解。万事开头难。我从只有4行代码的最简单的HelloWorld程序开始,然后介绍具有标准Windows程序界面的HelloWindows程序。对于这两个入门程序的每一行甚至每个单词我都进行深入介绍,讲清楚其中的原理,让后面的学习水到渠成。接着,我会介绍Windows窗口程序、GDI绘图、键盘与鼠标以及计时器和时间等内容。然后,我会介绍一个程序界面所需的菜单、图标光标、位图、子窗口控件、对话框和其他资源等。最后,我会带领读者通过Photoshop切片和自绘技术实现一个优雅的程序界面。

(1)初学Windows程序设计的读者通过本书可以高效全面地掌握Windows程序设计。

(2)学习Windows程序设计多年但仍有困惑的读者通过本书可以系统地学习Windows程序设计的方方面面。

(3)其他任何爱好或需要学习Windows API程序设计的读者,通过本书可以更好地了解Windows API程序设计的基本技巧。

在阅读本书之前,读者必须熟悉C或C++语法。除此之外,不需要具备任何其他专业知识。

(1)可以加入我提供的QQ群进行学习交流。

(2)可以到我提供的Windows中文网的相应版块中进行提问,我通常会集中时间进行统一解答。

读者在学习完本书后,可以继续阅读《深入浅出Windows API程序设计:核心编程篇》,进一步理解Windows API程序设计,并将其应用到实际场景中。

本书并没有涉及内核方面的相关知识。如果读者需要学习Windows操作系统的内核安全编程技术,那么推荐阅读谭文和陈铭霖所著的《Windows内核编程》和《Windows内核安全与驱动开发》。

本书可以成功出版,得益于多位专业人士的共同努力。感谢家人的无条件支持,感谢微软以及CSDN的朋友、15PB信息安全教育创始人任晓珲、《Windows内核编程》的作者陈铭霖、《Windows环境下32位汇编语言程序设计》的作者罗云彬、微软总部高级软件工程师Tiger Sun以及各软件安全论坛的朋友对本书提出宝贵的建议以及认可和肯定。

由于我的能力和水平的限制,书中难免会存在疏漏,欢迎读者批评指正。读者可以通过Windows中文网与我沟通。


王端明,从2008年开始参与Windows API程序设计,精通汇编语言、C/C++语言和Windows API程序设计,精通Windows环境下的桌面软件开发和加密 / 解密。曾为客户定制开发32位/64位Windows桌面软件,对加密/解密情有独钟,对VMProtect、Safengine等高强加密保护软件的脱壳或内存补丁有深入的研究和独到的见解,喜欢分析软件安全漏洞,曾在金山和360等网站发表过多篇杀毒软件漏洞分析的文章。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书免费提供配套源代码,要获得这一资源,请在异步社区本书页面中单击“配套资源”,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e56948”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书技术审校等工作,可以发邮件给本书的责任编辑(chencongcong@ptpress.com.cn)。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


本章首先简要介绍Windows的特色和编程语言的分类,然后通过编写第一个Windows程序,详细讲解这个程序的组成,介绍一些编程基础知识。

Microsoft Windows是美国微软公司研发的一套操作系统,它问世于1985年,起初仅仅是Microsoft DOS字符模式环境,由于微软不断地更新升级,后续的系统版本不但易用,而且也慢慢地成为用户喜爱的操作系统。Windows采用了图形用户界面(Graphic User Interface,GUI),与以前的DOS需要键入命令的方式相比更为人性化。随着计算机硬件和软件的不断升级,微软的Windows也在不断地升级,从架构的16位、16 + 32位混合版(Windows 9x)、32位,再到64位,系统版本从最初的Windows 1.0到大家熟知的Windows 95、Windows 98、Windows ME、Windows 2000、Windows XP、Windows Vista、Windows 7、Windows 8、Windows 8.1、Windows 10,以及Windows Server 2003、Windows Server 2008和Windows Server 2016企业级服务器操作系统,并仍在持续更新。微软始终致力于Windows操作系统的开发和完善。

Windows操作系统的主要特点包括图形用户界面、多用户、多任务,网络支持良好、多媒体功能出色、硬件支持良好、可供下载使用的应用程序众多等,这些特点足以让它广泛流行。以下是Windows的3个主要的特点。

图形用户界面。这是Windows最重要的特色,用户由此摆脱了原有字符模式操作系统必须死记硬背的键盘命令和令人一头雾水的屏幕提示,改为以鼠标为主,可以直接和屏幕上所见的界面进行交互。

多任务。Windows是一个多任务的操作系统环境,它允许用户同时运行多个应用程序。每个应用程序在屏幕上占据一块矩形区域,这个区域称为窗口。而且窗口是可以重叠的,用户可以移动这些窗口,或在不同的应用程序窗口之间进行切换,并可以在不同的应用程序之间进行数据交换和通信。

一致的用户界面。大部分Windows程序的界面看起来都差不多,例如,它们通常有标题栏和菜单栏。

程序员更关心的是隐藏在底层的细节,Windows究竟提供了什么便利?对程序员来说,Windows的以下特征更为重要。

大量的API函数调用。Windows支持几千种函数调用,涉及应用程序开发的各方面,程序员可以开发出具有精美用户界面和卓越性能的应用程序。

设备无关性。应用程序并不直接访问屏幕、打印机和键盘等硬件设备。Windows虚拟化了所有的硬件,只要有设备驱动程序,这个硬件就可以使用。应用程序不需要关心硬件的具体型号,这个特性与DOS编程中需要针对不同的显卡和打印机等编写不同的驱动程序相比,对程序员的帮助是巨大的。

内存管理方便。由于内存分页和虚拟内存的使用,每个应用程序都可以使用4GB的地址空间(Win32),DOS编程时必须考虑的640KB内存问题已经成为历史。64位系统支持的地址空间更大。

Windows API(Application Programming Interface)是Microsoft Windows平台的应用程序编程接口,其主要目的是让应用程序开发人员可以调用操作系统提供的一组例程功能,而无须考虑其底层的源代码实现及内部工作机制。API函数是构筑整个Windows框架的基石,它基于Windows的操作系统核心,上层是所有的Windows应用程序。

编程语言的种类非常多,总体来说可以分为机器语言、汇编语言和高级语言三大类。计算机所做的每一个操作,都按照用计算机语言编写的程序来执行,程序是计算机要执行的指令集合。程序是用我们所掌握的计算机语言来编写的,想控制计算机就需要通过计算机语言编写程序向其发出命令。

计算机内部只能识别二进制代码,用二进制的0和1描述的指令称为机器指令。全部机器指令的集合构成计算机的机器语言。计算机把0和1描述的指令转换为一列高低电平,使计算机的电子器件受到驱动并进行运算。用机器语言编写的程序称为目标程序,只有目标程序才能被计算机直接识别和执行,但是机器语言编写的程序没有明显特征,难以记忆,不便阅读和书写,且依赖于具体机器,局限性很大。机器语言属于低级语言。

汇编语言和机器语言都是直接对硬件进行操作,同样需要编程者将每一步具体的操作用指令的形式写出来,只不过汇编语言的指令采用了英文缩写的助记符,更容易识别和记忆。汇编程序通常由3个部分组成:指令、伪指令和宏指令。汇编程序的每一句指令只能对应实际操作过程中的一个很细微的操作,例如移动和自增,因此汇编源程序一般比较冗长、复杂、容易出错,而且使用汇编语言编程需要程序员具备较多的计算机专业知识,但是汇编语言的优点也是显而易见的,例如有些硬件底层操作通过高级语言很难实现,汇编语言生成的可执行文件(.exe或.dll)比较小,而且执行速度很快。

高级语言主要相对于汇编语言而言,它并不是特指某一种具体的编程语言,而是包括了多种编程语言,例如C、C++、Java。高级语言是大多数编程人员的选择,和汇编语言相比,它不但将许多相关的机器指令合成单条指令,并且去掉了与完成工作无关的细节,例如使用堆栈、寄存器等,这样就大大简化了程序中的指令,同时,由于省略了很多细节,编程人员也就不需要具备太多的专业知识。

通过高级语言编写的应用程序不能直接被计算机识别,必须经过转换才能执行,按转换方式可以将它们分为两类。

解释类:执行方式类似于日常生活中的“同声翻译”,应用程序源代码一边由相应编程语言的解释器“翻译”成目标代码(机器语言),一边被执行,因此执行效率比较低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器,但这种方式比较灵活,可以动态地调整、修改应用程序源代码,Python、JavaScript、Perl等都是解释类语言。

编译类:编译是指在程序执行以前,将程序源代码“翻译”成目标代码(机器语言),因此目标程序可以脱离其编程语言环境独立执行,使用比较方便、效率较高。但是如果需要修改应用程序,则必须先修改源代码,再重新编译生成新的目标文件,如果只有目标文件而没有源代码,则修改起来比较困难,C、C++、Delphi等都是编译类语言。

本书使用的操作系统为Windows 10 64位企业版(1703),IDE使用Visual Studio(VS)2019旗舰版集成开发工具。关于VS工具的下载以及安装,读者可以自行搜索安装教程。VS可以开发各种类型的项目,如果安装全部项目支持,可能需要几十GB磁盘空间,因此我们只安装“使用C++的桌面开发”就可以。

安装VS以后,建议读者同时安装一款功能强大的代码提示工具Visual Assist,安装文件以及安装方法参见Chapter1\Visual Assist X_10.9.2341.2。

安装Visual Assist以后的VS默认窗口布局参见Chapter1\1VS默认窗口布局.png,左侧的辅助窗口有服务器资源管理器和工具箱,右侧的辅助窗口有解决方案资源管理器、团队资源管理器和属性窗口等,这些辅助窗口都能隐藏、关闭或者调整位置,它们都可以在“视图”菜单项下找到。符合我工作习惯的VS窗口布局参见Chapter1\2我的VS窗口布局.png,右侧的辅助窗口可以选择自动隐藏。如果需要恢复默认窗口布局,请单击窗口菜单项→重置窗口布局。

打开VS,单击创建新项目(N)按钮,打开创建新项目对话框。

语言类型选择C++,目标平台选择Windows,项目类型选择所有项目类型。然后选中Windows桌面向导,界面如图1.1所示。

图1.1

单击下一步按钮,界面如图1.2所示,项目名称输入HelloWorld,选择一个保存位置,单击创建按钮。

图1.2

如图1.3所示,应用程序类型选择桌面应用程序(.exe),勾选空项目,然后单击确定按钮,VS会自动创建解决方案,因为选择的是空项目,所以一切源文件和头文件都需要我们自己逐一添加。

创建解决方案以后,默认情况下是Debug x86(32位程序,调试版本),如图1.4所示,因为Win32程序还将长期存在,所以本书程序默认选择该配置。后面会介绍Release发行版本和编译为64位程序时需要注意的问题。

图1.3

图1.4

在左侧的解决方案资源管理器中,右键单击鼠标,选择源文件→添加→新建项,选择C++文件,命名为HelloWorld.cpp,单击添加按钮。HelloWorld.cpp源文件的内容如下:

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int   
nCmdShow)
{
    MessageBox(NULL, TEXT("Hello World!"), TEXT("Caption"), MB_OKCANCEL | MB_ICONINFORMATION | MB_DEFBUTTON2);

    return 0;
}

按Ctrl+F5组合键运行程序,弹出一个消息框,标题为Caption,内容为Hello World!,如图1.5所示。

图1.5

下面详细解释本程序。

因为#include <Windows.h>是编译预处理指令,而不是C++语句,所以并不需要以分号结束。Windows.h是编写Windows程序最重要的头文件,在HelloWorld.cpp文件中,光标定位到第一行#include <Windows.h>中的Windows.h上,右键单击鼠标,选择转到文档(G)<Windows.h>,可以看到Windows.h头文件的内容。Windows.h头文件中包含了许多其他头文件,其中较重要且基本的如下。

WinDef.h:基本数据类型定义。

WinBase.h:Kernel(内核)有关定义。

WinGdi.h:图形设备接口有关定义。

WinUser.h:用户界面有关定义。

这些头文件对Windows的数据类型、函数声明、数据结构以及常量等作了定义。

和控制台程序有一个入口函数main一样,Windows程序的入口函数为WinMain,该函数由系统调用,入口函数WinMain在WinBase.h头文件中的声明如下:

int WINAPI WinMain(
    _In_     HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_     LPSTR     lpCmdLine,
    _In_     int       nShowCmd);

1.函数调用约定

WinMain函数名称前面的WINAPI在minwindef.h头文件中的定义如下:

#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define APIPRIVATE  __stdcall
#define PASCAL      __stdcall
#define APIENTRY    WINAPI

可以看到,CALLBACK、WINAPI、APIENTRY等都代表__stdcall__stdcall是一种函数调用约定,也称为标准调用约定。函数调用约定描述函数参数的传递方式和由谁来平衡堆栈,在程序中调用一个函数时,函数参数的传递是通过堆栈进行的,也就是说调用者把要传递给函数的参数压入堆栈,函数在执行过程中从堆栈中取出相应的参数使用。

打开OllyICE调试器(简称OD),把Chapter1\HelloWorld\Debug\HelloWorld.exe拖入OD中,在OD左下角的Command编辑框中输入:bpx MessageBoxW。这就为HelloWorld程序中调用MessageBox函数的位置设了一个断点,然后按F9键运行程序,在OD中可以看到程序在0115171B一行中断:

0115170A    68 41010000     push    141          ; MB_OKCANCEL | MB_ICONINFORMATION |   
MB_DEFBUTTON2
0115170F    68 307B1501     push    01157B30     ; UNICODE "Caption"
01151714    68 447B1501     push    01157B44     ; UNICODE "Hello World!"
01151719    6A 00           push    0            ; 0
0115171B    FF15 98B01501   call    [<&USER32.MessageBoxW>>; USER32.MessageBoxW
01151721    3BF4            cmp     esi, esp

HelloWorld程序中对MessageBox函数的调用被汇编为以上4个push和1个call调用共5行汇编代码。Win32 API函数都是使用__stdcall调用约定,可以看到MessageBox的函数参数是按照从右到左的顺序依次压入堆栈的。HelloWorld程序源代码中使用MessageBox函数调用,程序编译以后则是MessageBoxW函数调用,多了一个W,这个问题后面会讲。

现在程序执行到0115171B这一行,按F7键单步进入,到达MessageBoxW函数的内部,如图1.6所示。

图1.6

此时OD右下角堆栈窗口显示的堆栈空间如图1.7所示。

图1.7

从上面的堆栈窗口第一行可以看到,0115171B  FF15 98B01501 call [<&USER32.MessageBoxW>>的下一行(即一条指令)的地址01151721被压入堆栈,这是MessageBoxW函数执行完成后返回的地址。从图1.7中还可以清晰地看到HelloWorld程序对MessageBox函数的调用,其函数参数是按照从右到左的顺序依次压入堆栈的。

MessageBoxW函数内部需要使用刚才传递进来的函数参数,而且大部分函数内部需要使用局部变量。因为要不断压栈出栈,所以esp寄存器的值会经常发生变化。函数内部使用ebp寄存器作为指针来引用函数参数和局部变量,首先把ebp的值压入堆栈,然后把esp的值赋给ebp,之后就可以使用ebp作为指针了,在7777DB8A这一行再恢复ebp寄存器的值。

前面说过,函数调用约定描述函数参数是怎么传递和由谁来平衡堆栈的,7777DB8B一行 retn 10指令的功能是返回到01151721,并把esp寄存器的值加上十六进制的10(也就是16,加16是因为当初调用MessageBoxW函数的时候压入了4个函数参数,正好是16字节的堆栈空间)。执行retn 10指令以后esp寄存器会恢复为调用MessageBoxW函数以前的值,也就是执行0115170A这一行指令以前的值。另外,之所以把esp加上一个数来恢复esp的值,是因为压栈操作会导致esp的值变小,也就是堆栈生长方向的问题。

从本例可以看出,__stdcall函数调用约定按照从右到左的顺序把函数参数压入堆栈,并由函数自身来负责平衡堆栈。

常用的函数调用约定有__stdcall__cdecl(C调用约定)、__fastcall__pascal等。__stdcall__cdecl__fastcall的函数参数都是按照从右到左的顺序压入堆栈,而__pascal则是按照从左到右的顺序压入堆栈。__cdecl由函数调用方负责平衡堆栈,__stdcall__fastcall__pascal则是由函数自身负责平衡堆栈。

看一下__cdecl调用约定平衡堆栈的方式。打开VS,创建一个控制台应用程序的空项目CLanguage,CLanguage.c源文件的内容如下所示:

#include <stdio.h>

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

int main()
{
    int n;

    n = add(1, 2);
    printf("%d\n", n);
    return 0;
}

光标定位到n = add(1, 2);这一行,按F9键添加断点,然后按F5键调试运行,程序中断在n = add(1, 2);这一行,单击调试→窗口→反汇编,界面如图1.8所示。

图1.8

可以看到调用方在调用_add函数以后,是通过add esp,8语句进行平衡堆栈的。为什么010A18AC一行显示的函数调用是_add而不是add,后面再讲。

64位CPU除段寄存器以外,其余都是64位(8字节)。64位的通用寄存器在数量上增加了8个,共有16个通用寄存器,其中8个是为了兼容32位,将原来的名称由E**改为了R**,如EAX改为RAX,其余8个分别命名为R8~R15,EIP和EFLAGS都改为RIP和RFLAGS,浮点寄存器还是64位,分别是MMX0(FPR0)~MMX7(FPR7)。另外,还增加了16个128位的多媒体寄存器XMM0~XMM15,称为SSE指令,XMM0等多媒体寄存器又是256位寄存器YMM0等的低128位。

RAX等8个通用寄存器的低32位、低16位、低8位,那么可以使用相应的寄存器进行存取,例如RAX来说分别是EAX、AX、AL;R8等后来按序号命名的寄存器取64位、低32位、低16位、低8位分别用R8、R8D、R8W、R8B。

64位程序的函数调用有所不同。64位程序的函数调用约定最多可以通过寄存器传递4个函数参数,前4个参数从左到右依次存放于RCX、RDX、R8和R9寄存器,从第五个参数开始需要通过堆栈来进行传递,64位程序的新式函数调用约定可以明显地加快函数调用的速度。在64位程序中,进行函数调用时通常不再使用PUSH指令来传递参数,而是通过MOV指令把参数传递到寄存器或堆栈。64位程序的新式函数调用约定不再使用EBP寄存器作为指针来引用函数参数和局部变量,而是直接使用RSP堆栈指针寄存器。另外,由调用者负责堆栈平衡(和__cdecl一样)。掌握了32位程序的调试,再去调试64位程序是非常容易上手的。调试64位程序通常使用x64Dbg。

2.批注

WinMain函数名称下有一个绿色波浪线,这是一个警告,鼠标光标悬停于WinMain函数名称上时会弹出一个提示:C28251: "WinMain"的批注不一致: 此实例包含 无批注。参见C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ um\WinBase.h(933),如图1.9所示。

图1.9

打开WinBase.h头文件,定位到933行,如图1.10所示。

图1.10

WinMain函数声明的每个参数的数据类型前都有一个参数说明符:_In_、_In_opt_等,这些参数说明符称为参数批注。图1.9中提示的意思是,WinBase.h头文件中的WinMain函数声明有批注,而程序中的WinMain函数定义没有批注。

参数批注用于说明函数参数的性质和类型,可以帮助开发人员更好地了解如何使用这些参数,常见的参数批注如表1.1所示。

表1.1

参数批注

含义

_In_

该参数是一个输入参数,在调用函数的时候为该参数设置一个值,函数只可以读取该参数的值但不可以修改

_Inout_

该参数是一个输入输出参数,在调用函数的时候为该参数设置一个值,函数返回以后会修改该参数的值

_Out_

该参数是一个输出参数,函数返回以后会在该参数中返回一个值

_Outptr_

该参数是一个输出参数,函数返回以后会在该参数中返回一个指针值

与上面4个参数批注对应的还有_In_opt_、_Inout_opt_、_Out_opt_和_Outptr_opt_,opt表示可选择(optional),表示可以不使用该参数,也可以设置为0或者NULL(0),而表格中的4个不带opt的参数批注表示该参数必须指定一个合理的值。

在VS 2019以前,并不要求在函数声明和定义中设置参数批注,参数批注仅用于指导程序员正确使用函数参数。为了简洁,本书设置自定义函数的时候也不使用参数批注,但在具体介绍一个Windows API函数的时候,我们都会列出参数批注,以帮助大家正确使用函数参数。

3.WinMain的4个函数参数的含义

HINSTANCE hInstance,表示应用程序的当前实例的句柄,在Windows程序中句柄无非就是一个数值,程序中用它来标识某些对象,本例中hInstance实例句柄就唯一地标识了正在运行中的exe程序文件。

先说一下模块的概念。模块代表的是一个运行中的.exe或.dll文件,表示这个文件中的所有代码和资源,磁盘上的文件不是模块,载入内存后运行时叫作模块;另外,一个应用程序调用其他动态链接库中的API时,这些.dll文件也会被载入内存,这就产生了一些动态链接库模块。为了区分地址空间中的不同模块,每个模块都有一个唯一的模块句柄来标识。模块句柄实际上就是一个内存基地址,系统将.exe或.dll文件加载到地址空间的这个位置。

实例的概念源于Win16,Win16系统中运行的不同程序的地址空间并非是完全隔离的。一个可执行文件运行后形成模块,多次加载同一个可执行文件时,这个模块是公用的。为了区分多次加载的“复制”,把每个“复制”叫作实例,每个实例均用不同的实例句柄(HINSTANCE)值来标识。但在Win32中,每一个运行中的程序的地址空间是隔离的,每个实例都使用自己私有的4GB虚拟地址空间,不存在一个模块具有多个实例的问题。即使同一程序同时运行了多个,它们之间通常也是互不影响的。在Win32中,实例句柄就是模块句柄,但很多API函数中用到模块句柄的时候使用的名称还是实例句柄。

HINSTANCE hPrevInstance,表示应用程序上一个实例的句柄。在Win16中,当同时运行一个程序的多个副本时,同一程序的所有实例共享代码以及只读数据(例如菜单或对话框模板之类的资源),一个程序可以通过査看hPrevInstance参数从而得知是否有它的其他实例在运行,这样就可以把一些数据从前一个实例移到自己的数据区来。对于Win32应用程序,该参数始终为NULL。

LPSTR lpCmdLine,指向应用程序命令行参数字符串的指针,不包括可执行文件名。要获取整个命令行,可以调用GetCommandLine函数。例如,在D盘下有一个111.txt文件,当我们用鼠标双击这个文件时将启动记事本程序(notepad.exe),此时系统会将D:\111.txt作为命令行参数传递给记事本程序的WinMain函数,记事本程序得到这个文件的路径后,在窗口中显示该文件的具体内容。
LPSTR是一种Windows数据类型,在winnt.h头文件中定义如下:

typedef _Null_terminated_ CHAR *NPSTR, *LPSTR, *PSTR;
typedef char CHAR;

_Null_terminated_表示以零结尾的字符串,LPSTR表示一个以零结尾的char类型字符串的指针。LPSTR中的LP是Long Pointer(长指针),这是Win16遗留的概念,在Win32中不区分长短指针,指针都是32位。以零结尾,有时候也称为以空字符结尾、以NULL结尾等。

int nCmdShow,指定应用程序最初如何显示,例如在任务栏上正常显示、最大化到全屏显示或最小化显示。

MessageBox函数的功能是显示一个消息提示框,其中可以包含一个系统图标、一组按钮、一个消息标题和一条简短的消息内容。函数原型如下:

int WINAPI MessageBox(
    _In_opt_ HWND    hWnd,      // 消息框的所有者(拥有者)的窗口句柄
    _In_opt_ LPCTSTR lpText,    // 要显示的消息内容
    _In_opt_ LPCTSTR lpCaption, // 消息框的标题
    _In_     UINT    uType);    // 消息框的图标样式和按钮样式

第1个参数hWnd指定消息框的所有者的窗口句柄,HWND是Handle Window的缩写,即窗口句柄。在Win32中句柄实际上就是一个32位的数值。句柄的实际取值对于程序来说并不重要,Windows通过句柄来标识它所代表的对象,比如读者单击某个按钮,Windows通过该按钮的窗口句柄来判断读者单击了哪一个按钮。在 Windows 中,句柄的使用非常频繁,以后还将遇到HIC0N(图标句柄)、HCURSOR(光标句柄)以及HBRUSH(画刷句柄)等。

第2个参数lpText指定要显示的消息内容,LPCTSTR是一种Windows数据类型,在winnt.h头文件中定义如下:

typedef LPCWSTR PCTSTR, LPCTSTR;
typedef _Null_terminated_ CONST WCHAR *LPCWSTR, *PCWSTR;
typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
typedef wchar_t WCHAR;
typedef char CHAR;

CONST表示常量字符串,不可修改,就是说LPCTSTR是一个指向wchar_t或char类型常字符串的指针。后面将介绍wchar_t数据类型。

第3个参数lpCaption指定消息框的标题。

第4个参数uType指定消息框的图标样式和按钮样式。要指定在消息框中显示的按钮,可以使用表1.2所列的值。

表1.2

常量

显示的按钮

MB_ABORTRETRYIGNORE

中止、重试和忽略

MB_CANCELTRYCONTINUE

取消、重试和继续

MB_HELP

确定、帮助

MB_OK

确定

MB_OKCANCEL

确定、取消

MB_RETRYCANCEL

重试、取消

MB_YESNO

是、否

MB_YESNOCANCEL

是、否和取消

要指定在消息框中显示的图标,可以使用表1.3所列的值。

表1.3

常量

显示的图标

MB_ICONEXCLAMATION

感叹号图标

MB_ICONWARNING

感叹号图标

MB_ICONINFORMATION

在一个圆圈中有一个小写字母i组成的图标

MB_ICONASTERISK

在一个圆圈中有一个小写字母i组成的图标

MB_ICONQUESTION

问号图标

MB_ICONSTOP

停止标志图标

MB_ICONERROR

停止标志图标

MB_ICONHAND

停止标志图标

还可以指定消息框的默认按钮。默认按钮在显示消息框时突出显示的按钮,它有一个粗的边框,按下Enter键就相当于单击了这个按钮。要设置默认按钮,可以使用表1.4所列的值。

表1.4

常量

含义

MB_DEFBUTTON1

第1个按钮是默认按钮

MB_DEFBUTTON2

第2个按钮是默认按钮

MB_DEFBUTTON3

第3个按钮是默认按钮

MB_DEFBUTTON4

第4个按钮是默认按钮

MessageBox函数执行成功会返回一个整数值,指明用户单击了哪个按钮,返回值可以使用表1.5所列的值。

表1.5

返回值

含义

IDABORT

单击了中止按钮

IDCANCEL

单击了取消按钮,如果消息框有取消按钮,则当按下Esc键或单击取消按钮时,函数都将返回IDCANCEL值

IDCONTINUE

单击了继续按钮

IDIGNORE

单击了忽略按钮

IDNO

单击了否按钮

IDOK

单击了确定按钮

IDRETRY

单击了重试按钮

IDTRYAGAIN

单击了重试按钮

IDYES

单击了是按钮

例如,可以像下面这样判断返回值:

int nRet = MessageBox(NULL, TEXT("Hello World!"), TEXT("Caption"), MB_OKCANCEL | MB_ ICONINFORMATION | MB_DEFBUTTON2);
switch (nRet)
{
case IDOK:
    MessageBox(NULL, TEXT("用户单击了确定按钮"), TEXT("Caption"), MB_OK); // TEXT宏稍后再讲
    break;
case IDCANCEL:
    MessageBox(NULL, TEXT("用户单击了取消按钮"), TEXT("Caption"), MB_OK);
    break;
}

计算机只认识二进制的0和1,编译是把高级语言转换成计算机可以识别的二进制机器语言的过程。本节通过分步编译CLanguage.c来演示一个程序的编译过程。为了实现分步编译,我们在本机安装了MinGW Installer编译工具,这是Linux下的gcc编译器的Windows版本。将一个C/C++文件编译为可执行程序,需要经过预处理、汇编、编译、链接等阶段,下面分别进行介绍。

1.预处理

打开命令窗口,输入gcc -E CLanguage.c –o CLanguage.i。

-E选项表示只进行预处理,执行上述命令会生成经过预处理的CLanguage.i文件。用EditPlus软件打开CLanguage.i,可以看到短短的几行源代码变成了800多行:

……
# 1 "C:/Strawberry/c/i686-w64-mingw32/include/_mingw_print_pop.h" 1 3
# 994 "C:/Strawberry/c/i686-w64-mingw32/include/stdio.h" 2 3
# 2 "CLanguage.c" 2

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

int main()
{
    int n;

    n = add(1, 2);
    printf("%d\n", n);
    return 0;
}

预处理的过程做了以下工作:宏定义展开(例如#define 定义);处理所有的条件编译指令,例如#ifdef、#ifndef、#endif等;处理#include,将#include引用的文件插入该行;删除所有注释;添加行号和文件标识,这样在调试和编译出错的时候可以确定是哪个文件的哪一行。预处理的过程并不会检查语法错误。

2.汇编

继续在命令行窗口输入gcc -S CLanguage.i -o CLanguage.s。

-S(大写)选项表示只进行预处理和汇编,执行上述命令会生成经过预处理和汇编的CLanguage.s文件。用EditPlus软件打开CLanguage.s,可以看到:

    .file "CLanguage.c"
    .text
    .globl _add
    .def _add;  .scl  2;  .type  32;  .endef
_add:
    pushl %ebp
    movl  %esp, %ebp
    movl  8(%ebp), %edx
    movl  12(%ebp), %eax
    addl  %edx, %eax
    popl  %ebp
    ret
    .def  ___main;  .scl  2;  .type  32;  .endef
    .section .rdata,"dr"
LC0:
    .ascii "%d\12\0"
    .text
    .globl  _main
    .def  _main;  .scl  2;  .type  32;  .endef
_main:
    pushl  %ebp
    movl  %esp, %ebp
    andl  $-16, %esp
    subl  $32, %esp
    call  ___main
    movl  $2, 4(%esp)
    movl  $1, (%esp)
    call  _add
    movl  %eax, 28(%esp)
    movl  28(%esp), %eax
    movl  %eax, 4(%esp)
    movl  $LC0, (%esp)
    call  _printf
    movl  $0, %eax
    leave
    ret
    .ident  "GCC: (i686-posix-sjlj, built by strawberryperl.com project) 4.9.2"
    .def  _printf;  .scl  2;  .type  32;  .endef

汇编的过程会检查语法错误。

3.编译

继续在命令行窗口输入gcc -c CLanguage.s -o CLanguage.obj。

-c(小写)选项表示只进行预处理、汇编和编译,执行上述命令会生成经过预处理、汇编和编译的CLanguage.obj文件。编译过程就是将汇编文件生成目标文件的过程,在这个过程中会做一些优化处理。目标文件是二进制文件,无法使用文本编辑器打开,可以使用十六进制编辑工具打开查看。

4.链接

CLanguage.c用到了C标准库的printf函数,但是编译过程只是把源文件转换成二进制文件而已,这个二进制文件还不能直接执行,还需要把转换以后的二进制文件与要用到的库绑定链接到一起(实际上还会绑定其他对象,并做一些其他工作,在此不再深究)。

一步编译命令gcc CLanguage.c -o CLanguage.exe。

执行上述命令会生成经过预处理、汇编、编译和链接的exe可执行程序。

在VS中,一步编译的快捷键是Ctrl + F7,一步编译并执行的快捷键是Ctrl + F5。

我们知道,计算机只能存储二进制数据,那么该如何表示和存储字符呢?这就需要使用字符集来实现字符与整数之间的转换。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)起始于20世纪50年代后期,在1967年定案,是基于拉丁字母的一套计算机编码系统,主要用于显示现代英语和其他西欧语言,它是现今通用的单字节编码系统。ASCII最初是美国国家标准,供不同的计算机在相互通信时作为共同遵守的西文字符编码标准,后来被国际标准化组织(International Organization for Standardization,ISO)定为国际标准,称为ISO 646标准。

ASCII使用7位二进制数来表示128个字符,称为标准ASCII,包括所有的大写和小写字母、数字0~9、标点符号以及在美式英语中使用的特殊控制字符。

0~31及127(共33个)是控制字符或通信专用字符,例如控制字符包括LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符包括SOH(文头)、EOT(文尾)、ACK(确认)等。ASCII值8、9、10和13分别转换为退格、制表、换行和回车字符,它们并没有特定的图形显示,但会对文本显示产生影响。

32~126(共95个)是字符(32是空格),其中48~57为阿拉伯数字0~9;65~90为26个大写英文字母,97~122号为26个小写英文字母;其余为标点符号、运算符号等。

由ASCII码表可以看到,数字符号、大写字母符号和小写字母符号的编码都是连续的,所以只要记住数字符号的编码从0x30开始、大写字母符号的编码从0x41开始以及小写字母符号的编码从0x61开始便可以推算出其他数字符号和字母符号的编码。

标准ASCII仅使用了每字节的低7位进行编码,最多可以表示128个字符。这往往不能满足实际需求,为此在IBM PC系列及其兼容机上使用了扩展的ASCII码。扩展的ASCII码使用8位二进制数进行编码,扩展的ASCII包含标准ASCII中已有的128个字符,又增加了128个字符,总共是256个,值128~255用来表示框线、音标和其他欧洲非英语系的字母。

1987年4月,MS DOS 3.3把代码页的概念带进了IBM。每个代码页都是一个字符集,并且这一概念后来也被用到了Windows系统里。这样一来,原本的IBM字符集成为第437页代码页,微软自己的MS DOS Latin 1字符集成为第850页代码页。其他的代码页为其他语言制定,就是说较低的128个ASCII码总是表示标准ASCII字符,而较高的128个ASCII码则取决于定义代码页的语言。代码页的数量以超乎想象的速度递增,后来更是出现了不同操作系统对于同一个国家语言的代码页互相不兼容的情况。每个系统环境的代码页都对标准字符集进行了修订,这使局面很混乱。

单字节字符集肯定远远包含不了那些包括上万个字符的语言,例如中文、日文,因此这些国家都开发了表示自己本国文字的双字节字符集,用2字节(16位二进制数据)来表示除ASCII以外的字符(ASCII还是使用1字节来表示),其中常见的就是我国的GB系列编码了(GB为国标的拼音缩写)。不同国家创造出来的字符集虽说与ASCII兼容,但是编码却是互不兼容的,例如相同数值的2字节,在中文和日语中则表示两个不同的字符。这些不同国家的字符集,同样也被微软纳入了代码页体系中,例如中文就是第936页代码页(我们最常见的CP936就是这个意思,它表达的字符集和GBK是一样的)。

Windows支持4种不同的双字节字符集:代码页932(日文)、936(简体中文)、949(韩文)以及950(繁体中文)。在双字节字符集中,一个字符串中的每个字符都由1字节或2字节组成。以日文为例,如果第1字节在0x81~0x9F或0xE0~0xFC,就必须检查下一字节,才能判断出一个完整的字符。对程序员而言,和双字节字符集打交道如同一场噩梦,因为有的字符是1字节宽,有的字符却是2字节宽。

为了表示不同国家和地区的语言,编码方案有上百种,避免这种混乱的需求由来已久。Unicode是1988年由Apple和Xerox共同建立的一项标准,Unicode的诞生解决了双字节字符集的混乱问题。虽然同样是用16位(2字节)来表示字符,但Unicode只有一个字符集,包含了世界上任何一个国家和地区的语言所用的字符。Unicode标准定义了所有主要语言中使用的字母、符号、标点,其中包括了欧洲地区的语言、中东地区的希伯来语言、亚洲地区的语言等。Unicode有3种编码形式,允许字符以字节、字或双字格式存储。

UTF-8。UTF-8将有的字符编码为1字节,有的字符编码为2字节,有的字符编码为3字节,甚至有的字符编码为4字节。值在0x80(128)以下的字符(即标准ASCII)被转换为1字节,适合美国;0x80~0x7FF的字符被转换为2字节,适合欧洲和中东地区;0x800以上的字符被转换为3字节,适合东亚地区;最后,代理对(Surrogate Pair)被转换为4字节。代理对是UTF-16中用于扩展字符而使用的编码方式,采用4字节(两个UTF-16编码)来表示一个字符。UTF-8是一种相当流行的编码格式,但在对值为0x800以上的大量字符进行编码时,UTF-8不如UTF-16高效。

UTF-16。UTF-16将每个字符编码为2字节(16位)。在谈到Unicode时,除非专门声明,一般都是指UTF-16编码。Windows之所以使用UTF-16,是因为全球各地使用的大部分语言中,通常用一个16位值来表示每个字符,每个字符被编码为2字节,所以很容易遍历字符串并计算它的字符个数。但是,16位不足以表示某些语言的所有字符。对于不能表示的字符,UTF-16支持使用代理对,代理对是用32位(4字节)来表示一个字符的一种方式,由于只有少数应用程序需要使用这类字符,因此UTF-16在节省空间和简化编码这两个目标之间提供了一个很好的折衷。

UTF-32。UTF-32将每个字符都编码为4字节,用于不太关心存储空间的环境中。在将字符串保存到文件或传输到网络的时候,基于空间和速度的考虑,很少会使用这种格式,这种编码格式一般在应用程序内部使用。

把长度较小的Unicode值(例如字节)复制到长度较大的Unicode值(例如字或双字)中不会丢失任何数据。另外,Unicode的实现还有大小端(后面会讲)存储的区别,并且UTF-8还存在是否带有BOM标记的问题,因此在很多文本编辑器里有多种关于Unicode这一项的编码转换。

首先是字面上的差别,ASCII即American Standard Code for Information Interchange,美国信息互换标准代码;ANSI即American National Standard Institite,美国国家标准协会的一种编码标准。后者更强调国家标准,一般是面向世界范围内国家和地区之间的交流,该协会规定了很多类似的标准。为了让计算机支持更多语言,值在0x80~0xFFFF范围的字符使用2字节或多字节来表示,比如:汉字“中”在中文操作系统中使用[0xD6,0xD0]来存储。不同的国家和地区制定了不同的标准,由此产生了GB2312、GBK、GB18030、Big5、Shift_JIS等各自的编码标准。这些使用多字节来代表一个字符的各种延伸编码方式,称为ANSI编码。在简体中文Windows操作系统中,ANSI编码代表GBK编码;在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI编码代表Shift_JIS编码。不同的ANSI编码互不兼容。当信息在国际间交流时,两种语言的文字无法使用同一个ANSI编码,ANSI编码就是一个具体国家的多字节字符集。ANSI编码表示英文字符时使用1字节,表示中文时用2~4字节。

其次,一般会拿ANSI码和Unicode码对比,两者都是各种语言的表示方法。不同的是,ANSI在不同国家和地区的不同语言中有不同的具体标准,是国家标准,比如,在简体中文系统中就是GB2312、GBK。相对而言,Unicode正如其名,是Universal Code,具有统一、通用的意思,是国际化标准。

1.char数据类型

我们可以这样定义并初始化一个字符变量:

char c = 'A';

变量c需要1字节的存储空间,并用十六进制数值0x41来初始化(字母A的ASCII值为0x41)。

可以按如下方式定义并初始化一个char类型字符串的指针:

char *pStr = "Hello!";

在Win32中指针变量pStr需要4字节的存储空间。指针变量pStr指向的字符串需要7字节的存储空间,其中包括6字节的字符和一个字符串结束标志0。

可以按如下方式定义并初始化一个char类型字符数组:

char szStr[] = "Hello!";

字符数组szStr同样需要7字节的存储空间,其中包括6字节的字符和一个字符串结束标志0。

2.宽字符wchar_t

Unicode(一般指UTF-16)统一用2字节来表示一个字符。Unicode是现代计算机的默认编码方式,Windows 2000以后的操作系统,包括Windows 2000、Windows XP、Windows Vista、Windows 7、Windows 8、Windows 10、Windows Phone、Windows Server等(统称 Windows NT)都从底层支持Unicode。注意,说到宽字符集,通常指Unicode,也就是UTF-16,Unicode为宽字符集代言;说到多字节字符集通常指用1到多字节来表示一个字符,ANSI为多字节字符集代言。宽字符在内存中占用的空间通常比多字节字符多,但是处理速度更快,因为很多系统的内核(包括Windows NT内核)都是从底层向上使用Unicode编码的。用VS创建项目的时候,默认使用Unicode字符集,可以通过在解决方案资源管理器中右键单击项目名称→属性→配置属性→高级→字符集进行设置。

C/C++的宽字符数据类型为wchar_t。可以按如下方式定义并初始化一个wchar_t类型变量:

wchar_t wc = L'A';

大写字母L表明右边的字符需要使用宽字符存储。变量wc需要2字节的存储空间,并用十六进制数值0x0041来初始化。

可以按如下方式定义并初始化一个wchar_t类型字符串的指针:

wchar_t *pwStr = L"Hello!";

一个字符需要2字节来存储,指针变量pwStr指向的字符串需要14字节的存储空间,其中包括12字节的字符和2字节的字符串结束标志0。上述字符串在内存中的存储形式为48 00 65 00 6c 00 6c 00 6f 00 21 00 00 00。

可以按如下方式定义并初始化一个wchar_t类型字符数组:

wchar_t szwStr[] = L"Hello!";

字符数组szwStr同样需要14字节的存储空间。

sizeof操作符用于返回一个变量、对象或数据类型所占用的内存字节数,例如下面的代码:

char ch = 'A';                // 1
wchar_t wch = L'A';           // 2
char str[] = "C语言";         // 6,C占用1字节,语言占用4字节,还有1字节的字符
                              // 串结束标志
wchar_t wstr[] = L"C语言";    // 8,一个字符占用2字节,还有2字节的字符串结束标志
printf("ch = %d, wch = %d, str = %d, wstr = %d\n", 
    sizeof(ch), sizeof(wch), sizeof(str), sizeof(wstr));

输出结果为ch = 1, wch = 2, str = 6, wstr = 8。

注意:用char数据类型定义变量就表示使用多字节字符集存储字符,使用1字节或多字节来表示一个字符。标准ASCII部分的字符只需要使用1字节来表示,非标准ASCII部分的字符需要2字节或2字节以上来表示一个字符;用wchar_t数据类型定义变量表示使用Unicode字符集存储字符,使用2字节来表示一个字符。

3.TCHAR通用数据类型

Windows在winnt.h头文件中定义了自己的字符和宽字符数据类型:

typedef char CHAR;                  // 字符

#ifndef _MAC
    typedef wchar_t WCHAR;          // 宽字符
#else
    // Macintosh编译器没有定义wchar_t数据类型,宽字符被定义为16位整型数
    typedef unsigned short WCHAR; 
#endif

winnt.h头文件中还有如下定义:

#ifdef  UNICODE
    typedef WCHAR  TCHAR, *PTCHAR;
#else
    typedef CHAR   TCHAR, *PTCHAR;
#endif

用VS创建一个项目的时候,默认使用Unicode字符集。右键单击项目名称→属性→配置属性→C/C++→命令行,可以看到UNICODE和_UNICODE都被定义了,这两个宏不是在头文件中定义的,而是通过项目属性进行设置的;如果把项目属性设置为多字节字符集,则可以看到UNICODE和_UNICODE都会被取消定义。C语言代码通常用_UNICODE宏进行判断,Windows通常用UNICODE宏进行判断,所以这两个宏要么同时定义,要么一个都不定义,否则会出现难以预料的问题。

根据项目属性是否使用Unicode字符集,TCHAR被解释为CHAR(char)或WCHAR(wchar_t)数据类型。

4.TEXT宏

winnt.h头文件中有如下定义:

#ifdef  UNICODE
    #define __TEXT(quote) L##quote
#else
    #define __TEXT(quote) quote
#endif

#define  TEXT(quote)  __TEXT(quote)

##被称为“令牌粘贴”,表示把字母L和宏参数拼接在一起,假设宏参数quote是"Hello!",那么L##quote就是L"Hello!"。

就是说,如果源文件中有以下定义:

TCHAR szBuf[] = TEXT("C语言");

如果项目属性使用Unicode字符集,那么上面的定义将被解释为:WCHAR szBuf[] = L"C语言";。如果项目属性使用多字节或者ANSI字符集,则上面的定义将被解释为:CHAR szBuf[] = "C语言";。

5.字符串数据类型

winnt.h头文件中定义了许多字符串数据类型,例如:

typedef char CHAR;

typedef _Null_terminated_ CHAR          *NPSTR, *LPSTR, *PSTR;
typedef _Null_terminated_ CONST  CHAR   *LPCSTR, *PCSTR;

typedef _Null_terminated_ WCHAR         *NWPSTR, *LPWSTR, *PWSTR;
typedef _Null_terminated_ CONST  WCHAR  *LPCWSTR, *PCWSTR;

#ifdef  UNICODE
    typedef LPWSTR   PTSTR,  LPTSTR;
    typedef LPCWSTR  PCTSTR, LPCTSTR;
#else
    typedef LPSTR    PTSTR, LPTSTR, PUTSTR, LPUTSTR;
    typedef LPCSTR   PCTSTR, LPCTSTR, PCUTSTR, LPCUTSTR;
#endif

PSTR和LPSTR表示CHAR类型字符串;PCSTR和LPCSTR表示CHAR类型常字符串,C表示const。

PWSTR和LPWSTR表示WCHAR类型字符串;PCWSTR和LPCWSTR表示WCHAR类型常字符串。

PTSTR和LPTSTR表示TCHAR类型字符串;PCTSTR和LPCTSTR表示TCHAR类型常字符串。

如果希望我们的程序有ANSI版本和Unicode版本两个版本,可以通过编写两套代码分别实现ANSI版本和Unicode版本,但是针对ANSI字符和Unicode字符,维护两套代码是一件非常麻烦的事情,有了这些宏定义就可以实现对ANSI和Unicode编码的通用编程。

另外,入口点函数还可以写为如下格式:

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int  nCmdShow);

根据是否定义UNICODE,会被解释为WinMain或wWinMain:

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,  int nCmdShow);
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow);

本书中都是使用WinMain。如果使用_tWinMain,那么必须包含tchar.h头文件。

字符串处理是程序设计中的常见话题,本节介绍常用的C/C++字符串处理函数。学习这些函数有点枯燥,但是以后都会用到它们,所以本节必须介绍并要求大家掌握这些内容。

1.获取字符串的长度

获取字符串长度的函数是strlen和wcslen,请看函数声明:

size_t strlen(const char*    str);  // char类型字符串指针
size_t wcslen(const wchar_t* str);  // wchar_t类型字符串指针

size_t在vcruntime.h头文件中定义如下:

#ifdef _WIN64
    typedef unsigned __int64 size_t;
#else
    typedef unsigned int     size_t;
#endif

如果编译为64位程序,size_t代表64位无符号整型;如果编译为32位程序,size_t代表32位无符号整型。如果有不清楚的数据类型或数据结构定义,可以将其输入VS源文件中,右键单击转到定义,查看其定义;或者将光标定位到相关单词处按F1键打开微软官方帮助文档查看解释。

注意:strlen会将字符串解释为单字节字符串,因此即使该字符串包含多字节字符,其返回值也始终等于字节数;wcslen是strlen的宽字符版本,wcslen的参数是宽字符串,返回宽字符个数。这两个函数的返回值不包括字符串结尾的0,示例如下:

CHAR  str[] = "C语言";      // 5,C占用1字节,语言占用4字节
WCHAR wstr[] = L"C语言";    // 3,3个宽字符

// _tprintf是printf、wprintf的通用版本,稍后介绍_tprintf函数
_tprintf(TEXT("strlen(str) = %d, wcslen(wstr) = %d\n"), strlen(str), wcslen(wstr));

输出结果为strlen(str) = 5, wcslen(wstr) = 3。

_tprintf是printf和wprintf的通用版本,因此格式化字符串需要使用TEXT宏,今后一般不使用CHAR类型字符串""或WCHAR类型字符串L""形式的字符串定义。使用_tprintf需要包含tchar.h头文件,如果不包含,_tprintf下方会显示一个红色波浪线。如果读者不知道一个函数需要哪个头文件,则可以将光标定位到该函数,按F1键打开官方文档查看函数解释,会提示需要哪个.h头文件(有的函数还需要.lib导入库)。

strlen和wcslen这两个函数的通用版本为_tcslen,在tchar.h头文件中有如下定义:

#ifdef _UNICODE
    #define _tcslen wcslen
#else
    #define _tcslen strlen
#endif

示例如下:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

int main()
{
    TCHAR szStr[] = TEXT("C语言");    // 3 或 5
    _tprintf(TEXT("_tcslen(szStr) = %d\n"), _tcslen(szStr)); 

    return 0;
}

如果将项目属性设置为Unicode字符集,则输出结果为3;如果将项目属性设置为多字节字符集,则输出结果为 5。那么遇到多字节字符集,如何计算字符串的字符个数呢?实际上也有相关函数,但一般用不到,因为我们建议使用Unicode字符集编程。

2.查找一个字符串中首次出现的指定字符

strchr查找一个字符串中首次出现的指定字符,然后返回该字符出现的地址;strchr查找一个字符串中最后出现的指定字符,然后返回该字符出现的地址。这两个函数的通用版本分别是_tcschr和_tcsrchr,如果没有找到指定的字符或函数执行失败,则返回值为NULL。这两个函数只需要一个字符串和一个字符共两个参数,函数声明不再列出,示例如下:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
#include <locale.h>

int main()
{
    TCHAR szStr[] = TEXT("WindowsAPI是最为强大的编程语言!");
    LPTSTR lp = _tcschr(szStr, TEXT('最'));

setlocale(LC_ALL, "chs");   // 用_tprintf函数输出中文字符的时候,需要调用本函数设置区域

    _tprintf(TEXT("szStr的地址:%p lp的地址:%p \n"), szStr, lp);
    _tprintf(TEXT("szStr = %s lp = %s\n"), szStr, lp);
    // Unicode字符集
    // szStr的地址:0014FCC0 lp的地址:0014FCD6
    // szStr = WindowsAPI是最为强大的编程语言! lp = 最为强大的编程语言!
    // 多字节字符集
    // szStr的地址:003EFE38 lp的地址:003EFE44
    // szStr = WindowsAPI是最为强大的编程语言! lp = 最为强大的编程语言!

    return 0;
}

不管是设置为Unicode字符集,还是设置为多字节字符集,都是计算“WindowsAPI是”占用了多少字节的问题,设置为Unicode,0x0014FCD6 − 0x0014FCC0 = 0x16,也就是十进制的22;设置为多字节,0x003EFE44 − 0x003EFE38 = 0x0C,也就是十进制的12。这两种情况下lp都得到了正确的结果。

注意:用_tprintf函数输出中文字符的时候,需要调用setlocale函数(locale.h)设置区域为chs。

3.在一个字符串中查找另一个字符串

在一个字符串中查找另一个字符串首次出现的位置使用strstr和wcsstr函数,通用版本是_tcsstr:

char *strstr(
    const char *str,        // 在这个字符串中搜索
    const char *strSearch); // 要搜索的字符串
wchar_t *wcsstr(
    const wchar_t *str,
    const wchar_t *strSearch);

如果strSearch是str的子串,则返回strSearch在str中首次出现的地址;如果strSearch不是str的子串,则返回值为NULL。示例如下:

TCHAR szStr[] = TEXT("Hello, Windows, Windows API program simple and powerful!");
TCHAR szStrSearch[] = TEXT("Windows");

_tprintf(TEXT("%s\n"), _tcsstr(szStr, szStrSearch));
// Windows, Windows API program simple and powerful!

4.从一个字符串中查找另一个字符串中的任何一个字符

从一个字符串中查找另一个字符串中的任何一个字符首次出现的位置使用strpbrk和wcspbrk函数,通用版本是_tcspbrk:

char *strpbrk(
    const char *str,            // 在这个字符串中搜索
    const char *strCharSet);    // 要搜索的字符串,匹配任何一个字符均可
wchar_t *wcspbrk(
    const wchar_t *str,
    const wchar_t *strCharSet);

函数在源字符串str中找出最先含有搜索字符串strCharSet中任一字符的位置并返回,如果找不到则返回NULL。示例如下:

TCHAR szStr[] = TEXT("The 3 men and 2 boys ate 5 pigs");
TCHAR szStrCharSet[] = TEXT("0123456789");
LPTSTR lpSearch = NULL;

_tprintf(TEXT("1: %s\n"), szStr);

lpSearch = _tcspbrk(szStr, szStrCharSet);
_tprintf(TEXT("2: %s\n"), lpSearch);

lpSearch++;
lpSearch = _tcspbrk(lpSearch, szStrCharSet);
_tprintf(TEXT("3: %s\n"), lpSearch);

输出结果:

1: The 3 men and 2 boys ate 5 pigs
2: 3 men and 2 boys ate 5 pigs
3: 2 boys ate 5 pigs

5.转换字符串中的字符大小写

char *_strupr(char *str);
wchar_t *_wcsupr(wchar_t *str);

这两个函数的通用版本是_tcsupr。函数将str字符串中的小写字母转换为大写形式,其他字符不受影响,返回修改后的字符串指针。

char *_strlwr(char * str);
wchar_t *_wcslwr(wchar_t * str);

这两个函数的通用版本是_tcslwr。函数将str字符串中的大写字母转换为小写形式,其他字符不受影响,返回修改后的字符串指针。

示例如下:

TCHAR szStr[] = TEXT("WindowsAPI是一种强大的编程语言!");
_tprintf(TEXT("%s\n"), _tcsupr(szStr)); // WINDOWSAPI是一种强大的编程语言!
_tprintf(TEXT("%s\n"), _tcslwr(szStr)); // windowsapi是一种强大的编程语言!

按Ctrl + F5组合键编译运行,提示_wcsupr和_wcslwr函数可能不安全,建议使用安全版本的_wcsupr_s和_wcslwr_s函数,这两个安全版本函数的通用版本是_tcsupr_s和_tcslwr_s。C/C++语言中需要修改字符串的处理函数通常有一个安全版本,就是在函数名称后加一个_s后缀:

errno_t _strupr_s(
    char* str,               // 指定要转换的字符串,返回转换以后的字符串
    size_t numberOfElements);// str缓冲区的大小,字节单位,包括结尾的空字符,可以用
                             // _tcslen(str) + 1
errno_t _wcsupr_s(
    wchar_t* str,            // 指定要转换的字符串,返回转换以后的字符串
    size_t numberOfElements);// str缓冲区的大小,字符单位,包括结尾的空字符,可以用
                             // _tcslen(str) + 1

如果函数执行成功,则返回0;如果函数执行失败,则返回相关错误代码。_strlwr_s和_wcslwr_s的函数声明不再列出,格式都是相同的。

另外,把一个字符转换为大写字母的函数是toupper和towupper,通用版本是_totupper;把一个字符转换为小写字母的函数是tolower和towlower,通用版本是_totlower。非字母字符不做任何处理:

int toupper(int c);
int towupper(wint_t c);    // typedef unsigned short wint_t;

int tolower(int c);
int towlower(wint_t c);

示例如下:

TCHAR szStr[] = TEXT("Hello, Windows, Windows API program simple and 强大!");

for (size_t i = 0; i < _tcslen(szStr); i++)
{
    _tprintf(TEXT("%c"), _totupper(szStr[i]));
    // HELLO, WINDOWS, WINDOWS API PROGRAM SIMPLE AND 强大!
}

_tprintf(TEXT("\n"));
for (size_t i = 0; i < _tcslen(szStr); i++)
{
    _tprintf(TEXT("%c"), _totlower(szStr[i]));
    // hello, windows, windows api program simple and 强大!
}

6.字符串拼接

char* strcat(
    char*       strDestination,     // 目标字符串
    const char* strSource);         // 源字符串
wchar_t* wcscat(
    wchar_t*       strDestination,  // 目标字符串
    const wchar_t* strSource);      // 源字符串

这两个函数的通用版本是_tcscat。函数把源字符串strSource附加到目标字符串strDestination的后面,返回指向目标字符串的指针。函数不会检查目标缓冲区strDestination是否有足够的空间,可能造成缓冲区溢出。实际上任何修改字符串缓冲区的函数都会存在一个安全隐患,如果目标字符串缓冲区不够大,无法容纳新的字符串,那么会导致内存中的其他数据被破坏,建议使用安全版本的_tcscat_s函数:

errno_t strcat_s(
    char*       strDestination,
    size_t      numberOfElements,       // 目标字符串缓冲区的大小,字节单位
    const char* strSource);
errno_t wcscat_s(
    wchar_t*       strDestination,
    size_t         numberOfElements,    // 目标字符串缓冲区的大小,字符单位
    const wchar_t* strSource);

如果函数执行成功,则返回0;如果函数执行失败,则返回相关错误代码。示例如下:

TCHAR szStrDest[64] = TEXT("WindowsAPI");
TCHAR szStrSour[] = TEXT("是一种强大的编程语言!");
_tcscat_s(szStrDest, _countof(szStrDest), szStrSour);
_tprintf(TEXT("%s\n"), szStrDest);  // WindowsAPI是一种强大的编程语言!

_countof宏用于获取一个数组中的数组元素个数,本例中_countof(szStrDest)返回64。这里与sizeof进行比较,sizeof是求字节数,本例中如果设置为Unicode字符集,那么sizeof(szStrDest)返回128;而如果设置为多字节字符集,那么sizeof(szStrDest)返回64。

7.字符串复制

复制字符串的函数是strcpy和wcscpy,安全版本为strcpy_s和wcscpy_s,通用版本为_tcscpy_s:

errno_t strcpy_s(
    char*       strDestination,         // 目标字符串缓冲区的指针
    size_t      numberOfElements,       // 目标字符串缓冲区的大小,字节单位
    const char* strSource);             // 源字符串缓冲区的指针
errno_t wcscpy_s(
    wchar_t*       strDestination,      // 目标字符串缓冲区的指针
    size_t         numberOfElements,    // 目标字符串缓冲区的大小,字符单位
    const wchar_t* strSource);          // 源字符串缓冲区的指针

函数将源字符串strSource中的内容(包括字符串结尾的0字符)复制到目标字符串缓冲区strDestination,目标字符串缓冲区必须足够大以保存源字符串及其结尾的0字符。如果函数执行成功,则返回0;如果函数执行失败,则返回相关错误代码。示例如下:

TCHAR szStrDest[64];
TCHAR szStrSour[] = TEXT("WindowsAPI是一种强大的编程语言!");
_tcscpy_s(szStrDest, _countof(szStrDest), szStrSour);
_tprintf(TEXT("%s\n"), szStrDest);  // WindowsAPI是一种强大的编程语言!

在调用_tcscpy_s函数时,目标字符串缓冲区必须足够大以保存源字符串及其结尾的0字符,但是有时候某些字符串并不一定以0结尾(后面会遇到这种情况),例如下面的代码,pString是一个不以0结尾的字符串指针:

TCHAR szResType[128] = { 0 };

_tcscpy_s(szResType, 5, pString);

因为pString指向的字符串并不是以0结尾,所以pString可能指向一块很大的数据,然后有一个0字符,这时候调用_tcscpy_s函数就会出现目标缓冲区太小的错误提示。

在这种情况下,可以使用后面将要介绍的StringCchCopy函数:

TCHAR szResType[128] = { 0 };

StringCchCopy(szResType, 5, pString);

StringCchCopy函数只会从pString指向的字符串中复制5−1个字符,并把szResType缓冲区的第5个字符设置为0,要想得到5个字符的以0结尾的字符串,可以把StringCchCopy函数的第2个参数设置为5 + 1。强烈建议使用StringCchCopy代替_tcscpy_s函数!

当然,也可以使用内存复制函数memcpy_s,该函数可以指定目标缓冲区和源缓冲区的字节数(后面会介绍该函数),不会出现缓冲区溢出。

同样的理由,建议使用StringCchCat(后面有相关介绍)代替字符串拼接函数_tcscat和_tcscat_s。

8.字符串比较

比较两个字符串大小关系的函数是strcmp和wcscmp,通用版本为_tcscmp:

int strcmp(
    const char *string1,
    const char *string2);
int wcscmp(
    const wchar_t *string1,
    const wchar_t *string2);

函数对string1和string2执行序号(ASCII码值)比较并返回一个指示它们关系的值。返回值指明了string1和string2的大小关系,如表1.6所示。

表1.6

string1与string2的关系

小于0

string1小于string2

等于0

string1等于string2

大于0

string1大于string2

比较两个字符串的规则:逐个比较两个字符串中对应的字符,字符大小按照ASCII码值确定,从左向右开始比较,如果遇到不同字符,那么所遇第一对不同字符的大小关系就确定了两个字符串的大小关系;如果未遇到不同字符而某个字符串首先结束,那么先结束的字符串是较小的;否则两个字符串相等。例如:

TCHAR szStr1[] = TEXT("ABCDE"); // E的ASCII为0x45
TCHAR szStr2[] = TEXT("ABCDe"); // e的ASCII为0x65

int n = _tcscmp(szStr1, szStr2);
if (n > 0)
    _tprintf(TEXT("szStr1 大于 szStr2\n"));
else if (n == 0)
    _tprintf(TEXT("szStr1 等于 szStr2\n"));
else
    _tprintf(TEXT("szStr1 小于 szStr2\n"));
// 输出结果:szStr1 小于 szStr2

因为_tcscmp比较字符串按照ASCII值进行比较,所以字母要区分大小写。

_stricmp、_wcsicmp和_tcsicmp(通用版本)在比较字符串之前会首先将其转换成小写形式,适用于不区分大小写的字符串比较。

对于ASCII字符集顺序(就是ASCII值)和字典的字符顺序不同的区域设置,应该使用strcoll / wcscoll函数(通用版本_tcscoll)而不是_tcsicmp函数进行字符串比较,_tcscoll函数根据正在使用区域设置的代码页的LC_COLLATE类别设置比较两个字符串,而_tcsicmp则不受区域设置影响。在“C”区域设置下,ASCII字符集中的字符顺序与字典顺序相同,但是在其他区域设置中,ASCII字符集中的字符顺序可能与字典中的顺序不同,例如在某些欧洲代码页中,字符a(值0x61)位于字符ä(值0xE4)之前,但是在字典顺序中,字符ä在字符a之前。

LC_COLLATE是一组处理跟语言相关问题的规则,这些规则包括如何对字符串进行比较和排序等。按照C99标准的规定,程序在启动时区域设置为“C”。在区域设置“C”下,字符串的比较就是按照ASCII值逐字节地进行,这时_tcscoll与_tcsicmp函数没有区别;但是在其他区域设置下,字符串的比较方式可能就不同了,例如在简体中文区域设置下,_tcsicmp仍然按ASCII值比较,而_tcscoll对于汉字则是按拼音进行的(这与操作系统有关,Windows还支持按笔画排序,可以在区域和语言设置中进行修改):

int strcoll(
    const char *string1,
    const char *string2);
int wcscoll(
    const wchar_t *string1,
    const wchar_t *string2);

示例如下:

setlocale(LC_ALL, "chs");   // LC_ALL包括LC_COLLATE,英语国家则是en-US或English

TCHAR szStr1[] = TEXT("我爱老王");
// Unicode:11 62 31 72 01 80 8B 73 00 00  多字节:CE D2 B0 AE C0 CF CD F5 00
TCHAR szStr2[] = TEXT("我是老王");
// Unicode:11 62 2F 66 01 80 8B 73 00 00  多字节:CE D2 CA C7 C0 CF CD F5 00

int n = _tcscmp(szStr1, szStr2);
if (n > 0)
    _tprintf(TEXT("szStr1 > szStr2\n"));
else if (n == 0)
    _tprintf(TEXT("szStr1 == szStr2\n"));
else
    _tprintf(TEXT("szStr1 < szStr2\n"));
// 输出结果:szStr1 > szStr2

n = _tcscoll(szStr1, szStr2);
if (n > 0)
    _tprintf(TEXT("szStr1 > szStr2\n"));
else if (n == 0)
    _tprintf(TEXT("szStr1 == szStr2\n"));
else
    _tprintf(TEXT("szStr1 < szStr2\n"));
// 输出结果:szStr1 < szStr2

本例项目属性使用Unicode字符集,以后如果没有特别说明,那么项目均是使用Unicode字符集。

本例中“我爱老王”和“我是老王”在Unicode字符集和多字节字符集下的内存字节是不同的,在这两种字符集下使用_tcsicmp的比较结果是不同的。再次重复:Unicode是国际化编码,用一套字符集表示所有国家的字符;而ANSI是国家标准,同样的码值在不同的国家代表不同的字符。程序一开始就调用setlocale(LC_ALL, "chs");来设置中文区域设置,因此调用_tcscoll函数进行比较的结果就是“我爱老王”<“我是老王”。

如果不调用setlocale(LC_ALL, "chs");,就相当于设置了区域设置“C”,因为在程序启动时,将执行setlocale( LC_ALL, "C" );语句的等效项。

还是上面的代码,请查看不设置中文区域设置的情况:

TCHAR szStr1[] = TEXT("我爱老王");
// Unicode:11 62 31 72 01 80 8B 73 00 00  多字节:CE D2 B0 AE C0 CF CD F5 00
TCHAR szStr2[] = TEXT("我是老王");
// Unicode:11 62 2F 66 01 80 8B 73 00 00  多字节:CE D2 CA C7 C0 CF CD F5 00

int n = _tcscmp(szStr1, szStr2);
if (n > 0)
    _tprintf(TEXT("szStr1 > szStr2\n"));
else if (n == 0)
    _tprintf(TEXT("szStr1 == szStr2\n"));
else
    _tprintf(TEXT("szStr1 < szStr2\n"));
// 输出结果:szStr1 > szStr2

n = _tcscoll(szStr1, szStr2);
if (n > 0)
    _tprintf(TEXT("szStr1 > szStr2\n"));
else if (n == 0)
    _tprintf(TEXT("szStr1 == szStr2\n"));
else
    _tprintf(TEXT("szStr1 < szStr2\n"));
// 输出结果:szStr1 > szStr2

使用_tcsicmp进行比较的结果不变,而使用_tcscoll进行比较的结果变为“我爱老王”>“我是老王”。

9.分割字符串

用于分割字符串的函数是strtok、wcstok和_tcstok,安全版本为strtok_s、wcstok_s和_tcstok_s。函数声明如下:

char* strtok_s(
    char*       strToken,   // 要分割的字符串
    const char* strDelimit, // 分隔符字符串,分隔符字符串中的每个字符均为分割符
    char**      context);   // 返回strToken中剩余未被分割的部分,提供一个字符串
                            // 指针的指针即可
wchar_t* wcstok_s(
    wchar_t*       strToken,
    const wchar_t* strDelimit,
    wchar_t**      context);

当strtok_s / wcstok_s函数在参数strToken的字符串中发现参数strDelimit包含的分割字符时,会将该字符修改为字符0。在第一次调用时,参数strToken指向要分割的字符串,以后的调用则将参数strToken设置为NULL,每次调用成功函数会返回指向被分割出部分的指针,当字符串strToken中的字符查找到末尾时,函数返回NULL。需要注意的是,strtok_s / wcstok_s函数会破坏被分割的字符串。如果要分割的字符串strToken中不存在分隔符字符串strDelimit中指定的任何字符,函数会返回strToken字符串本身。示例如下:

TCHAR strToken[] = TEXT("A string\tof ,,tokens\nand some  more tokens");
TCHAR strDelimit[] = TEXT(" ,\t\n");    // 前面有个空格
LPTSTR lpToken = NULL;                  // 被分割出部分的指针
LPTSTR lpTokenNext = NULL;              // 剩余未被分割部分的指针

// 获取第一个字符串
lpToken = _tcstok_s(strToken, strDelimit, &lpTokenNext);

// 循环查找
while (lpToken != NULL)
{
    _tprintf(TEXT("%s\n"), lpToken);
    // 获取下一个
    lpToken = _tcstok_s(NULL, strDelimit, &lpTokenNext);
}
/*

输出结果:

A
string
of
tokens
and
some
more
tokens
*/

10.字符串快速排序

进行字符串快速排序的函数是qsort,安全版本为qsort_s:

void qsort(
    void*        base,     // 待排序的字符串数组
    size_t       num,      // 待排序的字符串数组中数组元素的个数
    size_t       width,    // 以字节为单位,各元素占用的空间大小
    int(__cdecl* compare)(const void*, const void*)); // 对字符串进行比较的回调函数
void qsort_s(
    void*        base,
    size_t       num,
    size_t       width,
    int(__cdecl* compare)(void*, const void*, const void*),
    void*        context); // 上面回调函数的参数

这个函数对于初学者比较复杂,因为涉及回调函数的概念。先看示例再作解释吧,在此以qsort函数为例:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
#include <locale.h>

// 回调函数声明
int compare(const void *arg1, const void *arg2);

int main()
{
    setlocale(LC_ALL, "chs");

    LPTSTR arrStr[] = {
        TEXT("架构风格之资源管理.AVI"),
        TEXT("模块化之合理内聚.AVI"),
        TEXT("总结.AVI"),
        TEXT("模块化之管理依赖.AVI"),
        TEXT("系统架构设计概述.AVI"),
        TEXT("架构风格之分布式.AVI")
    };
    qsort(arrStr, _countof(arrStr), sizeof(LPTSTR) , compare);

    for (int i = 0; i < _countof(arrStr); i++)
        _tprintf(TEXT("%s\n"), arrStr[i]);

    return 0;
}

int compare(const void *arg1, const void *arg2)
{
    // 因为arg1、arg2是数组元素的指针,所以需要*(LPTSTR *)
    return _tcscoll(*(LPTSTR *)arg1, *(LPTSTR *)arg2);
}

输出结果:

架构风格之分布式.AVI
架构风格之资源管理.AVI
模块化之管理依赖.AVI
模块化之合理内聚.AVI
系统架构设计概述.AVI
总结.AVI

qsort函数对指定数组中的元素进行排序。当然,数组元素也可以是其他类型,例如int类型。排序的规则是什么呢?在进行排序的时候,qsort函数会调用compare函数对两个数组元素进行比较(比较规则需要根据具体情况进行不同的设定),这就是回调函数的概念。回调函数compare由qsort函数负责调用,以后还会遇到由操作系统调用的回调函数。本例是升序排序,如果需要降序排序,只需要把_tcscoll函数的两个参数互换即可。

请注意:不同版本的VS的语法检查规则有所不同。上例中,变量arrStr是一个LPTSTR类型的数组,但是该数组中的数组元素都是常字符串指针,因此编译器可能会报错。此时把LPTSTR改为LPCTSTR类型即可,表示常字符串数组。以后如果遇到类似问题,请自行根据错误提示灵活处理。

数组元素排序完成以后,二分查找一个数组元素就很快了,这需要使用bsearch函数或安全版本的bsearch_s函数:

void* bsearch(
    const void*  key,       // 要查找的数据
    const void*  base,      // 要从中进行查找的数组
    size_t       num,       // 被查找数组中的数组元素个数
    size_t       width,     // 每个数组元素的长度,以字节为单位
    int(__cdecl* compare) (const void* key, const void* datum));
                           // 进行比较的回调函数

函数用二分查找法从数组元素base[0]~base[num-1]中查找参数key指向的数据。数组base中的数组元素应以升序排列,函数bsearch的返回值指向匹配项;如果没有发现匹配项,则返回NULL。bsearch函数的用法和qsort类似,此处不再举例。

11.字符串与数值型的相互转换

将字符串转换为双精度浮点型的函数是atof和_wtof,通用版本是_ttof:

double atof(const char*     str);
double _wtof(const wchar_t* str);

将字符串转换为整型或长整型的函数是atoi、_wtoi或atol、_wtol,通用版本是_ttoi或_ttol:

int atoi(const char*     str);
int _wtoi(const wchar_t* str);
long atol(const char*     str);
long _wtol(const wchar_t* str);

将字符串转换为64位整型或long long整型的函数是_atoi64、_wtoi64或atoll、_wtoll,通用版本是_ttoi64或_ttoll:

__int64 _atoi64(const char*    str);
__int64 _wtoi64(const wchar_t* str);
long long atoll(const char*     str);
long long _wtoll(const wchar_t* str);

上述函数并不要求字符串str必须是数值形式,在此以_ttof函数为例,假设字符串str为"−1.23456你好,老王",调用_ttof(str)函数返回的结果为double型的−1.23456。函数会跳过前面的空格字符,直到遇上数字或正负符号才开始转换,直到出现非数字或字符串结束标志时结束转换,并将转换后的数值结果返回。如果开头部分就是不可转换字符,例如"你好−1.23456你好,老王",则函数返回0.0。

将数值型转换为字符串的相关通用版本函数有_itot、_ltot、_ultot、_i64tot和_ui64tot:

char* _itoa(int                   value, char* buffer, int radix);
char* _ltoa(long                  value, char* buffer, int radix);
char* _ultoa(unsigned long        value, char* buffer, int radix);
char* _i64toa(long long           value, char* buffer, int radix);
char* _ui64toa(unsigned long long value, char* buffer, int radix);
wchar_t* _itow(int                   value, wchar_t* buffer, int radix);
wchar_t* _ltow(long                  value, wchar_t* buffer, int radix);
wchar_t* _ultow(unsigned long        value, wchar_t* buffer, int radix);
wchar_t* _i64tow(long long           value, wchar_t* buffer, int radix);
wchar_t* _ui64tow(unsigned long long value, wchar_t* buffer, int radix);

但是,我们知道修改字符串缓冲区的函数都存在一个缓冲区溢出安全隐患,因此建议使用这些函数的安全版本_itot_s、_ltot_s、_ultot_s、_i64tot_s和_ui64tot_s:

errno_t _itoa_s(int                   value, char* buffer, size_t size, int radix);
errno_t _ltoa_s(long                  value, char* buffer, size_t size, int radix);
errno_t _ultoa_s(unsigned long        value, char* buffer, size_t size, int radix);
errno_t _i64toa_s(long long           value, char* buffer, size_t size, int radix);
errno_t _ui64toa_s(unsigned long long value, char* buffer, size_t size, int radix);
errno_t _itow_s(int                   value, wchar_t* buffer, size_t size, int radix);
errno_t _ltow_s(long                  value, wchar_t* buffer, size_t size, int radix);
errno_t _ultow_s(unsigned long        value, wchar_t* buffer, size_t size, int radix);
errno_t _i64tow_s(long long           value, wchar_t* buffer, size_t size, int radix);
errno_t _ui64tow_s(unsigned long long value, wchar_t* buffer, size_t size, int radix);

参数value是要转换的数值;参数buffer是存放转换结果的字符串缓冲区;参数size用于指定缓冲区的大小;参数radix表示进制数,可以指定为2、8、10或16。如果函数执行成功,则返回0;如果函数执行失败,则返回相关错误代码。

例如下面的代码:

int n = 0x12CFFE20;
TCHAR szBuf[16] = { 0 };

_itot_s(n, szBuf, _countof(szBuf), 10);
_tprintf(TEXT("%s\n"), szBuf);      // 315620896

_itot_s(n, szBuf, _countof(szBuf), 16);
_tprintf(TEXT("%s\n"), szBuf);      // 12cffe20

前面介绍过,将字符串转换为双精度浮点型、整型或长整型、64位整型或long long整型的函数是_ttof、_ttoi或_ttol、_ttoi64或_ttoll,与之类似的还有 _tcstod、_tcstol、_tcstoul、_tcstoi64、_tcstoui64、_tcstoll和_tcstoull函数:

double             strtod(const char*        str, char**    endptr);
double             wcstod(const wchar_t*     str, wchar_t** endptr);
long               strtol(const char*        str, char**    endptr, int radix);
long               wcstol(const wchar_t*     str, wchar_t** endptr, int radix);
unsigned long      strtoul(const char*       str, char**    endptr, int radix);
unsigned long      wcstoul(const wchar_t*    str, wchar_t** endptr, int radix);
__int64            _strtoi64(const char*     str, char**    endptr, int radix);
__int64            _wcstoi64(const wchar_t*  str, wchar_t** endptr, int radix);
unsigned __int64   _strtoui64(const char*    str, char**    endptr, int radix);
unsigned __int64   _wcstoui64(const wchar_t* str, wchar_t** endptr, int radix);
long long          strtoll(const char*       str, char**    endptr, int radix);
long long          wcstoll(const wchar_t*    str, wchar_t** endptr, int radix);
unsigned long long strtoull(const char*      str, char**    endptr, int radix);
unsigned long long wcstoull(const wchar_t*   str, wchar_t** endptr, int radix);

可以看到,多了endptr和radix两个参数。endptr参数用于返回成功转换的最后一个字符之后的剩余字符串指针,可以设置为NULL;参数radix表示进制数,可以指定为2、8、10或16。

示例如下:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
#include <locale.h>

// 回调函数声明
int compare(const void *arg1, const void *arg2);

int main()
{
    setlocale(LC_ALL, "chs");

    LPCTSTR arrStr[] = {
        TEXT("4、原理—开发风格之资源管理.AVI"),
        TEXT("11、原理—总结.AVI"),
        TEXT("8、原理—模块化之管理依赖.AVI"),
        TEXT("6、原理—架构风格之适配与扩展.AVI"),
        TEXT("1、原理—系统架构设计概述.AVI"),
        TEXT("7、原理—模块化之重用与内聚.AVI"),
        TEXT("10、原理—模块化之确保扩展.AVI"),
        TEXT("3、原理—架构风格之分布式.AVI"),
        TEXT("9、原理—模块化之保持可用.AVI"),
        TEXT("2、原理—架构风格之系统结构.AVI"),
        TEXT("5、原理—架构风格之事件驱动.AVI"),
        TEXT("4、原理—架构风格之资源管理.AVI")
    };
    qsort(arrStr, _countof(arrStr), sizeof(LPCTSTR), compare);

    for (int i = 0; i < _countof(arrStr); i++)
        _tprintf(TEXT("%s\n"), arrStr[i]);

    return 0;
}

int compare(const void *arg1, const void *arg2)
{
    LPTSTR p1 = NULL;
    LPTSTR p2 = NULL;   // p1和p2返回的是数字字符后面的字符串
    double d1 = _tcstod(*(LPTSTR *)arg1, &p1);
    double d2 = _tcstod(*(LPTSTR *)arg2, &p2);

    // 先比较数字,如果数字相同,就比较数字后面的字符串
    if (d1 != d2)
    {
        if (d1 > d2)
            return 1;
        else
            return -1;
    }
    else
    {
        return _tcscoll(p1, p2);
    }
}

输出结果:

1、原理—系统架构设计概述.AVI
2、原理—架构风格之系统结构.AVI
3、原理—架构风格之分布式.AVI
4、原理—架构风格之资源管理.AVI
4、原理—开发风格之资源管理.AVI
5、原理—架构风格之事件驱动.AVI
6、原理—架构风格之适配与扩展.AVI
7、原理—模块化之重用与内聚.AVI
8、原理—模块化之管理依赖.AVI
9、原理—模块化之保持可用.AVI
10、原理—模块化之确保扩展.AVI
11、原理—总结.AVI

本例模拟的是Windows资源管理器对文件进行排序的结果。

12.格式化字符串

printf和wprintf函数用于向标准输出设备按指定格式输出信息。函数声明如下:

int printf(const char* format [, argument]...);
int wprintf(const wchar_t* format [, argument]...);

_tprintf是printf和wprintf的通用版本,如果定义了_UNICODE,则_tprintf会被转换为wprintf,否则为printf。输出中文的时候需要setlocale(LC_ALL, "chs");。

建议使用安全版本的printf_s和wprintf_s函数,通用版本为_tprintf_s。安全版本的printf_s、wprintf_s与printf、wprintf的函数声明是相同的。printf_s和wprintf_s函数会检查format参数中的格式化字符串是否有效,而printf和wprintf函数仅仅检查format参数是否为NULL。

表1.7列出了一些与printf类似的函数,与printf不同的是,表1.7中的这些函数是输出到缓冲区,而不是输出到标准输出设备。

表1.7

ANSI版本 宽字符版本 通用版本 通用安全版本
可变数目的参数
标准版 sprintf swprintf _stprintf _stprintf_s
限定最大长度版 _snprintf _snwprintf _sntprintf _sntprintf_s
Windows版 wsprintfA wsprintfW wsprintf
参数数组的指针
标准版 vsprintf vswprintf _vstprintf _vstprintf_s
限定最大长度版 _vsnprintf _vsnwprintf _vsntprintf _vsntprintf_s
Windows版 wvsprintfA wvsprintfW wvsprintf

printf前面的字母s表示string,即输出到字符串缓冲区。

这么多格式化输出到缓冲区的类printf函数,应该如何选择呢?为了避免缓冲区溢出,应该使用限定最大长度的_sntprintf_s或_vsntprintf_s函数,这两个函数的声明如下:

int _snprintf_s(char*      buf, size_t size, size_t count, const char*    format  
[, argument] ...);
int _snwprintf_s(wchar_t*  buf, size_t size, size_t count, const wchar_t* format  
[, argument] ...);
int _vsnprintf_s(char*     buf, size_t size, size_t count, const char*    format,   
va_list argptr);
int _vsnwprintf_s(wchar_t* buf, size_t size, size_t count, const wchar_t* format,   
va_list argptr);

buf参数用于指定输出的字符串缓冲区;size参数用于指定缓冲区的大小;count参数用于指定要输出到缓冲区的最大字符数,通常可以指定为_TRUNCATE(-1);format参数为格式化控制字符串;_sntprintf_s函数传递的是不定数目的参数,而_vsntprintf_s函数传递的是一个va_list类型的参数列表指针(实际编程中很少用到该类型)。

在Windows程序设计中,可以使用Windows版的wsprintf或wvsprintf函数,函数声明如下:

int WINAPIV wsprintf(       // #define WINAPIV __cdecl
    _Out_ LPTSTR  buf,
    _In_  LPCTSTR format,
    ...);
int WINAPI wvsprintf(       // #define WINAPI  __stdcall
    _Out_ LPTSTR  buf,
    _In_  LPCTSTR format,
    _In_ va_list  arglist);

从函数声明中可以看出,上面两个函数是有缓冲区溢出安全隐患的。微软也声明请勿使用wsprintf或wvsprintf函数,应该使用C/C++运行库提供的新增安全版本函数StringCbPrintf、StringCchPrintf或StringCbVPrintf、StringCchVPrintf来替换它们。

StringCbPrintf、StringCchPrintf、StringCbVPrintf和StringCchVPrintf的函数声明如下:

STRSAFEAPI StringCbPrintf(          // #define STRSAFEAPI __inline HRESULT __stdcall
    _Out_ LPTSTR  pszDest,
    _In_  size_t  cbDest,           // 目的缓冲区的大小,以字节为单位
    _In_  LPCTSTR pszFormat,
    ...);
STRSAFEAPI StringCchPrintf(
    _Out_ LPTSTR  pszDest,
    _In_  size_t  cchDest,          // 目的缓冲区的大小,以字符为单位
    _In_  LPCTSTR pszFormat,
    ...);
STRSAFEAPI StringCbVPrintf(
    _Out_ LPTSTR  pszDest,
    _In_  size_t  cbDest, 
    _In_  LPCTSTR pszFormat,
    _In_  va_list argList);
STRSAFEAPI StringCchVPrintf(
    _Out_ LPTSTR  pszDest,
    _In_  size_t  cchDest, 
    _In_  LPCTSTR pszFormat,
    _In_  va_list argList)

cbDest参数用于指定目标缓冲区的大小,以字节为单位,这个值必须足够大,以容纳格式化字符串加上结尾的0字符,允许的最大字节数是STRSAFE_MAX_CCH(2147483647) sizeof(TCHAR);cchDest参数用于指定目标缓冲区的大小,以字符为单位,这个值必须足够大,以容纳格式化字符串加上结尾的0字符,允许的最大字符数是STRSAFE_MAX_CCH。

上面4个函数需要包含strsafe.h头文件。返回值类型为HRESULT,可以使用SUCCEEDED宏来测试函数是否执行成功,稍后再介绍HRESULT数据类型。

总结一下,为了避免缓冲区溢出,应该使用限定最大长度的_sntprintf_s或_vsntprintf_s函数;在Windows程序设计中,可以使用C/C++运行库提供的新增安全版本函数StringCbPrintf、StringCchPrintf、StringCbVPrintf和StringCchVPrintf。在很多地方我可能使用了不安全的_tprintf和wsprintf等函数,请读者自行按上述说明进行使用。

下面以StringCchPrintf为例说明格式化字符串函数的使用:

#include <Windows.h>
#include <strsafe.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int   
nCmdShow)
{
    TCHAR szName[] = TEXT("老王");
    TCHAR szAddress[] = TEXT("山东济南");
    int nAge = 18;
    TCHAR szBuf[128] = { 0 };
    HRESULT hResult = E_FAIL;

    hResult = StringCchPrintf(szBuf, _countof(szBuf),
        TEXT("自我介绍\n我是:%s 来自:%s 年龄:%d\n"), szName, szAddress, nAge);
    if (SUCCEEDED(hResult))
        MessageBox(NULL, szBuf, TEXT("格式化字符串的使用"), MB_OKCANCEL | MB_ICONINFORMATION);
    else
        MessageBox(NULL, TEXT("函数执行失败"), TEXT("错误提示"), MB_OKCANCEL | MB_ICONWARNING);

    return 0;
}

程序执行效果如图1.11所示。

图1.11

13.字符串格式化为指定类型的数据

前面我们学习了一系列格式化字符串函数,实际编程中可能还需要把字符串格式化为指定类型的数据。例如sscanf_s(多字节版本)和swscanf_s(宽字符版本)函数可以从字符串缓冲区将数据读取到每个参数中。这两个函数与scanf的区别是,后者以标准输入设备为输入源,而前者以指定的字符串为输入源。上述两个函数是sscanf和swscanf的安全版本,函数声明如下:

int sscanf_s(
    const char* buffer,     // 字符串缓冲区
    const char* format      // 格式控制字符串,支持条件限定和通配符
    [, argument] ...);      // 参数指针,返回数据到每个参数
int swscanf_s(
    const wchar_t* buffer,
    const wchar_t* format 
    [,argument] ...);

例如下面的示例把一个十六进制形式的字符串转换为十六进制数值:

DWORD dwTargetRVA;
TCHAR szBuf[32] = TEXT("1234ABCD");

swscanf_s(szBuf, TEXT("%X"), &dwTargetRVA);     // 结果:dwTargetRVA等于十六进制的1234ABCD

14.Windows中的一些字符串函数

Windows也提供了各种字符串处理函数,例如lstrlen、lstrcpy、lstrcat和lstrcmp等。因为安全性问题,有些不建议使用。本节简要介绍一下这些函数。

(1)lstrlen

lstrlen函数用于计算字符串长度,以字符为单位:

int WINAPI lstrlen(_In_ LPCTSTR lpString);

(2)lstrcpy与StringCchCopy

lstrcpy函数用于字符串复制:

LPTSTR WINAPI lstrcpy(
    _Out_ LPTSTR lpString1,
    _In_  LPTSTR lpString2);

不建议使用这个函数,可能造成缓冲区溢出。缓冲区溢出是应用程序中许多安全问题的根源,在最坏的情况下,如果lpstring1是基于堆栈的缓冲区,则缓冲区溢出可能会导致攻击者向进程中注入可执行代码。

除了新的安全字符串函数,C/C++运行库还新增了一些函数,用于在执行字符串处理时提供更多控制。例如StringCchCopy函数:

HRESULT StringCchCopy(
    _Out_ LPTSTR  pszDest,  // 目标缓冲区
    _In_  size_t  cchDest,  // 目标缓冲区的大小,以字符为单位
    _In_  LPCTSTR pszSrc);  // 源字符串

cchDest参数指定的大小必须大于或等于字符串pszSrc的长度加1,以容纳复制的源字符串和终止的空字符。cchDest参数允许的最大字符数为STRSAFE_MAX_CCH(#define STRSAFE_MAX_CCH 2147483647)。该函数需要包含strsafe.h头文件。

StringCchCopy函数的返回值是HRESULT类型,可以返回表1.8所列的值。

表1.8

返回值

含义

S_OK

源字符串已被成功复制

STRSAFE_E_INVALID_PARAMETER

cchDest参数为NULL或大于STRSAFE_MAX_CCH

STRSAFE_E_INSUFFICIENT_BUFFER

目标缓冲区空间不足,复制操作失败,但是目标缓冲区包含被截断的以0结尾的源字符串的一部分

可以使用SUCCEEDED和FAILED宏来测试函数的返回值,这两个宏在strsafe.h头文件中定义如下:

#define SUCCEEDED(hr)   (((HRESULT)(hr)) >= 0)
#define FAILED(hr)      (((HRESULT)(hr)) < 0)

再看一下StringCchCopy函数返回值的定义:

#define S_OK                            ((HRESULT)0L)
#define STRSAFE_E_INVALID_PARAMETER     ((HRESULT)0x80070057L)
#define STRSAFE_E_INSUFFICIENT_BUFFER   ((HRESULT)0x8007007AL)
typedef _Return_type_success_(return >= 0) long HRESULT;

SUCCEEDED宏用于判断返回值是否大于等于0,可以这样使用这个宏:

HRESULT hr = StringCchCopy(...);
if (SUCCEEDED(hr))
{
    // StringCchCopy函数执行成功
}

(3)lstrcat与StringCchCat

lstrcat函数把一个字符串附加到另一个字符串后面:

LPTSTR WINAPI lstrcat(
    _Inout_ LPTSTR lpString1,
    _In_    LPTSTR lpString2);

同样,不建议使用这个函数,因为可能造成缓冲区溢出。建议使用C/C++运行库的新增函数StringCchCat:

HRESULT StringCchCat(
    _Inout_ LPTSTR  pszDest,    // 目标缓冲区
    _In_    size_t  cchDest,    // 目标缓冲区的大小,以字符为单位
    _In_    LPCTSTR pszSrc);    // 源字符串

目标缓冲区的大小必须大于或等于pszSrc的长度加pszDest的长度再加1,以容纳两个字符串和终止的空字符。同样,允许的最大字符数为STRSAFE_MAX_CCH。StringCchCat函数的返回值和用法与StringCchCopy函数类似。

(4)lstrcmp、lstrcmpi与CompareStringEx

lstrcmp函数用于比较两个字符串。如果需要执行不区分大小写的比较,则可以使用lstrcmpi函数:

int WINAPI lstrcmp(
    _In_ LPCTSTR lpString1,
    _In_ LPCTSTR lpString2);
int WINAPI lstrcmpi(
    _In_ LPCTSTR lpString1,
    _In_ LPCTSTR lpString2);

这两个函数的返回值和用法与前面介绍的C/C++字符串比较函数类似。

实际上,lstrcmp 与 lstrcmpi 函数在内部使用当前区域设置调用 CompareStringEx 函数,CompareStringEx函数是CompareString函数的扩展版本。Ex是Extend或Expand的缩写,意思是扩展、增强。后面大家会看到很多Windows API函数都有Ex扩展版本。

int CompareStringEx(
    _In_opt_ LPCWSTR          lpLocaleName,         // 区域设置名称
    _In_     DWORD            dwCmpFlags,           // 指示函数如何比较两个字符串的标志
    _In_     LPCWSTR          lpString1,            // 字符串1
    _In_     int              cchCount1,            // 字符串1的字符长度,可以设置为-1
    _In_     LPCWSTR          lpString2,            // 字符串2
    _In_     int              cchCount2,            // 字符串2的字符长度,可以设置为-1
    _In_opt_ LPNLSVERSIONINFO lpVersionInformation, // 一般设置为NULL
    _In_opt_ LPVOID           lpReserved,           // 保留参数
    _In_opt_ LPARAM           lParam);              // 保留参数

虽然CompareStringEx函数参数比较多,但是通常将最后3个参数设置为NULL。

第1个参数lpLocaleName是指向区域设置名称的字符串,或者是下列预定义值之一。

LOCALE_NAME_INVARIANT

LOCALE_NAME_SYSTEM_DEFAULT

LOCALE_NAME_USER_DEFAULT

区域设置可以让函数以符合当地语言习惯的方式来比较字符串,得到的结果对用户来说更有意义。

第2个参数dwCmpFlags是指示函数如何比较两个字符串的标志。通常情况下将其设置为0即可。该参数可以是表1.9所列值的组合,在此只列举比较重要的几个。

表1.9

标志

含义

LINGUISTIC_IGNORECASE或NORM_IGNORECASE

忽略大小写

NORM_IGNORESYMBOLS

忽略符号和标点符号

NORM_LINGUISTIC_CASING

对大小写使用语言规则,而不是文件系统规则

SORT_DIGITSASNUMBERS

将字符串前面的数字字符解释为数值型数字

第3个参数lpString1和第5个参数lpString2指定要比较的两个字符串;参数cchCount1和cchCount2分别指定这两个字符串的字符长度,不包括字符串终止的空字符。如果确定lpString1和lpString2指向的字符串分别都是以零结尾的,那么可以指定cchCount1和cchCount2为一个负数,例如−1,这时函数可以自动计算字符串的长度。

函数执行成功,返回值是CSTR_LESS_THAN、CSTR_EQUAL或CSTR_GREATER_THAN,分别代表lpString1小于、等于或大于lpString2。这3个宏在WinNls.h头文件中定义如下:

#define CSTR_LESS_THAN           1        // 字符串 1 小于字符串 2
#define CSTR_EQUAL               2        // 字符串 1 等于字符串 2
#define CSTR_GREATER_THAN        3        // 字符串 1 大于字符串 2

lstrcmp与lstrcmpi函数在内部使用当前区域设置调用CompareStringEx函数,并把返回值减去2,这是为了和C运行库的其他字符串比较函数的返回值结果一致。

函数执行失败,返回值为0,可以通过调用GetLastError函数获取错误代码。关于GetLastError函数以后再作介绍。

还是将字符串转换为数值型的示例,打开Chapter1\FileSort项目,修改compare回调函数如下:

int compare(const void *arg1, const void *arg2)
{
    return CompareStringEx(LOCALE_NAME_SYSTEM_DEFAULT, SORT_DIGITSASNUMBERS,
        *(LPTSTR *)arg1, -1, *(LPTSTR *)arg2, -1, NULL, NULL, NULL) - 2;
}

程序执行效果是相同的,事实上CompareStringEx函数更好用一些。

还有一个CompareStringOrdinal函数执行的是序号比较,可以用于不考虑区域设置的情况。如果是比较程序内部所用的字符串(例如文件路径、注册表项/值),便可以使用这个函数:

int CompareStringOrdinal(
    _In_ LPCWSTR lpString1,
    _In_ int     cchCount1,
    _In_ LPCWSTR lpString2,
    _In_ int     cchCount2,
    _In_ BOOL    bIgnoreCase);  // 是否忽略大小写,TRUE为忽略,FALSE为不忽略

函数的参数、返回值和用法与CompareStringEx类似。

如果一个Windows API函数需要字符串参数,则该函数通常有两个版本。例如MessageBox函数有MessageBoxA和MessageBoxW两个版本,MessageBoxA接受ANSI字符串,而MessageBoxW接受Unicode字符串。这两个函数的函数原型如下:

int WINAPI MessageBoxA(
    _In_opt_ HWND   hWnd,
    _In_opt_ LPCSTR lpText,
    _In_opt_ LPCSTR lpCaption,
    _In_ UINT       uType);
int WINAPI MessageBoxW(
    _In_opt_ HWND    hWnd,
    _In_opt_ LPCWSTR lpText,
    _In_opt_ LPCWSTR lpCaption,
    _In_ UINT        uType);

MessageBoxW版本接受Unicode字符串,函数名末尾的大写字母W代表Wide;MessageBoxA末尾的大写字母A表明该函数接受ANSI字符串。

我们平时在写程序的时候直接调用MessageBox即可,不需要调用MessageBoxW或MessageBoxA。在WinUser.h中,MessageBox实际上是一个宏,它的定义如下:

#ifdef UNICODE
    #define MessageBox  MessageBoxW
#else
    #define MessageBox  MessageBoxA
#endif

编译器根据是否定义了UNICODE来决定是调用MessageBoxA还是MessageBoxW。

MessageBox由动态链接库User32.dll导出,导出表中不存在MessageBox函数,只有MessageBoxA 和MessageBoxW这两个函数。MessageBoxA的内部源代码只是一个转换层,它负责分配内存,将ANSI字符串转换为Unicode字符串,然后使用转换后的Unicode字符串调用MessageBoxW;MessageBoxW函数执行完毕返回以后,MessageBoxA会释放它的内存缓冲区。所有这些转换都在后台进行,为了执行这些字符串转换,系统会产生时间和内存上的开销。从Windows NT开始,Windows的内核版本都完全使用Unicode来构建,Microsoft也逐渐开始倾向于只提供API函数的Unicode版本。因此为了使应用程序的执行更高效,我们应该使用Unicode字符集来开发应用程序。

在创建供其他软件开发人员使用的动态链接库时,可以选择在动态链接库中导出两个函数版本:一个是ANSI版本;另一个是Unicode版本。在ANSI版本中,只是分配内存并执行必要的字符串转换,然后调用该函数的Unicode版本。

MultiByteToWideChar函数可以将一个多字节字符串转换为宽字符串:

int MultiByteToWideChar(
    _In_      UINT   CodePage,      // 执行转换时使用的代码页
    _In_      DWORD  dwFlags,       // 指定转换类型的标志,一般设置为0
    _In_      LPCSTR lpMultiByteStr,// 指向要转换的多字节字符串的指针
    _In_      int    cbMultiByte,   // 要转换的多字节字符串的大小,以字节为单位,
                                    // 可以为-1
    _Out_opt_ LPWSTR lpWideCharStr, // 指向接收转换以后宽字符串的缓冲区的指针
    _In_      int    cchWideChar);  // lpWideCharStr指向的缓冲区的大小,以字符为单位

第1个参数CodePage指定执行转换时使用的代码页。该参数可以设置为操作系统中安装或可用的任何代码页的值,还可以指定为表1.10所列的值。

表1.10

常量

含义

CP_ACP

系统默认的Windows ANSI代码页

CP_OEMCP

当前系统OEM代码页

CP_SYMBOL

符号代码页(42)

CP_THREAD_ACP

当前线程的Windows ANSI代码页

CP_UTF8

UTF-8

第2个参数dwFlags指定转换类型的标志,默认值为MB_PRECOMPOSED(1),它会影响带变音符号(比如重音)的字符。一般情况下不使用该参数,直接设置为0即可。

第4个参数cbMultiByte指定要转换的多字节字符串的大小,以字节为单位,字母cb表示Count Byte字节数。如果确定lpMultiByteStr参数指向的字符串以零结尾,可以将该参数设置为−1,那么函数将处理整个lpMultiByteStr字符串,包括终止的空字符;如果cbMultiByte参数设置为0,那么函数将失败;如果cbMultiByte参数设置为正整数,那么函数将精确处理指定的字节数;如果指定的大小不包含终止的空字符,那么生成的Unicode字符串也不以空字符结尾。

第6个参数cchWideChar指定lpWideCharStr指向缓冲区的大小(单位为字符),字母cch表示Count CHaracter字符数。如果该参数设置为0,则函数返回所需的缓冲区大小(以字符为单位),包括终止的空字符。在这种情况下,函数不会在lpWideCharStr参数指向的缓冲区中返回数据。通常情况下,我们不知道需要多大的缓冲区,可以两次调用MultiByteToWideChar函数,第一次把lpWideCharStr参数设置为NULL,把cchWideChar参数设置为0,调用MultiByteToWideChar函数,函数返回所需的缓冲区大小,我们根据返回值分配合适的缓冲区,并指定参数cchWideChar的大小与其相等,然后进行第二次调用。

如果函数执行成功,则返回向lpWideCharStr参数指向缓冲区写入的字符数;如果函数执行失败,则返回0,可以通过调用GetLastError函数获取错误信息。

用MultiByteToWideChar函数实现一个简单的示例:

LPCSTR lpMultiByteStr = "Windows API程序设计";

// 第一次调用,获取所需缓冲区大小
int nCchWideChar = MultiByteToWideChar(CP_ACP, 0, lpMultiByteStr, -1, NULL, 0);

// 分配合适大小的缓冲区,进行第二次调用
LPWSTR lpWideCharStr = new WCHAR[nCchWideChar];     // new是C++中用于动态内
                                                    // 存分配的操作符
MultiByteToWideChar(CP_ACP, 0, lpMultiByteStr, -1, lpWideCharStr, nCchWideChar);

MessageBoxW(NULL, lpWideCharStr, L"Caption", MB_OK);
delete[] lpWideCharStr;               // delete是C++中用于释放内存的操作符

对应地,WideCharToMultiByte函数可以将宽字符串转换为多字节字符串,函数声明如下:

int WideCharToMultiByte(
    _In_      UINT    CodePage,
    _In_      DWORD   dwFlags,
    _In_      LPCWSTR lpWideCharStr,
    _In_      int     cchWideChar,
    _Out_opt_ LPSTR   lpMultiByteStr,
    _In_      int     cbMultiByte,
    _In_opt_  LPCSTR  lpDefaultChar,        // 在指定的代码页中遇到无法表示的
                                            // 字符时要使用的字符
    _Out_opt_ LPBOOL  lpUsedDefaultChar);   // 返回是否在转换过程中使用了上面
                                            // 指定的默认字符

这个函数的用法和MultiByteToWideChar函数是一样的,只是多了最后两个参数,这两个参数通常都可以设置为NULL。

参数lpDefaultChar表示转换过程中在指定的代码页中遇到无法表示的字符时作为替换的字符,可以将该参数设置为NULL表示使用系统默认值。默认字符通常是一个问号,这对文件名来说是非常危险的,因为问号是一个通配符。要获得系统默认字符,可以调用GetCPInfo或GetCPInfoEx函数。

lpUsedDefaultChar参数指向一个布尔变量。在宽字符串中,如果至少有一个字符不能转换为对应的多字节形式,则函数就会把lpUsedDefaultChar参数指向的BOOL变量设置为TRUE;如果所有字符都能成功转换,就会把lpUsedDefaultChar参数指向的BOOL变量设置为FALSE。对于后者,我们可以在函数返回后测试该变量,以验证宽字符串是否已全部成功转换。

在用sizeof运算符计算结构体所占字节数时,并不是简单地将结构体中所有字段各自占的字节数相加,这里涉及内存对齐的问题。内存对齐是操作系统为了提高内存访问效率而采取的策略,操作系统在访问内存的时候,每次读取一定的长度(这个长度是操作系统默认的对齐数,或者默认对齐数的整数倍)。如果没有对齐,为了访问一个变量可能会产生二次访问。比如有的平台每次都是从偶地址处开始读取数据,对于一个double类型的变量,如果从偶地址单元处开始存放,则只需一个读取周期即可读取到该变量;但是如果从奇地址处开始存放,则需要两个读取周期才可以读取到该变量。

如图1.12所示,假设内存对齐粒度为8,系统一次读取8字节。读取f1的时候,系统会读取内存单元0~7,但是发现f1还没有读取完,所以还需要再读取一次;而读取f2只需要一次便可以读取完。操作系统这样做的原因是拿空间换时间,提高效率。

图1.12

可以通过预编译命令#pragma pack(n)来改变对齐系数,其中的n就是指定的对齐系数,对齐系数不能任意设置,只能是内置数据类型的字节数,如1(char)、2(short)、4(int)、8(double),不能是3、5等。例如,可以按如下方式改变一个结构体的对齐系数:

#pragma pack(4)
struct MyStruct
{
    int a;
    char b;
    double c;
    float d;
};
#pragma pack()

如果需要获取系统默认的对齐系数,可以在源代码中使用#pragma pack(show)命令,编译运行。如果是Win32程序,会在VS底部的输出窗口显示:warning C4810: 杂注pack(show)的值== 8;如果是Win64,则默认对齐系数是16。

对于标准数据类型,例如char、int、float、double,它们的存放地址是其所占字节长度的整数倍。在Win32程序中,这些基本数据类型所占字节数为char = 1,int = 4,float = 4,double = 8。对于非标准数据类型,比如结构体,要遵循以下对齐原则。

原则1:字段的对齐规则,第一个字段放在offset为0的地方,以后每个字段的对齐按照#pragma pack(n)指定的数值和这个字段数据类型所占字节数中比较小的那个进行对齐。

原则2:结构体的整体对齐规则,在各个字段完成对齐之后,结构体本身也要进行对齐,对齐将按照#pragma pack(n)指定的数值和结构体所有字段中占用字节数最大的那个字段所占字节数(假设为m)中,比较小的那个进行对齐,也就是说结构体的大小是min(n, m)的整数倍。

原则3:结构体作为成员的情况,如果一个结构体里有其他结构体作为成员,则结构体成员要从其内部最大字段大小的整数倍地址开始存储。

举一个结构体对齐的例子:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

#pragma pack(4)
struct MyStruct
{
    int a;      // 存放在内存单元0~3
    char b;     // min(1, pragma pack(4))等于1,所以存放在内存单元4
    double c;   // min(8, pragma pack(4))等于4,所以从内存单元8开始存放,存放
                // 在内存单元8~15
    float d;    // min(4, pragma pack(4))等于4,所以从内存单元16开始存放,存放
                // 在内存单元16~19
};              // MyStruct正好占用20个内存字节,是min(8, pragma pack(4))的整数倍

struct MyStruct2 {
    char a;     // 存放在内存单元0
    MyStruct b; // MyStruct结构的最大字段为8,所以从内存单元8开始存放,存放在内存
                // 单元8~27
    double c;   // min(8, pragma pack(4))等于4,所以从内存单元28开始存放,存放
                // 在内存单元28~31
};              // MyStruct2正好占用32个内存字节,是min(8, pragma pack(4))的
                // 整数倍
#pragma pack()

int main()
{
    _tprintf(TEXT("MyStruct = %d\n"), sizeof(MyStruct));    // 20
    _tprintf(TEXT("MyStruct2 = %d\n"), sizeof(MyStruct2));  // 32

    return 0;
}

如果不指定#pragma pack(4)Win32程序的默认对齐系数则是8,在这种情况下,这两个结构体的对齐情况如下所示:

struct MyStruct
{
    int a;      // 存放在内存单元0~3
    char b;     // min(1, pragma pack(8))等于1,所以存放在内存单元4
    double c;   // min(8, pragma pack(8))等于8,所以从内存单元8开始存放,存放在
                // 内存单元8~15
    float d;    // min(4, pragma pack(8))等于4,所以从内存单元16开始存放,存放
                // 在内存单元16~19
};              // MyStruct占用20个内存字节,不是min(8, pragma pack(8))的整数
                // 倍,所以是24

struct MyStruct2 {
    char a;     // 存放在内存单元0
    MyStruct b; // MyStruct结构的最大字段为8,所以从内存单元8开始存放,存放在内存
                // 单元8~31
    double c;   // min(8, pragma pack(8))等于8,所以从内存单元32开始存放,存放
                // 在内存单元32~40
};              // MyStruct2正好占用40个内存字节,是min(8, pragma pack(8))的
                // 整数倍

int main()
{
    _tprintf(TEXT("MyStruct = %d\n"), sizeof(MyStruct));    // 24
    _tprintf(TEXT("MyStruct2 = %d\n"), sizeof(MyStruct2));  // 40

    return 0;
}

有了上面的知识,我们可以按照数据类型来调整结构体内部字段的先后顺序以减少内存的消耗,例如我们将结构体MyStruct中的顺序调整为MyStruct2,sizeof(MyStruct)的结果为12,而sizeof (MyStruct)的结果为8:

#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

struct MyStruct
{
    char a;
    int b;
    char c;
};

struct MyStruct2
{
    char a;
    char c;
    int b;
};

int main()
{
 _tprintf(TEXT("MyStruct = %d\n"), sizeof(MyStruct));    // 12
 _tprintf(TEXT("MyStruct2 = %d\n"), sizeof(MyStruct2));  // 8

    return 0;
}

本章介绍了大量的基础知识,特别是字符串话题,这是在程序开发过程中必不可缺的。让我们愉快地进入第2章的学习,一起打开Windows程序设计之门!

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e56948”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


相关图书

云原生测试实战
云原生测试实战
Kubernetes快速入门(第2版)
Kubernetes快速入门(第2版)
Kubernetes零基础实战
Kubernetes零基础实战
深入浅出Windows API程序设计:核心编程篇
深入浅出Windows API程序设计:核心编程篇
云原生技术中台:从分布式到云平台设计
云原生技术中台:从分布式到云平台设计
持续集成与持续交付实战:用Jenkins、Travis CI和CircleCI构建和发布大规模高质量软件
持续集成与持续交付实战:用Jenkins、Travis CI和CircleCI构建和发布大规模高质量软件

相关文章

相关课程