慢羊羊的空间

无为,无我,无欲,居下,清虚,自然

在程序中正确使用 Unicode 和 MBCS 字符集 金牌收录

概述

在 Windows 下做开发,初学者经常面临字符集选择的问题。本文详细解释 MBCS 字符集和 Unicode 字符集的正确使用方法,以及为什么写程序要用 Unicode 字符集。同时对 UTF-8 做了简单介绍。

在程序中正确使用字符集

MBCS 字符集用 char 表示字符,即每个字符一个字节。

Unicode 字符集用 wchar_t 表示字符,即每个字符两个字节。

以 VC 为例,微软在 VC6.0 的时候,创建项目的默认字符集是 MBCS。从 VC2002(就是 VC7.0)开始,默认字符集就变为了 Unicode,直到今天。

为了减少字符编码造成的种种问题,请务必确保整个项目使用相同的字符集编码。比如,如果设置项目字符集为 Unicode,那么整个项目都要用 Unicode 方式处理字符串。具体方法:

1. 设置项目字符集

有两种方法设置项目字符集。

方法一,在项目属性中设置:

以 VC2010 Express 为例:在 Solution Explorer 右击项目 -> Properties,打开的属性窗口中,左侧选择 Configuration Properties / General 分类,右边将 Character Set 设置为 Use Unicode Character Set 即表示将项目设置为 Unicode 字符集,设置为 Use Multi-Byte Character Set 即表示将项目设置为 MBCS 字符集。

方法二,在代码中设置:

在代码的最顶部增加以下代码,表示程序使用 Unicode 字符集:

#define UNICODE
#define _UNICODE

在代码的最顶部增加以下代码,表示程序使用 MBCS 字符集:

#undef UNICODE
#undef _UNICODE

备注:_UNICODE 用于 C 运行库,UNICODE 用于 WINAPI。

2. 写程序时使用对应的字符集

请参考下表:

MBCS 字符集 Unicode 字符集 自适应 MBCS/Unicode
字符串常量 "Hello World" L"Hello World" _T("Hello World")
字符串变量 char s[10]; wchar_t s[10]; TCHAR s[10];
字符串操作函数 strlen wcslen _tcslen
strcpy wcscpy _tcscpy
strcmp wcscmp _tcscmp
…… …… ……
STL 字符串 string wstring (无)

注:

1. 自适应 MBCS/Unicode,是指代码会根据宏定义,自动转换为相应字符集对应的操作。例如,TCHAR 在定义了宏 UNICODE / _UNICODE 的时候,会在编译时转换为 wchar_t,否则,就转换为 char。

2. printf、itoa 等许多字符串操作函数,都有对应的 Unicode 函数,具体的函数名请参考微软的帮助文档。

3. 再次强调,请务必确保整个项目使用相同的字符集编码,这对于减少编码造成的问题很有帮助。

通过示例展现 Unicode 比 MBCS 的优势

  • 实验环境 Windows 7 + VC2010 Express + EasyX_20220116。(在 Win10 下可以得到同样的结果)
  • 因为代码中涉及到英文中文韩文字符,需要将 .cpp 文件以 UTF-8 编码保存。方法:File -> Save xxx.cpp As...,在文件另存为对话框里,“保存”按钮右边有一个向下的箭头,点击后选择“Save with Encoding...”,同意覆盖原文件,然后 Encoding 选择 Unicode (UTF-8 with signature) - Codepage 65001,Line endings 不用调整,确定保存即可。

两个示例程序的具体设置与代码

第一个示例程序:

项目名称 TestUnicode
项目类型:Win32 Console Application
项目字符集:Unicode

#include <graphics.h>
#include <conio.h>

int main()
{
	wchar_t s[] = L"厉害了我的国";

	initgraph(640, 480);
	settextstyle(24, 0, L"微软雅黑");
	outtextxy(10, 30, s);
	_getch();
	closegraph();

	return 0;
}

第二个示例程序:

项目名称 TestMbcs
项目类型:Win32 Console Application
项目字符集:MBCS

#include <graphics.h>
#include <conio.h>

int main()
{
	char s[] = "厉害了我的国";

	initgraph(640, 480);
	settextstyle(24, 0, "微软雅黑");
	outtextxy(10, 30, s);
	_getch();
	closegraph();

	return 0;
}

执行效果

在中文系统下的执行 TestUnicode.exe 和 TestMbcs.exe,效果没有任何区别,如下图所示:

现在将系统切换为英文。操作步骤如下图所示(以英文版 Win7 系统为例):

然后再次执行 TestUnicode.exe 和 TestMbcs.exe,执行效果如下图所示:

也就是说,即便美国用户懂中文,也看不到 MBCS 字符集程序里面的中文。

有些同学可能会说,我的程序只给中文用户使用,是不是可以使用 MBCS 字符集呢?我们试着修改一下程序,把字符串“厉害了我的国”修改为“Computer 翻译为韩文是 컴퓨터”,然后在中文 Win7 系统下执行,效果如下:

PS:暂时先忽略 TestMbcs 项目中 VC 的警告:

main.cpp(7): warning C4566: character represented by universal-character-name '\uCEF4' cannot be represented in the current code page (936)

可以看到:

如果你的程序使用 MBCS 字符集,那么在其他语言的计算机上运行,会无法正常显示。如果你的程序中使用了多语言(例如韩文),那么即便在中文语言的计算机上,也无法正常显示。

原理解释

ASCII 码规定了每个字符一个字节,前 128 个属于常规 ASCII 码,后 128 个属于扩展 ASCII 码。常规 ASCII 码里面含有英文大小写字母、阿拉伯数字、常见标点符号等。扩展 ASCII 码里面是一些不常用的字符。

于是在过去,想要表示多语言的时候,就利用了扩展 ASCII 码不常用的特点,将两个连续的扩展 ASCII 码表示成其它语言。

例如中文的 GB2312 编码,将汉字分成 94 个区和 94 个位,区和位分别使用了扩展 ASCII 码的 161~254 这个范围。“中国”两字的区位码分别是 5448 2590,那么可以构造一个这样的程序(暂时忽略编译警告):

// 设置为 MBCS 字符集
#undef UNICODE
#undef _UNICODE

#include <graphics.h>
#include <conio.h>

int main()
{
	// “中”的区位码是 54 48,“国”的区位码是 25 90
	char s[] = {160 + 54, 160 + 48, 160 + 25, 160 + 90, 0};

	initgraph(640, 480);
	settextstyle(24, 0, "微软雅黑");
	outtextxy(10, 30, s);
	_getch();
	closegraph();

	return 0;
}

执行这个程序,可以成功的输出“中国”两个字。

这就是在 MBCS 字符集下中文的表示形式。

其他语言类似,比如韩文的 EUC-KR 编码(过去叫 KSC5601 编码),也用的两个扩展 ASCII 码,并且范围也是 0xA1-0xFE。台湾地区的 BIG5 编码也覆盖了这个范围。那么问题来了:两个扩展 ASCII 码,究竟表示中文、韩文还是繁体中文或者别的语言?

这取决于操作系统的系统区域(locale)设置,
当 locale 设置为简体中文,那么两个连续的扩展 ASCII 码就会根据 GB2312 编码解析;
当 locale 设置为韩文,那么两个连续的扩展 ASCII 码就会根据 EUC-KR 编码解析。

由此可知:

  1. 系统区域设置错误,MBCS 字符集的字符串就无法正确显示。
  2. MBCS 字符集无法实现多语言混合显示。

当编码与 locale 不匹配的时候,就会出现“乱码”。

Unicode 字符集整合了全世界所有语言的文字,任何语言的任何一个文字,在 Unicode 编码中都有唯一的值对应。因此不再需要设置“系统区域(locale)”,也就不会产生“乱码”了。

就像前面的实验看到的那样,采用 Unicode 字符集的程序,无论是同时显示多少种语言,无论在什么语言的操作系统上执行,都可以正常显示。

扩展知识:关于 UTF-8 和 Unicode

目前,有两大组织都在做“全球每个字符有一个编码”这件事。关于字符编码,有 Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4 等等很多名词,有兴趣的可以自行搜索。这里只介绍 UTF-8 编码。

UTF-8 编码下,ASCII 码表中的字符仍然只占一个字节,其它语言的字符需要 2 ~ 4 个字节表示,每个字符的长度不固定。

这个编码的优势是针对英文节省空间。缺点是时间复杂度飙升。比如 Unicode 编码下,求字符串中字符数量的时间复杂度是 O(1),但是在 UTF-8 编码下,求字符串中字符数量的时间复杂度是 O(n)。

微软最早推广 Unicode 编码的时候,全世界收录了 3 万多字符,用两个字节表示一个字符绰绰有余,这个方案得到了推广。但是后来,Unicode 收录的字符越来越多,直到超出了 65535,用两个字节无法表示一个字符。所以准确的说,现在使用 Unicode 并不一定能表示一个中文字符,有一些生僻字需要占用 4 个字节。

现在看来,Unicode 并不那么完美,好在 99.9% 的情况遇不到那些生僻字。而 UTF-8 编码处理多语言得从 0 开始,需要重新编写每一个字符串函数。比如“逆序字符串”这样简单的功能,如果是 UTF-8 编码的字符串,那么复杂度会提升数十倍。

所以目前,相对来说,用 Unicode 编码是较高性价比的方案。

在控制台输出 Unicode 编码的字符串

控制台的历史悠久,还没有 Unicode 的时候就有控制台。为了兼容性,目前的控制台默认接收 MBCS 编码。如果用 wprintf 输出一个 Unicode 字符串,会因为编码问题导致无法正确显示中文。正确的做法是,先设置控制台的 locale(只需要在程序开始设置一次):

#include <stdio.h>
#include <locale.h>							// setlocale() 需要这个头文件

int main()
{
	setlocale(LC_ALL, "chs");				// 设置控制台 locale 为中文(chs)
	wchar_t s[] = L"Unicode 编码测试";		// 定义 Unicode 字符串
	wprintf(L"%s\n", s);					// 输出 Unicode 字符串
	return 0;
}

注:
1. 即便如此设置,甚至即便再设置控制台的字体为“SimSun-ExtB”,控制台仍然无法显示极少数的生僻字,例如“𠾆”字。
2. Windows 窗口程序和 EasyX 程序都默认支持 Unicode 编码,支持显示生僻字,可以直接使用,无需设置 locale。

Unicode、MBCS、UTF-8 编码之间的相互转换

MBCS 表示“多字节编码”,UTF-8 和 ANSI 都属于 MBCS。Windows API 提供了相关转换函数,以下是几种编码相互转换的代码:

// ANSI 转换为 Unicode 编码
wstring ANSIToUnicode(const string& src)
{
	wstring dst;
	int len = MultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, NULL, 0);			// 获取转换后的长度(包括结尾 \0)
	dst.resize(len);															// 调整 dst 容量
	MultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, &dst[0], (int)dst.size());	// 转换
	return dst;
}

// Unicode 转换为 ANSI 编码
string UnicodeToANSI(const wstring& src)
{
	string dst;
	int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), -1, NULL, 0, NULL, NULL);			// 获取转换后的长度(包括结尾 \0)
	dst.resize(len);																		// 调整 dst 容量
	WideCharToMultiByte(CP_ACP, 0, src.c_str(), -1, &dst[0], (int)dst.size(), NULL, NULL);	// 转换
	return dst;
}

// UTF8 转换为 Unicode 编码
wstring UTF8ToUnicode(const string& src)
{
	wstring dst;
	int len = MultiByteToWideChar(CP_UTF8, 0, src.c_str(), -1, NULL, 0);		// 获取转换后的长度(包括结尾 \0)
	dst.resize(len);															// 调整 dst 容量
	MultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, &dst[0], (int)dst.size());	// 转换
	return dst;
}

// Unicode 转换为 UTF8 编码
string UnicodeToUTF8(const wstring& src)
{
	string dst;
	int len = WideCharToMultiByte(CP_UTF8, 0, src.c_str(), -1, NULL, 0, NULL, NULL);		// 获取转换后的长度(包括结尾 \0)
	dst.resize(len);																		// 调整 dst 容量
	WideCharToMultiByte(CP_ACP, 0, src.c_str(), -1, &dst[0], (int)dst.size(), NULL, NULL);	// 转换
	return dst;
}

注:截至目前,MultiByteToWideChar / WideCharToMultiByte 函数仍无法支持极少数的生僻字,如“𠾆”。所以,保持整个项目都使用 Unicode 编码,是一种可以应付绝大多数情况的简便有效的方法。

(完)

评论 (2) -

  • 是不是用Unicode编码就不会出现乱码的情况了
    • Unicode 编码下,A 就是 A,所以不会乱码。MBCS 编码下,A 是否是 A,取决于 L 是否是 1。当 L 是 2 的时候,A 是 B。这就埋下了乱码的隐患。

添加评论