002-告别乱码-libiconv-C++开源库108杰

news2025/3/13 5:55:36

本课文包含三个视频!

为什么中文版Windows是编程出现乱码的高发地带?怎么用 libiconv 把国标编码的汉字转换成宇宙统一码?怎么简化 libiconv 那些充满坑的 纯C 函数API?

在这里插入图片描述

1. 安装 libiconv

通常,你在 MSYS2 中安装过 GCC 编译套件,你的 msys2 系统中就会有 libiconv 开发库可用。可通过如下指令验证:

pacman -S libiconv

查看是否出现带有 “[已安装]” (或 installed)的 libiconv 库?

如果确实需要单独安装,指令为:

pacman -S mingw-w64-ucrt-x86_64-libiconv 

再次提醒,如果你使用的 GCC 不是 UCRT64 版本,那么你可能需要的库名称应是 mingw-w64-x86_64-libiconv (64位OS)或 mingw-w64-i686-libiconv (32位OS,不敢相信你在还在用)

2. 转码基础知识 🎥

Windows (中文版)是 编程出现乱码的高发地带之一。原因来自历史包袱:当年,各国政府(自然包括我国)都要求微软出的本地语言版本的操作系统,其字符编码,必须遵守当年各国国标。

004-libiconv编码转换-1基础知识-c++108杰

3. 函数使用包装

3.1 主要函数介绍

libiconv 主要函数有三个:

  • iconv_open

原型:

iconv_t iconv_open(char const* toCode, char const * fromcode)
  • 其中, iconv_t 为 “void *” 的别名
  • 函数失败时,并不返回 nullptr,而是返回 (iconv_t)(-1)
  • 入参依序为:目标编码名称,源编码名称

以下称该函数打开成功得到的结果为 “句柄”。

  • iconv

原型:

size_t iconv( iconv_t cd,
              char** in, size_t* inBytesLeft, 
              char** out, size_t* outBytesLeft);

第一个参数为句柄。其余四个参数都既为入参也为出参。

  1. in:作入参,用于指示当前待转换的源字符串缓存区开始位置;作出参,用于告诉调用者,本次转换的结束位置(即下次转换的开始位置);

  2. inByteLeft:作入参,指示当前待转换的源字符串缓存区有多大;作出参,告诉调用者,本次转换后还剩下多少字节未转换;

  3. out: 作入参,用于指示可用于存储转换结果的缓存区开始位置;作出参,用于告诉调用者,本次转换后,结果存放的结束位置(可考虑作为下次用于存储结果的开始位置,本课堂出于简化,未采用此方法);

  4. outBytesLeft: 作入参,用于指示本次转换可用来存储结果的缓存区大小(字节数);作出参,用于告诉调用者,本次转换后,用于存储结果的缓存区还剩余多少字节。

in 和 out 的内容都有可能造成本函数转换中途停下。最典型的如:out ,也就是输出缓存区大小不够了。比如说,源字符串有 61 字节,但目标输出缓存区(也就是 out)只有20个字节,就会造成 iconv() 转换若干字节后,就停下,并借助 errno (C库的一个宏,类似全局变量,但本质是对应到一个函数调用,且线程安全),告诉调用者:输出缓存区不足。

注意,编码转换并非 1:1 转换,由于源编码和目标编码用以表达单一个本地字符(比如一个汉字)的长度不一样,因此二者之间并不存在某个简单的比例关系。典型的如使用 UTF-8 编码表达一个汉字,可能是 2字节,也可能是 3字节、4字节。

为了避免 “输出缓存区不足” ,最粗暴的方法就是为 out 分配一个 “巨大” 的空间——比如,是源字符串长度的 4 倍、5 倍……这种方法既浪费内存,并且通常需要使用到 new 来动态分配内存,进一步拉低性能。

我们的解决方法相对复杂,但高效(或者说性能均衡):采用固定大小的临时连续内存来存储每次转换的结果,同时准备一个字符串流(std::stringstream)来连续存储每次转换的结果(新结果追加到旧结果之后)。

3.2 函数简化封装 🎥

005-libiconv编码转换-2简化函数-C++108杰

3.3 函数封装主要代码

namespace d2::myiconv
{

// 转换结构
struct IConvResult
{
    std::string result; // 转换成功得到的,使用新编码的字符串

    int errorNumber = 0; // 对应 errno, 出错号
    std::string errorMessage; // 出错信息
};

// 转换函数
IConvResult Convert(std::string_view in, char const* fromCode, char const* toCode)
{

IConvResult Convert(std::string_view in, char const* fromCode, char const* toCode)
{
    IConvResult ir;

    // 调用 iconv_open
    iconv_t cd = iconv_open(toCode, fromCode);

    if (cd ==(iconv_t)(-1)) // 打开失败
    {
        ir.errorNumber = errno; // C -> C++

        switch (ir.errorNumber)
        {
        case EINVAL:
            ir.errorMessage = "不支持的编码";
            break;
        case ENOMEM:
            ir.errorMessage = "内存不足";
            break;
        default:
            ir.errorMessage = "未知错误";
            break;
        }

        return ir;
    }

    char* inBufferPtr = const_cast<char *>(in.data()); // 指向输入缓存位置 (非常量)
    size_t inBytesLeft = in.size(); // 输入缓存大小

    std::stringstream ss;

    std::size_t const sizeOfOutBuffer = 20; // 输出缓存区大小
    char outBuffer[sizeOfOutBuffer]; // 输出缓存

    while (inBytesLeft > 0) // 输入缓存中,还有剩余字符未被转换
    {
        char *outBufferPtr = outBuffer;
        size_t outBufferLeft = sizeOfOutBuffer;

        size_t result = iconv(cd, &inBufferPtr, &inBytesLeft, &outBufferPtr, &outBufferLeft);

        if (result == (size_t)(-1)) // 转换停止了
        {
            auto n = errno;

            switch (n)
            {
                case E2BIG: // 输出缓存区不够用了……
                {                
                    break;
                }

                case EILSEQ: 
                {
                    ir.errorNumber = n;
                    ir.errorMessage = "输入字符序列不符合指定编码规则";
                    break;
                }

                case EINVAL:
                {
                    ir.errorNumber = n;
                    ir.errorMessage = "输入的字符序列不完整"; 
                    break;
                }
                
                default:
                {
                    ir.errorNumber = n;
                    ir.errorMessage = "转换过程发生未知错误";                
                    break;
                }
            }
        }

        if (ir.errorNumber != 0)
        {
            break; // while
        }

        ss.write(outBuffer, sizeOfOutBuffer - outBufferLeft); // 将本轮的输出结果,写入输出流
    } // while

    iconv_close(cd);
    
    if (ir.errorNumber == 0)
    {
        ir.result = ss.str();
    }

    return ir;
}       

} //namespace d2::myiconv

使用示例:

char const* gbk = "假设这是一个GBK编码的字符串";

auto ir = d2:myiconv::Convert(gbk, "GBK", "UTF-8");

if (ir.errorNumber != 0)
{
    std::cout << ir.errorNumber << " : " << ir.errorMessage << "\n";
}
else
{
    // 转换成功:
    std::cout << ir.result << std::endl;
}

对应的 CMakeLists.txt 示例:

cmake_minimum_required(VERSION 3.10.0)
project(HelloLibIconv VERSION 0.1.0 LANGUAGES C CXX)

add_executable(HelloLibIconv main.cpp gbk_str.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE iconv)
target_link_directories(${PROJECT_NAME} PRIVATE "c:/msys64/ucrt64/lib")

其中的 gbk_str.cpp 在 VSCODE 中应明确使用 GBK 编码保存,其内容为:

char const* gbk_str = "我是一个GBK字符编码的字符串!请保障我所在的CPP文件编码为GBK!"; 

4. 项目应用 🎥

上一节中 libfswatch 在监控 Windows 下名字带汉字的文件对象时,文件名输出会出现乱码。其原因于:libfswatch 从 Windows 读文件对象信息时,未使用特定的 UNICODE 版本的 API,而是使用 Windows 本地语言版 API,因此读到的文件名中的中文字符是 GBxxxx 编码(既中国国标),但 libfswatch 将它视为 UTF-8 编码。

基于 libiconv,使用我们所包装的函数,解决乱码的核心代码是:

        // 输出变动的文件路径:
        auto utf8Path = d2::myiconv::Convert(event.get_path(), "GBK", "UTF-8"); // 编码转换
        if (utf8Path.errorNumber != 0)
        {
            std::cout << utf8Path.errorMessage << std::endl;
            break;
        }

        std::cout << utf8Path.result << "\n";

下面的视频给出了采用我们所写的 Convert() 的解决方案。

006-libiconv编码转换-3项目应用-C++108杰

5. C++ 封装

d2::myiconv::Convert()函数使用起来,比原来的纯C函数“三板斧”组合,要方便不少,但也有个比较明显的缺点:无法复用 iconv_open() 得到的句柄,每次转换都需要先打开最后再关闭。如果仅是一次性转换无所谓,但有时有多个字符串需要分开转换,句柄不能复用的弊端就比较明显了。

解决方法是使用 C++ 面向对象的思想进一步加以封装,我们给出一种思路的类设计(仅 class 设计):

namespace d2::myiconv
{

// 转换结果
struct IConvResult
{
    std::string result; // 转换成功得到的,使用新编码的字符串

    int errorNumber = 0; // 对应 errno, 出错号
    std::string errorMessage; // 出错信息
};

class IconvHelper final // 注:实现为 final 类
{
public:
    // 构造(失败时可抛出异常)
    IconvHelper(char const* fromCode, char const* toCode) noexcept(false);
   
    IconvHelper(IconvHelper const& ) = delete; // 不允许复制
    IconvHelper& operator = (IconvHelper const&) = delete;

    IconvHelper(IconvHelper&& ih) noexcept; // 支持转移
    IconvHelper& operator = (IconvHelper&& ih) noexcept; 

    ~IconvHelper() noexcept; // 析构

    // 静态转换,方便一次性转换 (不允许抛出异常)
    static IConvResult Convert(std::string_view in,
                  char const* fromCode, char const* toCode) noexcept;

    // 非静态的转换方法,方便多次复用 (不允许抛出异常)
    IConvResult Convert(std::string_view in) noexcept;  

private:
    iconv_t cd;
};

} //  namespace d2::myiconv

请进入 d2school 网站 本课的作业区,完成符合上述类定义的 C++ 版本的 libiconv 封装,并及时交作业。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2314113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DeepSeek赋能智慧交通:城市交通流量智能预测与优化,开启智能出行新时代

在数字化转型的浪潮中&#xff0c;智慧交通正成为提升城市运行效率、改善居民出行体验的关键领域。 DeepSeek作为人工智能领域的前沿技术&#xff0c;凭借其强大的数据分析、智能决策和多模态交互能力&#xff0c;正在为智慧交通注入新的活力&#xff0c;推动交通管理从“经验…

Token登录授权、续期和主动终止的方案(Redis+Token(非jwtToken))

1、RedisToken方案的授权 1.1 基本原理 登录后使用UUID生成token&#xff0c;前端每次请求都会带上这个token作为授权凭证。这种方案是能自动续签&#xff0c;也能做到主动终止。所以很多项目用的都是RedisToken方案&#xff0c;简单方便问题少。缺点就是需要依赖Redis和数据…

强大的数据库DevOps工具:NineData 社区版

本文作者司马辽太杰&#xff0c; gzh&#xff1a;程序猿读历史 在业务快速变化与数据安全日益重要的今天&#xff0c;生产数据库变更管理、版本控制、数据使用是数据库领域的核心挑战之一。传统的解决方式往往采用邮件或即时通讯工具发起审批流程&#xff0c;再通过堡垒机直连数…

【动态规划篇】1137. 第 N 个泰波那契数

前言&#xff1a; 动态规划问题一般分为五步&#xff1a; 先确定一个状态表示根据状态表示来推导状态方程初始化填表顺序返回值 ①状态表示 先创建一个以为数组&#xff0c;起名为dp,这个一维数组就叫做dp表 把dp表填满&#xff0c;填满后的某个值就是我们想要的结果状态表…

网络信息安全专业(710207)网络安全攻防实训室建设方案

一、引言 随着信息技术的飞速发展&#xff0c;网络空间安全已成为国家安全的重要组成部分&#xff0c;对网络信息安全专业人才的需求日益增长。为满足网络信息安全专业&#xff08;专业代码710207&#xff09;的教学需求&#xff0c;提升学生在网络安全攻防领域的实践能力&…

【Linux】:线程池

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家带来线程池相关的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结构…

共享内存(System V)——进程通信

个人主页&#xff1a;敲上瘾-CSDN博客 进程通信&#xff1a; 匿名管道&#xff1a;进程池的制作&#xff08;linux进程间通信&#xff0c;匿名管道... ...&#xff09;-CSDN博客命名管道&#xff1a;命名管道——进程间通信-CSDN博客 目录 一、共享内存的原理 二、信道的建立 …

ctfhub-HTTP协议

请求方式 它要我们使用CTF**B Method,其实就是ctfhub方式 我们直接抓包试一试&#xff0c;把GET改成CTFHUB,在发送到repeater 在repeater处点击发送&#xff0c;得到响应 302跳转 点击“give me flag"没有任何变化&#xff0c;我们抓个包试试 我们把它发送到repeater&…

【TMS570LC4357】之工程创建

备注&#xff1a;具体资料请在官网海淘.TMS570LC4357资料 在线文档Hercules Safety MCU Resource Guide — Hercules Safety MCUs Documentation XDS100 Debug Probe (ti.com) Git https://git.ti.com/git/hercules_examples/hercules_examples.git https://git.ti.com/cgit/h…

一种改进的Estimation-of-Distribution差分进化算法

为了充分利用差分进化&#xff08;DE&#xff09;的强大开发和estimation-of-distribution算法&#xff08;EDA&#xff09;的强大探索&#xff0c;提出了一种混合estimation-of-distribution算法的改进差分进化IDE-EDA。首先&#xff0c;提出了一种新的协同进化框架&#xff0…

[数据结构]排序之希尔排序( 缩小增量排序 )

希尔排序法又称缩小增量法。希尔排序法的基本思想是&#xff1a; 先选定一个整数&#xff0c;把待排序文件中所有记录分成个 组&#xff0c;所有距离为的记录分在同一组内&#xff0c;并对每一组内的记录进行排序。然后&#xff0c;取&#xff0c;重复上述分组和排序的工 作。当…

进程(下)【Linux操作系统】

文章目录 进程的状态R状态&#xff1a;S状态&#xff1a;D状态&#xff1a;T状态t状态Z状态&#xff1a;孤儿进程X状态&#xff1a; 进程的优先级如果我们要修改一个进程的优先级重置进程优先级 进程切换进程的调度 进程的状态 在内核中&#xff0c;进程状态的表示&#xff0c…

Insar结合ISCE2,某一个文件进行并行-stackSentinel.py

stackSentinel.py 依次执行 run_01 到 run_15&#xff0c;记录各自的日志 并行执行 run_16 里的所有命令&#xff0c;仍然记录日志 不知道对不对&#xff0c;测试的时间有点长就给停了 #!/bin/bash# ✅ 适用于 WSL/Linux runfiles_path"/mnt/e/insar_order_test/Stack…

2.2.3 TCP—UDP-QUIC

文章目录 2.2.3 TCP—UDP-QUIC1. TCP如何做到可靠性传输1. ACK机制2. 重传机制3. 序号机制4. 窗口机制5. 流量机制6. 带宽机制 2. tcp和udp如何选择1. tcp和udp格式对比2. ARQ协议&#xff08;Automatic Repeat reQuest&#xff0c;自动重传请求&#xff09;1. ARQ协议的主要类…

【Golang】第一弹-----初步认识GO语言

笔上得来终觉浅,绝知此事要躬行 &#x1f525; 个人主页&#xff1a;星云爱编程 &#x1f525; 所属专栏&#xff1a;Golang &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 一、Go语言的简单介绍 1、G…

K8S学习之基础二十三:k8s的持久化存储之nfs

K8S持久化存储之nfs ​ 在 Kubernetes (k8s) 中使用 NFS&#xff08;Network File System&#xff09;作为存储解决方案是一种常见的方式&#xff0c;特别是在需要共享存储的场景中。以下是关于如何在 Kubernetes 中使用 NFS 存储的详细说明&#xff1a; 1. 准备 NFS 服务器 …

【Linux通信篇】深入理解进程间通信——管道

--------------------------------------------------------------------------------------------------------------------------------- 每日鸡汤&#xff1a;找一个对的人&#xff0c;然后好好去爱。一个你跟他在一起&#xff0c;然后又可以舒舒服服做自己的人。 -------…

Redis--Set类型

目录 一、引言 二、介绍 三、命令 1.sadd,smembers,sismember 2.spop&#xff0c;srandmember 3.smove&#xff0c;srem 4.sinter&#xff0c;sinterstore 5.sunion,sunionstore,sdiff,sdiffstore 四、内部编码 1.intset 2.hashtable 五、应用场景 1.使用Set保存用…

【0013】Python数据类型-列表类型详解

如果你觉得我的文章写的不错&#xff0c;请关注我哟&#xff0c;请点赞、评论&#xff0c;收藏此文章&#xff0c;谢谢&#xff01; 本文内容体系结构如下&#xff1a; Python列表&#xff0c;作为编程中的基础数据结构&#xff0c;扮演着至关重要的角色。它不仅能够存储一系…

文件上传靶场(10--20)

目录 实验环境&#xff1a; 具体内容实现&#xff1a; 第十关&#xff08;双写绕过&#xff09;&#xff1a; 第十一关&#xff1a;&#xff08;%00截断&#xff0c;此漏洞在5.2版本中&#xff09; 正确用法 错误用法 思路&#xff1a; 操作过程&#xff1a; 第十二关…