C C++最全深入浅出字符编码(1),35岁的程序员被裁

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

汉字编码

上面介绍的几种编码格式,UTF-8GBK等都支持汉字,但是标准不同,因此,在实际进行开发的过程中,对汉字的处理也不尽相同。

如何判断汉字编码

无论是UTF-8GBK,还是GB18030,或者BIG5,它都是向下兼容ASCII的,为了区分ASCII码和汉字,在汉字的高位补1。
这也就是说,如果我们以int的形式取出单个字符的值,汉字都是小于0的。
因此,判断是否是汉字也就变得简单了:

enum boolean{true, false};
typedef int boolean;

boolean isChinese(char ch){
    return (ch < 0) ? true : false;
}

写一段代码验证一下:

void test01(){
    char str[20];
    memset(str, 0, sizeof(str));
    strcpy(str, "hello汉字");
    for (int i = 0; i < strlen(str); i++){
        if (isChinese(str[i]) == true){
            printf("str[%d]: Chinese\n", i);
        }else{
            printf("str[%d]: English\n", i);
        }
    }
}

我们在main函数里调用test01函数,得到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3agEKhs-1683503222799)(/img/bVbL2Tz)]

因为在utf-8下,一个汉字占3字节,所以后面从5~10这6个字节正好代表着2个汉字。
如果我们把编码改成GB2312,运行可以得到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2uHepWzK-1683503222800)(/img/bVbL2TR)]
可以看到,只有最后4个字节是汉字,充分说明了GB2312编码格式下,一个汉字占2个字节。

如何处理汉字截断问题

如果我们把上面的字符串按字符打印出来,得到下面的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aBwxtXQi-1683503222801)(/img/bVbL2VW)]
可以看到,所有的汉字都乱码了,原因就在于,UTF-8编码下,每个汉字占3个字节,一个字节不足以表示完整的汉字,所以打印出来都是乱码的。
在实际开发中,比较常见的需要处理的问题是,截取一定长度的字符串,但是如果截取的位置正好是个汉字,难免会遇到汉字被截断的问题。
那么,这类问题如何处理呢?
根据汉字的编码规则,我们知道,UTF-8GBK对汉字的处理是不一样的。
UFT-8一个汉字是3字节,且规则如下:

1110xxxx 10xxxxxx 10xxxxxx

所以,我们很容易知道,汉字的首字节范围为11100000~11101111,转成十六进制为0xe0~0xef,第二、三字节的范围为10000000~10111111,转成十六进制范围为0x80~0xbf
所以UTF-8的汉字截断问题处理可以如下:

void HalfChinese_UTF8(const char *input, size_t input_len, char *output, size_t *output_len)
{
    char current = *(input + input_len);
    if (isChinese(current) == false)
    {
        *output_len = input_len;
        strncpy(output, input, *output_len);
        return;
    }
    //汉字
    *output_len = input_len;
    //1110xxxx 10xxxxxx 10xxxxxx
    //第二位和第三位的范围是10000000~10ffffff,转成十六进制是0x80~0xbf,在这个范围内都说明是汉字被截断
    while ((current&0xff) < 0xc0 && (current&0xff) >= 0x80)
    {
        (*output_len)++;
        current = *(input + *output_len);
    }
    strncpy(output, input, *output_len);
}

该函数有四个参数,其中inputinput_len作为原始输入,input_len代表需要截取的位置,outputoutput_len作为输出,output为截断处理后的字符串,output_len为截断处理后的长度。
我们使用下面的代码进行测试:

void test02()
{
    char in[20], out[20];
    memset(in, 0, sizeof(in));
    memset(out, 0, sizeof(out));
    strcpy(in, "hello汉字");
    size_t out_len = 0;
    for (int i = 1; i <= strlen(in); i++)
    {
        HalfChinese_UTF8(in, i, out, &out_len);
        printf("out: %s\n", out);
    }
}

运行后结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMLaQbSs-1683503222803)(/img/bVbL3r4)]

如果是GBK编码,要稍微麻烦一点。因为我们知道,GBK是双字节表示汉字,且第一个字节的值从 0x810xFE,第二个字节的值从 0x400xFE(不包括0x7F),单从字符的值无法判断到底是汉字的首字节还是后一个字节(因为二者的值有重复部分)。
如果字符串纯为汉字倒还好办,我们已经知道汉字占2个字节,直接根据长度的奇偶来判断就可以,但如果是中英文夹杂就不能采用这种方式了。
在这里,我使用的是先对字符串进行一道过滤处理,判断字符串中除掉英文字符后纯汉字的长度,如果为奇数,代表汉字被截断,加1就能取其完整的汉字,如果是偶数,说明正好是一个完整的汉字,无需处理,直接返回即可。
代码实现如下:

void HalfChinese_GBK(const char *input, size_t input_len, char *output, size_t *output_len){
    char current = *(input + input_len);
    if (isChinese(current) == false)
    {
        *output_len = input_len;
        strncpy(output, input, *output_len);
        return;
    }
    *output_len = input_len;
    if (MoveEnglish(input, input_len) %2 != 0){
        (*output_len)++;
    }
    strncpy(output, input, *output_len);
}

int MoveEnglish(const char *input, size_t input_len){
    int out_len = input_len;
    for (int i = 0; i < input_len; i++)
    {
        if (isChinese(input[i]) == false){
            out_len++;
        }
    }
    return (out_len > 0) ? out_len : 0;
}

同样使用上面的测试代码进行测试,得到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3mO5W6ia-1683503222804)(/img/bVbL3sd)]

如何实现编码之间互相转换

既然编码格式这么多,那么怎么进行编码之间的转换呢?
在C语言下,主要是利用系统的iconv函数完成。
iconv函数包含在头文件iconv.h中,其函数原型如下所示:

size_t iconv (iconv_t __cd, char **__restrict __inbuf,
                size_t *__restrict __inbytesleft,
                char **__restrict __outbuf,
                size_t *__restrict __outbytesleft);

第一个参数是转换的一个句柄,由iconv_open函数创建,第二个参数是输入的字符串,第三个参数是输入字符串的长度,第四个参数是转换后的输出字符串,第五个参数是输出字符串的长度。在编码转换完成之后,需要调用iconv_close函数关闭句柄。所以完整的调用顺序为:

  • iconv_open打开iconv句柄
  • 调用iconv进行编码转换
  • iconv_close关闭句柄

还有一点需要注意的是,__inbytesleft__outbytesleft的长度,因为不同编码对于汉字的处理字节数不同,比如从UTF-8转换为GBK,同样都是两个汉字,转换前长度为6,转换后长度为4。也就是说,在编码转换过程中,字符串可能会变长或缩短,如果长度不正确,很容易造成越界,从而导致错误。
完整的编码转换功能封装如下:

boolean convert_encoding(char *in, size_t in_len, char *out, size_t out_len, const char *from, const char *to)
{
    if (strcasecmp(from, to) == 0){
        size_t len = (in_len < out_len) ? in_len : out_len;
        memcpy(out, in, len);
        return true;
    }

    iconv_t cd = iconv_open(from, to);
    if (cd == (iconv_t)-1){
        printf("iconvopen err\n");
        return false;
    }
    size_t inbytesleft = in_len;
    size_t outbytesleft = out_len;

    char *src = in;
    char *dst = out;

    size_t nconv;
    nconv = iconv(cd, &src, &inbytesleft, &dst, &outbytesleft);
    if (nconv == (size_t)-1){
        if (errno == EINVAL){
            printf("EINVAL\n");
        } else {
            printf("error:%d\n", errno);
        }
    }
    iconv_close(cd);
    return true;
}

注意,由于使用到了libiconv,编译时需要加-liconv进行链接。
测试代码如下:

void test04()
{
    char in[20], out[20];
    memset(in, 0, sizeof(in));
    memset(out, 0, sizeof(out));
    strcpy(in, "hello汉字world");
    if (false == convert_encoding(in, strlen(in), out, 20, "utf-8", "gbk")){
        printf("failed\n");
        return;
    }
    printf("in: %s\nout:%s\n", in, out);
}

以上代码运行结果如下所示:

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

Dsgn9zP-1715708306274)]
[外链图片转存中…(img-JqBEdCwb-1715708306275)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值