已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也

已经过去了很长一段时间,感觉顶楼的有些地方值得重新考虑,这里重新总结一下。

这里将不会讨论任何与MFC有关的内容。因为MFC也需要熟练的Win32基础才能正确使用,现在来看,掌握Win32远比掌握MFC要重要得多。

XXXXXXXXXXXXXXXXXXXXXXXXXX/en-us/library/windows/desktop/dd374083(v=vs.85).aspx

C/C++在Visual C++中的字符串编码

源代码可以保存为ANSI格式、Unicode格式(含BOM)、Unicode big endian格式(含BOM)、UTF-8(含BOM)。

char[]字符串字面量"xxx"所使用的编码,是编译时系统当前ANSI代码页的编码。简体中文系统中就是GBK。

wchar_t[]字符串字面量L"xxx"所使用的编码,是UTF-16编码。

C/C++运行库使用的编码,是运行时系统当前ANSI代码页的编码。

虽然C/C++运行库也支持wchar_t[],但是仅推荐使用少数功能,因为C90Amendment1大多数功能是在Unicode还没有普及的时候发明的,因此大多数功能实现得不可靠。建议使用WinAPI实现需要wchar_t[]的功能。

WinAPI头文件windows.h

Windows系统调用是由Win32 API(WinAPI)进行的。Win32 API的最初版本是1993年的Windows NT,它设计了两套WinAPI:

  • ANSI版本——以-A结尾,使用char[]作为字符串类型,使用ANSI字符集
  • Unicode版本——以-W结尾,使用wchar_t[]作为字符串类型,使用Unicode字符集

后者(Unicode版本)是Windows NT原生的,前者是Windows NT通过自动转换字符串模拟的。

使用WinAPI需要引入头文件windows.h。

如果在引入windows.h之前定义了UNICODE宏(即选择“使用Unicode字符集”),则会将没有结尾的WinAPI映射到-W结尾的WinAPI上;如果没有定义这个宏(即选择“使用多字节字符集”或“未设置”),则会映射到-A结尾的WinAPI上。

为了表示通用的WinAPI字符类型,头文件定义了一个类型TCHAR,当定义了UNICODE宏,TCHAR它会映射到wchar_t,否则会映射到char;为了重定向字面量,定义了一个宏TEXT(x),当定义了UNICODE宏,TEXT("xxx")会映射到L"xxx",否则会映射到"xxx"。

引入tchar.h后,可以使用_TCHAR类型,以及更简短的_T("xxx")宏,定义_UNICODE宏以后,会映射到L"xxx",否则会映射到"xxx"。

为了windows.h与tchar.h的一致性,定义UNICODE宏的同时需要定义带下划线的_UNICODE宏,以同时使用宽字符映射,没有定义UNICODE宏的情况下同时定义_MBCS宏,以支持GBK等双字节字符串处理。

对字符串进行转换

**字节字符串与Unicode宽字符串之间转换,比较可靠的方法是使用WideCharToMultiByte和MultiByteToWideChar。**它的优点是稳定性强,并且支持ANSI、OEM、UTF-7、UTF-8等多种字符集,用对应的代码页表示。

  • ANSI代码页:0(CP_ACP)(Windows代码页)
  • OEM代码页:1(CP_OEMCP)(DOS代码页)
  • UTF-7代码页:65000(CP_UTF7)
  • UTF-8代码页:65001(CP_UTF8)

如果在某些地方因为预处理宏冲突等问题不能引入windows.h,可以使用如下函数定义:

<code class="language-cpp">__declspec(dllimport)
int __stdcall MultiByteToWideChar(
        unsigned int codepage, unsigned long flags,
        const char *srcstr, int srcsize,
        wchar_t *dststr, int dstsize);
__declspec(dllimport)
int __stdcall WideCharToMultiByte(
        unsigned int codepage, unsigned long flags,
        const wchar_t *srcstr, int srcsize,
        char *dststr, int dstsize,
        const char *defchr, int *useddefchr);
</code>

示例代码:

<code class="language-cpp">// 日常科普:char和wchar_t转换

// wchar_t[]转char[]
wchar_t src_wstr[] = L"convert from wchar_t[] to char[]\r\n";
// 短字符串
char dst_str_short[1024] = "";
WideCharToMultiByte(CP_ACP, 0, src_wstr, -1, dst_str_short, 1024, NULL, NULL);
OutputDebugStringA(dst_str_short); // 使用char[]
// 长字符串
char *dst_str_long = NULL;
do // 多步可能失败的操作,用do-while(0)-break
{
	int dst_str_cchsize = WideCharToMultiByte(CP_ACP, 0, src_wstr, -1, NULL, 0, NULL, NULL);
	if (dst_str_cchsize <= 0) break; dst_str_long="(char" *)heapalloc(getprocessheap(), heap_zero_memory, dst_str_cchsize * sizeof (char)); if (dst_str_long="=" null) widechartomultibyte(cp_acp, 0, src_wstr, -1, dst_str_long, dst_str_cchsize, null, null); 下面是使用char[]的代码 outputdebugstringa(dst_str_long); }while(0); !="NULL)" do-while(0)-break之后只有清理代码 { heapfree(getprocessheap(), dst_str_long); } char[]转wchar_t[] char src_str[]="from char[] to wchar_t[]\r\n" ; 短字符串 wchar_t dst_wstr_short[1024]="L"";" multibytetowidechar(cp_acp, src_str, dst_wstr_short, 1024); outputdebugstringw(dst_wstr_short); 使用wchar_t[] 长字符串 *dst_wstr_long="NULL;" do 多步可能失败的操作,用do-while(0)-break int dst_wstr_cchsize="MultiByteToWideChar(CP_ACP," 0); (dst_wstr_cchsize <="0)" dst_wstr_long="(wchar_t" (wchar_t)); (dst_wstr_long="=" dst_wstr_long, dst_wstr_cchsize); 下面是使用wchar_t[]的代码 outputdebugstringw(dst_wstr_long); dst_wstr_long); code></=></code>

文本的处理

ANSI文本:建议使用系统的CharNextExA、CharPrevExA或_mbsinc、_mbsdec函数处理,而不要使用自己编写的算法。

UTF-8文本:建议直接转换为Unicode(宽字符)处理。如果要手工处理UTF-8文本,一定要在处理之前把char *指针转换为unsigned char *指针,不然由于char通常是有符号的,往往会导致编写的多字节算法无效。

宽字符文本:一般可以直接使用C运行库的wcs系列函数处理。如果需要获得单个码点的值,或者数码点数,需要使用IS_SURROGATE_PAIR判断UTF-16代理对。如果需要数复杂文字的组合字符数,需要使用CharNextW或CharPrevW函数,或者使用Uniscribe复杂文字分析技术。

引入tchar.h后,可以使用_tcslen、_tcscpy、_tcscat、_tcstol、_tcstoi64、_tcstod等函数,它们随_UNICODE宏和_MBCS宏是否定义而被映射到strxxx、wcsxxx、_mbsxxx函数。其中_tcstod(strtod、wcstod)函数是比较重要的,它可以将字符串转换为浮点数,这个功能很难用WinAPI实现。

引入strsafe.h后,可以使用StringCbXxxxx和StringCchXxxxx系列,前者要求缓冲区的字节长度,后者要求缓冲区的字符长度,它们实际上是包装了运行库函数的缓冲区安全化内联函数。同时,它们和WinAPI的规则是相同的,也使用UNICODE宏以及-W和-A结尾。其中StringCbPrintf和StringCchPrintf函数是比较重要的,它们可以将浮点数转换为字符串,这个功能很难用WinAPI实现。

命令参数

Visual C++支持宽字符入口点,通过它们可以获取获取宽字符命令参数。

<code class="language-cpp">int wmain(int argc, wchar_t *argv[]);
int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t *szCmdLine, int nShowCmd);
</code>

引入tchar.h后,可以使用_tmain和_tWinMain宏表示入口点。

<code class="language-cpp">int _tmain(int argc, _TCHAR *argv[]);
int __stdcall _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, _TCHAR *szCmdLine, int nShowCmd);
</code>

使用这个入口点以后,只会初始化_wenviron全局变量,environ全局变量并没有被自动初始化,不过,只需调用一次getenv("")即可修复这个问题。

除此之外,使用GetCommandLineW可以在任何地方获取Unicode宽字符命令参数。GetCommandLineW返回的是固定地址,不需要释放。使用GetCommandLineA可以获取相应的多字节版本命令参数。

使用CommandLineToArgvW可以获取Unicode宽字符版本的argv。CommandLineToArgvW使用LocalAlloc分配了一个内存块,使用完需要使用LocalFree释放。它只能在Windows 2000以上操作系统使用。

<code class="language-cpp">#include <windows.h>

int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t *szCmdLine, int nShowCmd)
{
	// 主程序:获取Unicode宽字符版本的命令参数
	int argcW = 0;
	wchar_t **argvW = CommandLineToArgvW(GetCommandLineW(), &argcW);
	if (argvW)
	{
		for (int i = 0; i < argcW; i++)
		{
			MessageBox(NULL, argvW[i], L"命令参数", MB_OK);
		}
		LocalFree(argvW);
	}

	return 0;
}
</windows.h></code>

文件I/O

一般来说,使用stdio.h就足够了,可使用fopen、_wfopen、_tfopen打开文件,fclose关闭文件。读写使用fread、fwrite或fgets、fputs等其它函数。

  • 如果想要按照\n读取换行符,则应使用文本模式"r/w/a[+]"。
  • 如果想要按照\r\n读取换行符,则应使用二进制模式"r/w/a[+]b"。

可以考虑使用open、_wopen、_topen、sopen、_wsopen、_tsopen打开文件,使用_read和_write读写文件。如果需要转换\n与\r\n换行符,使用_O_TEXT,否则使用_O_BINARY。

如果需要临时禁止转换换行符,使用_setmode(_fileno(stdxxx), _O_BINARY);。使用_setmode(_fileno(stdxxx), _O_TEXT);恢复转换换行符。在VC++2015之前并不支持freopen(NULL, "xxx");这种做法。

如果需要更高的性能和灵活性,可以使用CreateFile打开文件,使用CloseHandle关闭文件。使用ReadFile和WriteFile读写文件。

要注意的是,stdio.h读写文件,对于普通文件和管道或设备,具有一致的行为,而后两种方法,则对文件和管道或设备具有不一致的行为,更加贴近底层一些。

不要尝试使用", ccs=[UNICODE|UTF-16|UTF-8]"、O[W|U8|U16]TEXT,它是VC++2005时期微软的中二产物,手工转换虽然麻烦一点,但可靠性高。

标准I/O和控制台I/O

I/O编程实际上分为两个方向:面向一般设备(普通文件、管道、conin$/conout$等)、面向控制台(conin$/conout$)。

面向一般设备(普通文件、管道、conin$/conout$等)

直接使用stdio.h中的printf/scanf/fgets/puts/getchar/putchar等函数即可,并且为了和其它部分的C/C++程序统一编码,一般假定它们是ANSI。

不要和attrib/tree/more这些MS-DOS工具链统一编码。微软为了避免用户产生乱码的困惑,因此它们被设计为使用OEM代码页,是仿古程序(仿MS-DOS程序)。但是实际上仿古程序的编程是很复杂的,一般人不需要也没必要知道。

不建议直接使用_read/_write或ReadFile/WriteFile,因为普通文件、管道、conin$/conout$的I/O特性各不相同,如果要同时支持这些东西,相当于重新写了一遍stdio.h的函数。

面向控制台(conin$/conout$)

使用GetStdHandle(x)函数可以直接获取标准输入、标准输出、标准错误的Win32文件句柄,参数x可以是STD_INPUT_HANDLE、STD_OUTPUT_HANDLE、STD_ERROR_HANDLE。它们也可以通过(HANDLE)_get_osfhandle(_fileno(stdxxx))获取,如果你要与标准库同步的话。

除此之外,还可以使用CreateFile自行打开conin$/conout$。一定要使用如下标准形式。

<code>HANDLE hconin = CreateFile(TEXT("conin$"), GENERIC_READ|GENERIC_WRITE,
	FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hconout = CreateFile(TEXT("conout$"), GENERIC_READ|GENERIC_WRITE,
	FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
</code>

可以使用ReadConsole/WriteConsole,其中ReadConsoleA/WriteConsoleA可以支持控制台当前代码页(可以通过GetConsoleCP()/GetConsoleOutputCP()获得),而ReadConsoleW/WriteConsoleW支持Unicode。可以支持。使用这些函数要注意几点:

  • 请求字符数不要低于256字符,因为行缓冲最小是256字符(包含\r\n)。
  • 请求字符数不要高于4096字符,C运行库的默认缓冲区大小,因为太大的请求字符数在WinXP/2003/Vista/7上容易RPC失败,4096字符已经能够满足大多数应用需要了。Win8+因为改成NtDeviceIoControlFile了,因此已经没有这个问题,但应该考虑兼容性问题。
  • ReadConsoleA缓冲区尽量大一个字节,因为双字节代码页(如936)中这玩意的实现有bug,容易缓冲区溢出。
  • UTF-8代码页65001虽然可以通过chcp切换过去,但是ReadConsoleA会出问题,因为它只是为了单字节代码页(如437)和双字节代码页(如936)设计的。

如何看待奇葩且难以改变的Win32编码体系?

为什么Windows的编码设定这么奇葩?ANSI字符集被淘汰了吗?ANSI字符集是为了Windows 95/98/Me而生的吗?在936之外,需要兼容932、949、950、1251、1252等其它ANSI代码页的系统吗?文本文件的编码情况如何?控制台的编码情况如何?文本文件的编码情况如何?

Windows并不是可以随意重编译的开源生态,而是二进制兼容的闭源生态。ANSI的WinAPI,编码无法随意升级,只能停留在上个世纪90年代的水平。Unicode的WinAPI,编码也不能改为UTF-8或UTF-32,只能使用UTF-16。

ANSI字符集并没有被淘汰,仍然是必需的。在编程中,我们一般认为ANSI和Unicode都够用了,使用Unicode更好,使用ANSI也是可以接受的。至于在用户名上使用emoji会发生什么困扰,是用户自己作死的事情。

在基于Windows NT的Windows操作系统中,ANSI字符集和Unicode字符集在Win32编程中地位是相同的。ANSI字符集是为了便于从(当年的)其它平台迁移而生的,远早于Windows 95/98/Me的出现,所以ANSI字符集不是为了Windows 95/98/Me而存在的。

如果程序要求GBK字符集,则没有必要兼容这些代码页,因为即使兼容了这些代码页,中文也会变成一堆问号。如果程序只要求ASCII字符集,可以考虑支持其它代码页。

在Windows中,文本文件分为ANSI、Unicode、Unicode big endian、UTF-8文本文件,其中后三者分别要以\xFF\xFE、\xFE\xFF、\xEF\xBB\xBF这些BOM序列开头,但是也经常遇到不包含BOM的UTF-8。通常的做法是,不带BOM的文件默认为ANSI,带有BOM的支持Unicode、UTF-8两种即可。可以放弃支持ANSI转而支持不带BOM的UTF-8,但是一定要同时支持带有BOM的UTF-8,不然肯定会对用户产生困扰。

控制台的编码情况比较复杂。控制台交互使用的是Unicode,但是控制台程序的文本流I/O(管道或重定向文件)并没有统一的编码标准,实际使用的可能是GetConsoleCP()和GetConsoleOutputCP()代码页的编码,但也有可能是固定为ANSI或OEM或437代码页的编码,甚至可能是UTF-8编码。对于控制台程序的文本流I/O,一般可以认为是代码页不确定的ASCII文本,应该提供选项让用户选择可读的编码。

文号 / 822030

千古风流
名片发私信
学术分 4
总主题 466 帖总回复 2942 楼拥有证书:进士 学者 笔友
注册于 2009-05-30 21:22最后登录 2019-01-31 17:16
主体类型:个人
所属领域:无
认证方式:邮箱
IP归属地:未同步

个人简介

暂未填写
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

插入资源
全部
图片
视频
音频
附件
全部
未使用
已使用
正在上传
空空如也~
上传中..{{f.progress}}%
处理中..
上传失败,点击重试
等待中...
{{f.name}}
空空如也~
(视频){{r.oname}}
{{selectedResourcesId.indexOf(r.rid) + 1}}
处理中..
处理失败
插入表情
我的表情
共享表情
Emoji
上传
注意事项
最大尺寸100px,超过会被压缩。为保证效果,建议上传前自行处理。
建议上传自己DIY的表情,严禁上传侵权内容。
点击重试等待上传{{s.progress}}%处理中...已上传,正在处理中
空空如也~
处理中...
处理失败
加载中...
草稿箱
加载中...
此处只插入正文,如果要使用草稿中的其余内容,请点击继续创作。
{{fromNow(d.toc)}}
{{getDraftInfo(d)}}
标题:{{d.t}}
内容:{{d.c}}
继续创作
删除插入插入
插入公式
评论控制
加载中...
文号:{{pid}}
加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}
ID: {{user.uid}}