例:
现有有一个文本文件: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;
}
以下是输出结果:
在记事本的右下角可以看到 01.txt 的编码格式为ANSI。
如果打开编码格式为UTF-8的 02.txt 会怎么样呢?
似乎出现了乱码。
我们先了解一些关于编码格式的知识
Aya的学习笔记:字符编码格式
ASCII (美国信息交换标准代码,American Standard Code for Information Interchange)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准。
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通常用两个字节表示一个字符(UTF-16),原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。
Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。
因为Unicode有多种编码格式,编码方式不同的文本在硬件看来都是一堆二进制码。所以需要BOM标记来区分。
BOM(Byte Order Mark),字节顺序标记。出现在文本文件头部,Unicode编码标准中用于标识文件是采用哪种格式的编码。
不同编码格式的文档存储在寄存器上,如果依次按字节读取数据,读出的也并不相同。
如上,硬件读取到了采用UTF-8编码的数据,在 Windows 10 简体中文 环境下这些数据被当作GBK编码的数据显示,于是出现乱码。
实际上对于计算机来说,这些并不是乱码。而是UTF-8编码的数据,只是人们并不能识别。
所以对于不同编码的文档,需要用不同方式显示为人类能识别的符号。
由于02.txt的文本太多不方便演示,现新建03.txt设置成ANSI编码格式。04.txt设置成UTF-8编码格式。
内容为:
注:最后一行是标点符号也是全角。
修改上述代码如下
#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;
}
4行文本在控制台输出5行
最后一行 FF 换算为二进制为 1111 1111 是文本结束符(EOF)。
可以对照ASCII码表看出,
前三行是纯粹的ASCII码。
最后一行除最后一个换行符外,四个汉字加上全角的逗号(,)句号(。)共6个字符,占12字节。
实际上这就是GBK编码。与ASCII码兼容,每个汉字占两字节。
GBK:
你:C4 E3
好:BA C3
世:CA C0
界:BD E7
我们发现,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
以中文举例:
“你”
Unicode: 4F60
0100 1111 0110 0000
UTF-8: E4BDA0
1110 0100 1011 1101 1010 0000
把Unicode和UTF-8固定格式以外部分对齐可以轻易发现规律。
Unicode转换为UTF-8只需要在每个字节中除固定格式外的二进制位上按序填入Unicode编码。
现在知道了输出的乱码是什么,那么如何转换呢?
当然是用转换函数了!
下面介绍的是MultiByteToWideChar,WideCharToMultiByte,mbstowcs,wcstombs。四种转换函数。
Windows提供了两种API函数:MultiByteToWideChar()和WideCharToMultiByte()
用于字符串和宽字符串的互相转换。
头文件:windows.h
这两个函数是由Windows API函数,不具有通用性。
函数功能:
该函数映射一个字符串到一个宽字符(unicode)的字符串。由该函数映射的字符串没必要是多字节字符组。
函数原型:
int MultiByteToWideChar(
UINT CodePage,//指定执行转换的字符集
DWORD dwFlags,//一组位标记
LPCCH lpMultiByteStr,//指向将被转换字符串的字符。
int cchMultiByte,//上个参数:字符串的长度。
LPWSTR lpWideCharStr,//指向接收被转换字符串的缓冲区。
int cchWideChar//缓冲区的宽字符个数。
);
参数类型: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转换格式 |
参数类型:DWORD (双字,4字节,无符号长整形,unsigned long)
一组位标记用以指出是否未转换成预作或宽字符(若组合形式存在),是否使用象形文字替代控制字符,以及如何处理无效字符。你可以指定下面是标记常量的组合,含义如下:
宏定义 | 值 | 说明 |
---|---|---|
MB_PRECOMPOSED | 1 | 总是使用预制字符,即有单个预制字符时,就不会使用分解的基字符和不占空间字符。此为函数的默认选项,不能和MB_COMPOSITE合用 |
MB_COMPOSITE | 2 | 总是使用分解字符,即总是使用基字符+不占空间字符的方式 |
MB_USEGLYPHCHARS | 4 | 使用像形字符代替控制字符 |
MB_ERR_INVALID_CHARS | 8 | 设置此选项,函数遇到非法字符就失败并返回错误码ERROR_NO_UNICODE_TRANSLATION,否则丢弃非法字符 |
注意:
对于下列代码页,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。
参数类型:LPCCH (指针类型,const char*)
指向将被转换字符串的字符。
参数类型:int
指定由参数lpMultiByteStr指向的字符串中字节的个数。
如果lpMultiByteStr指定的字符串以空字符终止。可以设置为-1
如果字符串不是以空字符中止,设置为-1可能失败,可能成功。
此参数设置为0函数将失败。
参数类型:LPWSTR (指针类型,wchar_t*)
指向接收被转换字符串的坐标。
参数类型:int
指定由参数lpWideCharStr指向的缓冲区的宽字符个数。
若此值为零,函数返回缓冲区所必需的宽字符数,在这种情况下,lpWideCharStr中的缓冲区不被使用。
注意
指针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值。
函数功能:
该函数可以映射一个unicode字符串到一个多字节字符串,执行转换的代码页,接收转换字符串,允许额外的控制等操作。
函数原型:
int WideCharToMultiByte(
UINT CodePage,//指定执行转换的字符集
DWORD dwFlags,//一组位标记
LPCWCH lpWideCharStr,//指向将被转换宽字符串的字符。
INT cchWideChar,//上个参数:宽字符串的长度。
LPSTR lpMultiByteStr,//指向接收被转换字符串的缓冲区。
INT cchMultiByte,//缓冲区的字符个数。
LPCCH lpDefaultChar,// 遇到一个不能转换的宽字符,函数便会使用此参数指向的字符
LPBOOL pfUsedDefaultChar //至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE
);
既然要解决乱码问题,为什么先介绍字符转宽字符的函数呢?
当然是因为WideCharToMultiByte函数的参数多啊!
WideCharToMultiByte函数的参数比MultiByteToWideChar函数多两个,两个函数的前五个参数几乎相同。
参数类型:LPCWCH (指针类型,const wchar_t*)
指向将被转换的unicode字符串。
参数类型:LPSTR (指针类型,char*)
指向接收被转换字符串的坐标。
lpDefaultChar 参数类型:LPCCH (指针类型,const char*)
pfUsedDefaultChar 参数类型:LPBOOL (指针类型,int*)
只有当WideCharToMultiByte函数遇到一个宽字节字符,而该字符在uCodePage参数标识的代码页中并没有它的表示法时,WideCharToMultiByte函数才使用这两个参数。(通常都取值为NULL)
如果宽字节字符不能被转换,该函数便使用lpDefaultChar参数指向的字符。如果该参数是NULL(这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。
pfUsedDefaultChar参数指向一个布尔变量,如果Unicode字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为TRUE。如果所有字符均被成功地转换,那么该函数就将该变量置为FALSE。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。
修改上述代码如下
#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;
}
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的报错,无法执行。
函数功能:
用于将多字节编码字符串转换为宽字符编码字符串,即将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)
参数类型:指针类型,size_t*
以指针的方式使用主函数的变量。
变量被赋值转换后的字符串的长度,包括结束符。单位wchar_t
参数类型:指针类型,wchar_t*
指向转换后的宽字符串坐标。
参数类型:size_t (大小,无符号整型,unsigned int)
指定由参数_DstBuf指向的缓冲区的宽字符个数。
参数类型:const char*
被转换字符串首地址。
参数类型:size_t
缓冲区的宽字符个数,用于裁剪转换后的宽字符串。
成功返回0, 失败则返回失败代码。
函数功能:
用于将为宽字符编码字符串转换多字节编码字符串,即将字符串从wchar_t* 转换成char* 。从unicode字符转换成多字节字符。
函数原型:
errno_t __cdecl wcstombs_s(
size_t* _PtNumOfCharConverted,
char* _Dst,
size_t _DstSizeInBytes,
const wchar_t* _Src,
size_t _MaxCountInBytes
);
和mbstowcs_s几乎相同
参数类型:指针类型,size_t*
以指针的方式使用主函数的变量。
转换后的多字节字符串的字符数。
参数类型:指针类型,char*
指向转换后的字符串的坐标。
参数类型:size_t
用来接收转换后字符char类型缓冲区的大小。单位char。
参数类型:const wchar_t*
被转换宽字符串首地址。
参数类型:size_t
缓冲区的字符个数,用于裁剪转换后的宽字符串。
成功返回0, 失败则返回失败代码。
修改上述代码如下
#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;
}