2022-04-14 在TXT文档中查找汉字——C语言及C++中多字节与宽字符的区别

在TXT文档中查找汉字——C语言及C++中多字节与宽字符的区别


老林的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++圣经中,宽字符只能在附录中出现大约一两百字,而近乎所有的学习者,都会选择性忽略,直至被汉字问题吊打。

问答区关于汉字问题的现状

不去问答区,我近乎无法知道,对于字符类型的理解,我们的学习者究竟有多混乱,而我们的回答者,又有多随意。

面对同样一个 “为什么我输出汉字是乱码” 的回答,充斥着各种奇技淫巧,包括不限于:

  1. 更改编码模式,

  2. 更改终端模式,

  3. 更有甚者,更改操作系统地区模式,

  4. 更改注册表以更改终端编码模式

等等等等。

如何带着一辆自行车去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 语言编程核心突破>


  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值