老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>
汉字是什么类型
汉字是char类型么,显然不是,在C或C++的语境里,char的值一般都在0-255之间,显然,汉字远远超过了这个区间。
但我们赋值的时候还是用char数组或string来赋值,也一样可以输出汉字,当然输入就是另外一回事了。
当你可以用char来输出汉字,意味着你在用多字节模拟宽字符,比如汉字 “我” ,在utf8编辑环境中,它的strlen是3,也就是要用3个char才能表示一个汉字。
这在普通的文本输出时没有问题,但是涉及到文本编辑时候,那就是大问题了。
你如何查找修改汉字?在C或C++中所有用于普通字符,也就是char的函数是无法做到的。
当然,面对这种问题的不只是中文,其余所有非英语国家都会遇到同样的问题,如何编辑本地区的文字。
这种问题广泛存在于非英语国家,于是宽字符应运而生,也就是wchar_t。 这是由两字节(Windows)或四字节(Linux)组成的一种派生类,一个字节只能容纳256个不同的符号,两个字节就是65536种,近乎可以囊获全球各地区的多数符号。
宽字符的使用现状
如此贴心的设置是否所有程序员都运用自如?答案恰恰相反,程序员们,尤其是没有经历汉字折磨的程序员,基本不了解,不知道,甚至忽略,就如原来的我。
为什么?
大概因为经典的C和C++的教科书作者,都是英语国家的人,创立C和C++的教父们,也是英语国家的人,在他们的C圣经,C++圣经中,宽字符只能在附录中出现大约一两百字,而近乎所有的学习者,都会选择性忽略,直至被汉字问题吊打。
问答区关于汉字问题的现状
不去问答区,我近乎无法知道,对于字符类型的理解,我们的学习者究竟有多混乱,而我们的回答者,又有多随意。
面对同样一个 “为什么我输出汉字是乱码” 的回答,充斥着各种奇技淫巧,包括不限于:
-
更改编码模式,
-
更改终端模式,
-
更有甚者,更改操作系统地区模式,
-
更改注册表以更改终端编码模式
等等等等。
如何带着一辆自行车去3里外的商场,有人说推着,有人说倒着推省力,更有甚者建议扛着去。
我不反对,像这种做法确实可以暂时解决问题,但自行车难道就不能用它应有的使用方法,骑着去么。
什么时候该使用宽字符,怎么使用宽字符
按道理,涉及非char字符的字符就可以使用宽字符,比如汉字。
但是,情况比这复杂一些,因为你可能只是一个程序的维护者,一个库的使用者,当程序的缔造者就不用宽字符定义汉字的时候,你只能接受,要么重构程序,要么妥协。
我的意见是,妥协。编码方式比程序本身更为底层,有时候妄图在一堆复杂程序代码中改变编码,近乎一定会引出bug。
还有一个问题,不同编译器对宽字符的支持是不同的,甚至其底层调用的c运行时库对宽字符编码,存在bug,数年没有修复,比如Windows的msvcrt,用msys2 + mingw64系统的程序员,怕是逃不掉了。还好,不是每个程序都涉及汉字的编辑。
同时,宽字符使用,需要给程序一个地区信息,同样一个设置方法,在gcc和在clang中,效果完全不同,有些甚至就是摆设。
具体参见:MinGW-W64使得printf、cout、wprintf、wcout显示出中文的种种
回到正题,什么时候该使用宽字符,当你是程序的开始的开发者,程序涉及中文的输入,输出,编辑,那么用宽字符。
虽然在不同平台可移植性不强,但是C语言和C++就不是以可移植性见长的语言,就是同样一个系统,不同编译器,甚至相同编译器的不同版本都不兼容,那还多这几个宽字符么。
用一个问答中的例子粗略的说一下宽字符的使用:在TXT文本中查找特定汉字
在不涉及汉字的情况,比较字符和字符串有很多方法,无论是用C的标准函数,C++的==比较符号,都顺理成章。
当涉及汉字,且 IO 不仅包含标准输入输出,还包括文件的输入输出,就要极为小心。
下面这个程序是通过标准输入,取得一个汉字,通过文件流,取得用于比较的字符串,查找文件汉字中是否有标准输入中传来的汉字,统计个数。
说一下环境,这是在windows系统下,用msys2 + Clang64,使用clang.exe 编译的C语言程序。程序编码是utf8,用于查找汉字的文件编码是utf16le。
如果你的环境与上面任何一项不符,都保证不了程序最后的正确,C与C++不以兼容性见长。
我们一步一步拆解:
#include <locale.h>
这里引入此头文件的意图,是用于使用如下函数:
puts(setlocale(LC_CTYPE, ""));
这个函数的意思,是设置地区为本地,在中国,就是“.936”,也就是简体中文gbk,也就是windows cmd的默认设置。之所以为空,是因为有些编译器不一定实现“.936”的设置方法,但空字符串和“C”字符串是标准中强制支持的。空代表本地区,C代表C默认。
#include <wchar.h>
上面这个头文件,则是给出宽字符类型的定义,在我的环境下,只要有<stdio.h> 文件,就可以使用宽字符,这里只是说明,如果没有包含上述库文件,可以单独用此头文件以使用宽字符。
#include <string.h>
string的头文件,不仅提供普通字符的操作函数,也包括对宽字符的支持。
char ch[] = "中";
int len = (int)strlen(ch);
以上是让你看一下,你眼中的一个汉字在计算机眼中是几个字符。utf8下 len = 3,不是你想象的2字节。
len = sizeof(wchar_t);
这个是看一下wchar_t占用的字节数,windows一般为2字节,linux是4字节。
wchar_t Wstr[N];
wchar_t Srch;
这是设置宽字符和宽字符数组。
//用utf16le编码格式存储可以直接读入宽字符。
if ((fp = fopen("word.txt", "rb")) == NULL)
{
printf("文件打开失败!\n");
exit(0);
}
这是用二进制打开一个文本文件,请务必确认文本文件是以utf16le为编码方式
fputws(L"输入统计的汉字:\n", stdout);
fwscanf(stdin, L"%lc", &Srch);
从标准IO输入输出也要用宽字符函数。宽字符的表示方法是:L“ … ”,输入的类型是 “%lc” 或 “%ls” 这个 L 和小写 l( L 的小写形式) 应该是地区的意思。
while ((stringlen = fread(Wstr, sizeof(wchar_t), N, fp)) != 0)
{
for (int i = 0; i < (int)stringlen; i++)
{
if (Wstr[i] == Srch)
{
count++;
}
}
}
用二进制的方式读取文件,同时将读取字符的长度用于for循环。注意,这不是字符拷贝,而是内存搬运,不要指望程序会自动给你的字符串添加末位0。所以指定边界,需要自己做。
宽字符的比较可以直接用==,因为它在Windows下是 一个 双字节的字符
以下是原程序。
#include <locale.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#define N 40
int main()
{
char ch[] = "中";
int len = (int)strlen(ch);
len = sizeof(wchar_t);
int count = 0;
wchar_t Wstr[N];
wchar_t Srch;
FILE *fp;
//用utf16le编码格式存储可以直接读入宽字符。
if ((fp = fopen("word.txt", "rb")) == NULL)
{
printf("文件打开失败!\n");
exit(0);
}
puts(setlocale(LC_CTYPE, ""));
fputws(L"输入统计的汉字:\n", stdout);
fwscanf(stdin, L"%lc", &Srch);
size_t stringlen = 0;
while ((stringlen = fread(Wstr, sizeof(wchar_t), N, fp)) != 0)
{
for (int i = 0; i < (int)stringlen; i++)
{
if (Wstr[i] == Srch)
{
count++;
}
}
}
printf("%d\n", count);
fclose(fp);
return 0;
}
注意,以上代码在linux不可用,因为linux 的wchar_t是utf-32编码,也就是4字节。
此时可以用 <uchar.h> 头文件引入char16_t,这是双字节字符,在输入文件为utf16le编码可以使用,但问题是无法输出到标准输出。需要转换。
对于中文的输出,linux其环境为utf8编码,普通字符char表示中文,不存在乱码问题,但你不要觉得Linux就没坑,一旦涉及到汉字的编辑,又不得不用宽字符,需要转换:
extern size_t mbstowcs (wchar_t *__restrict __pwcs,
const char *__restrict __s, size_t __n) __THROW;
对于Linux环境用二进制操作中文文本文件,不能用windows那样直接读取wchar_t字符串,我们没有现成的文本编辑器能将文本转为utf-32。但可以通过文本方式读取utf8编码的文本文件,在后序进行转换。但转换中有个坑,我踩过。
因为utf8编码的中文字符是3字节,所以你要保证,输入包含中文字符的字符串不能截断中文字符,也就是说,不能把3个char字符切开,但你如何保证呢。
你可以保证设的buffer足够大,大到足够装下文本中最大的段落,也就是任意两个换行符中间的所有字符串。
所以,如果不涉及标准输出,可以用char16_t进行代替。以下是Linux的完整实现。注意输入的文件也是utf16le编码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <uchar.h>
#define N 40
int main()
{
int count = 0;
char16_t Srch = u'是';
char16_t str[N] = u"\0";
FILE *fp;
if ((fp = fopen("word.txt", "rb")) == NULL)
{
printf("文件打开失败!\n");
exit(0);
}
fputs("输入统计的汉字:\n", stdout);
size_t stringlen = 0;
while ((stringlen = fread(str, sizeof(char16_t), N - 1, fp)) != 0)
{
for (int i = 0; i < (int)stringlen; i++)
{
if (str[i] == Srch)
{
count++;
}
}
}
printf("%d\n", count);
fclose(fp);
return 0;
}
当然,坑还没填完,如果要输出到标准输出,需要再转回utf8,另外,Linux对于宽字符输出到标准输出目前也不够完善,比如用不了fputws(),在gcc,这就是个摆设。可以用fprint(stdout, “%ls\n”, wchar_string) 解决。
给一个字符转换的实现,其实就是对iconv进行简单封装。
#ifndef UTFCONV
#define UTFCONV
#include <iconv.h>
#include <string.h>
#include <uchar.h>
//将长度为len字节的字符串str从oriCode转换为newCode,存储到长度为outlen字节的outstr字符串
int codeConv(const char *oriCode, const char *newCode, char *str, size_t len, char *outstr, size_t outlen)
{
memset(outstr, 0, outlen);
iconv_t cd = iconv_open(newCode, oriCode);
if (cd == NULL)
{
return -1;
}
if (iconv(cd, &str, &len, &outstr, &outlen) == -1)
{
iconv_close(cd);
return -1;
}
iconv_close(cd);
return 0;
}
//将长度为len字节的字符串str从utf16转换为utf8,存储到长度为outlen字节的outstr字符串
int U16U8(char16_t *str, size_t len, char *outstr, size_t outlen)
{
return codeConv("utf-16le", "utf-8", (char *)str, len, outstr, outlen);
}
//将长度为len字节的字符串str从utf8转换为utf16,存储到长度为outlen字节的outstr字符串
int U8U16(char *str, size_t len, char16_t *outstr, size_t outlen)
{
return codeConv("utf-8", "utf-16le", str, len, (char *)outstr, outlen);
}
#endif
看到最后,估计小伙伴会说还是不要跳这个坑了吧。too young,这是你能决定的么,毕竟这里是china,早晚要用到中文。
其实我们大部分时间可能不必读写二进制文件,以文本模式读写宽字符,在Windows和Linux都是很友好的,widows可以用宽字符直接读写GBK编码的文件,Linux可以用宽字符直接读写utf8编码的文件。
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#define N 40
int main()
{
int count = 0;
wchar_t Wstr[N];
wchar_t Srch;
FILE *fp;
// windows用gbk可以读宽字符,Linux用utf8可以读宽字符
if ((fp = fopen("word.txt", "r")) == NULL)
{
printf("文件打开失败!\n");
exit(0);
}
puts(setlocale(LC_CTYPE, ""));
fputws(L"输入统计的汉字:\n", stdout);//Linux要用fputs("输入统计的汉字:\n", stdout);
Srch = fgetwc(stdin);
size_t stringlen = 0;
while (fgetws(Wstr, N, fp))
{
stringlen = wcslen(Wstr);
for (int i = 0; i < (int)stringlen; i++)
{
if (Wstr[i] == Srch)
{
count++;
}
}
}
printf("%d\n", count);
fclose(fp);
return 0;
}
老林的C语言新课, 想快速入门点此 <C 语言编程核心突破>