Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s

例:
现有有一个文本文件:01.txt,需要把文档内容输出到控制台。

根据上述要求:

#include 

int main()
{
	FILE* fp = NULL;
	char filename[] = "01.txt";
	char ch;
	if (fopen_s(&fp, filename, "r") != 0)//打开文件。
		return -1;
	while (feof(fp) == 0)//输出文件内容。
	{
		ch = fgetc(fp);
		putchar(ch);
	}
	fclose(fp);
	return 0;
}

以下是输出结果:
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第1张图片
在记事本的右下角可以看到 01.txt 的编码格式为ANSI。
如果打开编码格式为UTF-8的 02.txt 会怎么样呢?
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第2张图片
似乎出现了乱码。

为何不能正常输出

我们先了解一些关于编码格式的知识
Aya的学习笔记:字符编码格式

编码格式

ASCII

ASCII (美国信息交换标准代码,American Standard Code for Information Interchange)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准。

  • 空格 0x20
  • 数字 0x30
  • 大写字母 0x41
  • 小写字母 0x61

ANSI

ANSI是一种字符代码,为使计算机支持更多语言,通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符。超出此范围的使用0x80~0xFFFF来编码,即扩展的ASCII编码。
不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。
这些使用多个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。
在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。
不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。ANSI编码表示英文字符时用一个字节,表示中文用两个或四个字节。
为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

Unicode与Unicode转换格式

Unicode通常用两个字节表示一个字符(UTF-16),原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。
Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。
因为Unicode有多种编码格式,编码方式不同的文本在硬件看来都是一堆二进制码。所以需要BOM标记来区分。
BOM(Byte Order Mark),字节顺序标记。出现在文本文件头部,Unicode编码标准中用于标识文件是采用哪种格式的编码。

  • UTF-8
    UTF-8是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符,而且其编码中的第一个字节仍与ASCII相容,使得原来处理ASCII字符的软件无须或只进行少部份修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。
  • UTF-8编码规则:
    如果只有一个字节则其最高二进制位为0;
    如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。
  • UTF-8转换表:
    1字节 0xxxxxxx
    2字节 110xxxxx 10xxxxxx
    3字节 1110xxxx 10xxxxxx 10xxxxxx
    4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
    6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

原因

不同编码格式的文档存储在寄存器上,如果依次按字节读取数据,读出的也并不相同。
如上,硬件读取到了采用UTF-8编码的数据,在 Windows 10 简体中文 环境下这些数据被当作GBK编码的数据显示,于是出现乱码。
实际上对于计算机来说,这些并不是乱码。而是UTF-8编码的数据,只是人们并不能识别。
所以对于不同编码的文档,需要用不同方式显示为人类能识别的符号。

乱码输出了什么

由于02.txt的文本太多不方便演示,现新建03.txt设置成ANSI编码格式。04.txt设置成UTF-8编码格式。
内容为:
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第3张图片
注:最后一行是标点符号也是全角。

修改上述代码如下

#include 

int main()
{
	FILE* fp = NULL;
	char filename[] = "03.txt";
	char ch;
	if (fopen_s(&fp, filename, "r") != 0)
		return -1;
	while (feof(fp) == 0)
	{
		ch = fgetc(fp);
		//putchar(ch);
		printf("%hhX ", ch);//以一个字节的16进制整型输出ch
		if (ch == '\n')
			putchar('\n');//为控制台输出整齐,当ch为'\n'时控制台换行
	}
	fclose(fp);
	return 0;
}

分析03.txt的内容

Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第4张图片
4行文本在控制台输出5行
最后一行 FF 换算为二进制为 1111 1111 是文本结束符(EOF)。
可以对照ASCII码表看出,

  • 0000 1010 (0x0A)为换行符’\n’。
  • 0011 0000 (0x30)开始是数字1~9。
  • 0100 0001 (0x41)开始是大写字母A~Z。
  • 0110 0001 (0x61)开始是小写字母a~z。

前三行是纯粹的ASCII码。
最后一行除最后一个换行符外,四个汉字加上全角的逗号(,)句号(。)共6个字符,占12字节。
实际上这就是GBK编码。与ASCII码兼容,每个汉字占两字节。

GBK:
你:C4 E3
好:BA C3
世:CA C0
界:BD E7

分析04.txt的内容

Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第5张图片
我们发现,UTF-8格式编码的txt文件除汉字外都是ASCII码
汉字部分,每三个字节有一个E开头的字节。
例如 E4 BD A0 转换成二进制
十六进制:E4 BD A0
二进制:1110 0100 1011 1101 1010 0000
符合UTF8规范,第一个字节开头1110,代表这是一个三字节字符。剩下两个是10开头,表示这不是第一个字节。

UTF:
你:E4 BD A0
好:E5 A5 BD
世:E4 B8 96
界:E7 95 8C

UTF-8和Unicode的关系

以中文举例:
“你”
Unicode: 4F60
0100 1111 0110 0000
UTF-8: E4BDA0
1110 0100 1011 1101 1010 0000
把Unicode和UTF-8固定格式以外部分对齐可以轻易发现规律。
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第6张图片

Unicode转换为UTF-8只需要在每个字节中除固定格式外的二进制位上按序填入Unicode编码。

字符编码格式转换

现在知道了输出的乱码是什么,那么如何转换呢?
当然是用转换函数了!
下面介绍的是MultiByteToWideChar,WideCharToMultiByte,mbstowcs,wcstombs。四种转换函数。

MultiByteToWideChar,WideCharToMultiByte

Windows提供了两种API函数:MultiByteToWideChar()和WideCharToMultiByte()
用于字符串和宽字符串的互相转换。

头文件:windows.h
这两个函数是由Windows API函数,不具有通用性。

MultiByteToWideChar函数

函数功能:
该函数映射一个字符串到一个宽字符(unicode)的字符串。由该函数映射的字符串没必要是多字节字符组。

函数原型:

int MultiByteToWideChar(
UINT CodePage,//指定执行转换的字符集
DWORD dwFlags,//一组位标记
LPCCH lpMultiByteStr,//指向将被转换字符串的字符。
int cchMultiByte,//上个参数:字符串的长度。
LPWSTR lpWideCharStr,//指向接收被转换字符串的缓冲区。
int cchWideChar//缓冲区的宽字符个数。
);

MultiByteToWideChar函数参数

CodePage

参数类型:UINT (无符号整型,unsigned int)

指定执行转换的多字节字符所使用的字符集
这个参数可以为系统已安装或有效的任何字符集所给定的值。你也可以指定其为下面的任意一值:

宏定义 说明
CP_ACP 0 ANSI编码
CP_OEMCP 1 OEM编码
CP_MACCP 2 MAC编码
CP_THREAD_ACP 3 当前线程的ANSI编码
CP_SYMBOL 42 SYMBOL转换格式
CP_UTF7 65000 UTF-7转换格式
CP_UTF8 65001 UTF-8转换格式
dwFlags

参数类型:DWORD (双字,4字节,无符号长整形,unsigned long)

一组位标记用以指出是否未转换成预作或宽字符(若组合形式存在),是否使用象形文字替代控制字符,以及如何处理无效字符。你可以指定下面是标记常量的组合,含义如下:

宏定义 说明
MB_PRECOMPOSED 1 总是使用预制字符,即有单个预制字符时,就不会使用分解的基字符和不占空间字符。此为函数的默认选项,不能和MB_COMPOSITE合用
MB_COMPOSITE 2 总是使用分解字符,即总是使用基字符+不占空间字符的方式
MB_USEGLYPHCHARS 4 使用像形字符代替控制字符
MB_ERR_INVALID_CHARS 8 设置此选项,函数遇到非法字符就失败并返回错误码ERROR_NO_UNICODE_TRANSLATION,否则丢弃非法字符

注意:

  • 组合字符由一个基础字符和一个非空字符构成,每一个都有不同的字符值。
  • 每个预作字符都有单一的字符值给基础/非空字符的组成。在字符è中,e就是基础字符,而重音符标记就是非空字符。
  • 函数的缺省动作是转换成预作的形式。如果预作的形式不存在,函数将尝试转换成组合形式。
  • 标记MB_PRECOMPOSED和MB_COMPOSITE是互斥的,而标记MB_USEGLYPHCHARS和MB_ERR_INVALID_CHARS则不管其它标记如何都可以设置。

对于下列代码页,dwFlags必须为0,否则函数返回错误码ERROR_INVALID_FLAGS。

代码页 全称
50220 日文(JIS)
50221 日文(JIS-允许一个字节的片假名)
50222 日文(JIS-允许一个字节的片假名 - SO/SI)
50225 韩文(ISO)
50227 简体中文(ISO-2022)
50229 繁体中文(ISO-2022 )
52936 简体中文(HZ)
54936 简体中文(GB18030)
57002到57011 国际信息交换标准代码 (ISCII) 梵文 孟加拉 泰米尔语 泰卢固语 阿萨姆语 奥里亚语 卡纳达语 马拉雅拉姆语 古吉拉特语 旁遮普语
65000 Unicode (UTF-7)
42 Symbol

对于Unicode (UTF-8) dwFlags必须为0或MB_ERR_INVALID_CHARS,否则函数都将失败并返回错误码ERROR_INVALID_FLAGS。

lpWideCharStr

参数类型:LPCCH (指针类型,const char*)

指向将被转换字符串的字符。

cchMultiByte

参数类型:int

指定由参数lpMultiByteStr指向的字符串中字节的个数。
如果lpMultiByteStr指定的字符串以空字符终止。可以设置为-1
如果字符串不是以空字符中止,设置为-1可能失败,可能成功。
此参数设置为0函数将失败。

lpWideCharStr

参数类型:LPWSTR (指针类型,wchar_t*)

指向接收被转换字符串的坐标。

cchWideChar

参数类型:int

指定由参数lpWideCharStr指向的缓冲区的宽字符个数。
若此值为零,函数返回缓冲区所必需的宽字符数,在这种情况下,lpWideCharStr中的缓冲区不被使用。

返回值

  • 如果函数运行成功,并且cchWideChar不为零,返回值是由lpWideCharStr指向的缓冲区中写入的宽字符数。
  • 如果函数运行成功,并且cchMultiByte为零,返回值是接收到待转换字符串的缓冲区所需求的宽字符数大小。
  • 如果函数运行失败,返回值为零。
  • 若想获得更多错误信息,请调用GetLastError函数。它可以返回下面所列错误代码:
    ERROR_INSUFFICIENT_BUFFER
    ERROR_INVALID_FLAGS
    ERROR_INVALID_PARAMETER
    ERROR_NO_UNICODE_TRANSLATION。

注意

指针lpMultiByteStr和lpWideCharStr必须不一样。如果一样,函数将失败,GetLastError将返回ERROR_INVALID_PARAMETER的值。
如果MB_ERR_INVALID_CHARS被设置并且在资源字符串中遇到无效的字符时,函数将失败。
如果MB_ERR_INVALID_CHARS不被设置,或是DBCS串中发现了头字节而没有有效的尾字节,无效字符将转换为缺省字符,但不是资源字符串中的缺省字符。
当无效字符被发现,且MB_ERR_INVALID_CHARS值被设置,函数返回零,GetLastErro显示ERROR_NO_UNICODE_TRANSLATION的出错
Windows CE:不支持参数CodePage中的CP_UTF7和CP_UTF8的值,以及参数dwFlags中的WC_NO_BEST_FIT_CHARS值。

WideCharToMultiByte函数

函数功能:
该函数可以映射一个unicode字符串到一个多字节字符串,执行转换的代码页,接收转换字符串,允许额外的控制等操作。

函数原型:

int WideCharToMultiByte(
UINT CodePage,//指定执行转换的字符集
DWORD dwFlags,//一组位标记
LPCWCH lpWideCharStr,//指向将被转换宽字符串的字符。
INT cchWideChar,//上个参数:宽字符串的长度。
LPSTR lpMultiByteStr,//指向接收被转换字符串的缓冲区。
INT cchMultiByte,//缓冲区的字符个数。
LPCCH lpDefaultChar,// 遇到一个不能转换的宽字符,函数便会使用此参数指向的字符
LPBOOL pfUsedDefaultChar //至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE;

既然要解决乱码问题,为什么先介绍字符转宽字符的函数呢?
当然是因为WideCharToMultiByte函数的参数多啊!
WideCharToMultiByte函数的参数比MultiByteToWideChar函数多两个,两个函数的前五个参数几乎相同。

WideCharToMultiByte函数参数

lpWideCharStr

参数类型:LPCWCH (指针类型,const wchar_t*)

指向将被转换的unicode字符串。

lpMultiByteStr

参数类型:LPSTR (指针类型,char*)

指向接收被转换字符串的坐标。

lpDefaultChar和pfUsedDefaultChar

lpDefaultChar 参数类型:LPCCH (指针类型,const char*)
pfUsedDefaultChar 参数类型:LPBOOL (指针类型,int*)

只有当WideCharToMultiByte函数遇到一个宽字节字符,而该字符在uCodePage参数标识的代码页中并没有它的表示法时,WideCharToMultiByte函数才使用这两个参数。(通常都取值为NULL)

如果宽字节字符不能被转换,该函数便使用lpDefaultChar参数指向的字符。如果该参数是NULL(这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。

pfUsedDefaultChar参数指向一个布尔变量,如果Unicode字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为TRUE。如果所有字符均被成功地转换,那么该函数就将该变量置为FALSE。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。

返回值

  1. 如果函数运行成功,并且cchMultiByte不为零,返回值是由 lpMultiByteStr指向的缓冲区中写入的字节数。
  2. 如果函数运行成功,并且cchMultiByte为零,返回值是接收到待转换字符串的缓冲区所必需的字节数。(此种情况用来获取转换所需Char的个数)
  3. 如果函数运行失败,返回值为零。
  4. 若想获得更多错误信息,请调用GetLastError函数。它可以返回下面所列错误代码:
    ERROR_INSUFFICIENT_BJFFER
    ERROR_INVALID_FLAGS
    ERROR_INVALID_PARAMETER
    ERROR_NO_UNICODE_TRANSLATION。

利用MultiByteToWideChar,WideCharToMultiByte。在控制台输出UFT-8编码的文本

修改上述代码如下

#include 
#include 

int main()
{
	char filename[] = "02.txt";
	FILE* fp = NULL;
	const char* str;
	const char* cstr;
	const wchar_t* wstr;
	char* sp;
	size_t size;

	if (fopen_s(&fp, filename, "r") != 0)
		return -1;
	/*创建并初始化一个与文本中字符个数相等的字符串str*/
	for (size = 0; feof(fp) == 0; fgetc(fp), size++);
	str = malloc(size);
	memset(str, 0, size);
	fclose(fp);
	/*为字符串str赋值,结束符换成'\0'*/
	fopen_s(&fp, filename, "r");
	for (sp = str; feof(fp) == 0; *sp = fgetc(fp), sp++);
	*(sp - 1) = '\0';
	fclose(fp);
	
	puts(str);
	
	/*创建宽字符串wstr,赋值str的Unicode编码格式*/
	size = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
	wstr = malloc(2 * size);
	memset(wstr, 0, 2 * size);
	MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, size);
	/*创建字符串cstr,赋值wstr的ANSI编码格式*/
	size = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
	cstr = malloc(size);
	memset(cstr, 0, size);
	WideCharToMultiByte(CP_ACP, 0, wstr, -1, cstr, size, NULL, NULL);

	puts(cstr);

	return 0;
}

运行结果
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第7张图片
实现方式

  1. 文本文件内容读入字符串str。
  2. 将str从UTF-8转码到Unicode存入wstr。
  3. 将wstr从Unicode转码到Unicode存入cstr。
  4. 输出wstr。

mbstowcs_s,wcstombs_s

C++中的标准库函数提供了两种函数mbstowcs_s()和wcstombs_s()
用于字符串和宽字符串的互相转换。

头文件:stdlib.h
这两个函数是C语言标准库函数,具有通用性。

使用前用 setlocale(LC_ALL, “.936”)重置为中文环境。
头文件:locale.h

mbstowcs_s()和wcstombs_s()分别是mbstowcs()和wcstombs()函数的安全版本,继续使用mbstowcs()和wcstombs()在新版本的Visual Studio编译器会产生代码c4996的报错,无法执行。

mbstowcs_s函数

函数功能:
用于将多字节编码字符串转换为宽字符编码字符串,即将char* 转换成wchar_t* 。从多字节字符转换成unicode字符。

函数原型:

errno_t __cdecl mbstowcs_s(
size_t* _PtNumOfCharConverted,
wchar_t* _DstBuf,
size_t _SizeInWords,
const char* _SrcBuf,
size_t _MaxCount
);

返回值类型:errno_t (错误码,int)

mbstowcs_s函数参数

_PtNumOfCharConverted

参数类型:指针类型,size_t*

以指针的方式使用主函数的变量。
变量被赋值转换后的字符串的长度,包括结束符。单位wchar_t

_DstBuf

参数类型:指针类型,wchar_t*

指向转换后的宽字符串坐标。

_SizeInWords

参数类型:size_t (大小,无符号整型,unsigned int)

指定由参数_DstBuf指向的缓冲区的宽字符个数。

_SrcBuf

参数类型:const char*

被转换字符串首地址。

_MaxCount

参数类型:size_t

缓冲区的宽字符个数,用于裁剪转换后的宽字符串。

返回值

成功返回0, 失败则返回失败代码。

wcstombs_s函数

函数功能:
用于将为宽字符编码字符串转换多字节编码字符串,即将字符串从wchar_t* 转换成char* 。从unicode字符转换成多字节字符。

函数原型:

errno_t __cdecl wcstombs_s(
size_t* _PtNumOfCharConverted,
char* _Dst,
size_t _DstSizeInBytes,
const wchar_t* _Src,
size_t _MaxCountInBytes
    );

wcstombs_s函数参数

和mbstowcs_s几乎相同

_PtNumOfCharConverted

参数类型:指针类型,size_t*

以指针的方式使用主函数的变量。
转换后的多字节字符串的字符数。

_Dst

参数类型:指针类型,char*

指向转换后的字符串的坐标。

_DstSizeInBytes

参数类型:size_t

用来接收转换后字符char类型缓冲区的大小。单位char。

_Src

参数类型:const wchar_t*

被转换宽字符串首地址。

_MaxCountInBytes

参数类型:size_t

缓冲区的字符个数,用于裁剪转换后的宽字符串。

返回值

成功返回0, 失败则返回失败代码。

利用MultiByteToWideChar,wcstombs_s在控制台输出UFT-8编码的文本

修改上述代码如下

#include 
#include 
#include 
#include 

int main()
{
	char filename[] = "02.txt";
	FILE* fp = NULL;
	const char* str;
	const char* cstr;
	const wchar_t* wstr;
	char* sp;
	size_t size;

	if (fopen_s(&fp, filename, "r") != 0)
		return -1;
	/*创建并初始化一个与文本中字符个数相等的字符串str*/
	for (size = 0; feof(fp) == 0; fgetc(fp), size++);
	str = malloc(size);
	memset(str, 0, size);
	/*为字符串str赋值,结束符换成'\0'*/
	fopen_s(&fp, filename, "r");
	for (sp = str; feof(fp) == 0; *sp = fgetc(fp), sp++);
	*(sp - 1) = '\0';

	puts(str);

	/*创建宽字符串wstr,赋值str的Unicode编码格式*/
	size = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
	wstr = malloc(2 * size);
	memset(wstr, 0, 2 * size);
	MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, size);

	setlocale(LC_ALL, ".936");

	/*创建字符串cstr,赋值wstr的ANSI编码格式*/
	wcstombs_s(&size, NULL, 0, wstr, 0);
	cstr = malloc(size);
	memset(cstr, 0, size);
	wcstombs_s(NULL, cstr, size, wstr, size);

	puts(cstr);
	return 0;
}

运行结果
Aya的学习笔记:C语言,字符编码格式与转换。详解 MultiByteToWideChar,WideCharToMultiByte,mbstowcs_s,wcstombs_s_第8张图片

你可能感兴趣的:(Aya的学习笔记)