002-libIconv 字符集转换库

一、基础知识

UNICODE——统一码

1.libIconv转换标准

1748684698668

2.libIconv典型用法

1748684842079

  1. 打开转换句柄
  2. 使用句柄循环转换
  3. 关闭句柄

3.转换过程详解

size_t iconv(iconv_t cd,
	char** inbuf, size_t* inbytesleft,
	char** outbuf, size_t* outbytesleft
);

参数解析:

  • cd:句柄;
  • inbuf:所要转换的字符串的缓冲区,虽然没有用 const修饰符修饰,但在转换过程中,字符串内容不会被修改
  • inbytesleft:用于指示源字符串 inbuf有多少个字节;
  • outbuf :转换后字符串缓冲区;
  • outbytesleft:用于指示存放结果的 outbuf有多少字节。

为什么缓冲区要使用 char**类型?

虽然源字符串不会被修改,但是指向源字符串的指针会修改。

举例来说:输入缓冲区有“我爱你”的国标字符(国标每字符占用2字节),在转换过程中,可能转换到第4个字节就停下了。这个时候,作为一个指向源字符串缓冲区的指针,inbuf就会停在[4]的位置,即:只成功转换“我爱”两个字符。对应的,随着转换的进行, outbuf也会向后偏移,直到转换完成或者转换出现问题停下来。同样的,inbytesleft作为 size_t*类型的指针,可以用来告知上层调用者,还有多少输入字符待转换;outbytesleft告知调用者,输出缓冲区还有多少空间可以用来填充转换后的数据。

3.1iconv()返回-1的情况,即:转换什么时候会停下来?

1748685770071

二、代码实践

1.设计思路

考虑转换失败的原因:

  1. 源码格式和目标格式错误;
  2. 输入字符串不完整或者大小指示错误;
  3. 输出字符串缓冲区溢出。

通常认为,源码格式和目标格式均为固定值,而且输入字符串大小和内容均为已知,那么为了避免错误产生,就应当在 outbuf上下功夫:

  • 方法1:申请一个巨大的 outbuf 确保其永远够用
    • 浪费内存;
    • 通常需要动态分配内存,效率低。
  • 方法2:包装函数
    • 要求用户传入的源字符串的内容必须是完整的;
    • 使用固定大小的 outbuf作为输出字符串缓冲区,循环调用,将转换的结果填充到真正的目标缓冲区。

2.代码实现

#include <iconv.h>

#include <iostream>
#include <sstream>
#include <string>

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

    int errorNum = 0; // 转换过程中发生的错误码,errno
    std::string errorMsg; // 转换过程中发生的错误信息
};

IConvResult iconvConvert(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.errorNum = errno;

        switch (ir.errorNum) {
        case EILSEQ:
            ir.errorMsg = "非法字符序列";
            break;
        case EINVAL:
            ir.errorMsg = "非法参数";
            break;
        case ENOMEM:
            ir.errorMsg = "内存不足";
            break;
        default:
            ir.errorMsg = "未知错误";
            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 outBytesLeft = sizeOfOutBuffer;

        // 调用iconv函数进行转换
        size_t result = iconv(cd, &inBufferPtr, &inBytesLeft, &outBufferPtr, &outBytesLeft);

        if (result == (size_t)(-1)) // 转换停止了
        {
            auto n = errno; // 获取错误码

            switch (n) {
            case E2BIG:
                ir.errorMsg = "输出缓冲区太小";
                break;
            case EILSEQ:
                ir.errorNum = n;
                ir.errorMsg = "非法字符序列";
                break;
            case EINVAL:
                ir.errorNum = n;
                ir.errorMsg = "非法参数";
                break;
            default:
                ir.errorMsg = "未知错误";
                break;
            }
        }

        if (ir.errorNum != 0) {
            break;
        }

        ss.write(outBuffer, sizeOfOutBuffer - outBytesLeft);
    }

    iconv_close(cd);

    if (ir.errorNum == 0) {
        ir.result = ss.str();
    }

    return ir;
}

int main(int, char**)
{
    std::system("chcp 65001 > nul");

    extern char const* gbk_str;
    std::cout << "original string: " << gbk_str << std::endl;

    auto ir = iconvConvert(gbk_str, "gbk", "utf-8");

    if (ir.errorNum == 0) {
        std::cout << "converted string: " << ir.result << std::endl;
    } else {
        std::cout << "error: " << ir.errorMsg << std::endl;
        std::cout << "converted string: " << ir.result << std::endl;
    }

    std::system("pause");
}