第八章:字符串与编码——std::string与std::wstring、UTF-8/16/32处理、ICU库集成

字符串处理,是跨平台开发里最容易被忽视的坑。我见过太多项目,明明业务逻辑跑得好好的,一换语言环境就崩——乱码、截断、内存越界,全来了。说白了,字符串不只是字符的容器,它背后是一整套编码体系的博弈。

这一章,我们就来聊聊C++里那几套字符串工具,以及怎么在跨平台场景下优雅地处理编码问题。

8.1 std::string 与 std::wstring:选谁?

很多新手会问:既然有std::string,为什么还要搞个std::wstring?

嗯,这得从字符宽度说起。std::string用的是char,一个字节。对于ASCII字符集,这没问题。但你要处理中文、日文、阿拉伯文,一个字节根本不够。于是就有了wchar_t,宽字符,通常是2字节或4字节,取决于平台。

std::wstring就是基于wchar_t的字符串类。看起来很美,对吧?

但问题来了——wchar_t的长度在不同平台上不一样。Windows上是2字节(UTF-16),Linux/macOS上是4字节(UTF-32)。这就导致了一个尴尬的局面:你用std::wstring写的代码,换个平台就水土不服。

我的建议:除非你确定只在Windows上跑,否则尽量别用std::wstring。我个人习惯是,内部统一用std::string + UTF-8,只在系统API边界做转换。

8.2 UTF-8/16/32:三种编码,一个世界

先理清概念。UTF-8、UTF-16、UTF-32都是Unicode的编码方案,只是存储方式不同。

编码特点典型场景
UTF-8变长,1-4字节,兼容ASCII网络传输、文件存储、跨平台
UTF-16变长,2或4字节,Windows原生Windows API、Java/.NET内部
UTF-32定长,4字节,简单但浪费空间内部处理、字符逐个操作

我在项目中遇到过最典型的坑:从Linux服务器拉下来的文本文件,用Windows记事本打开,中文全变成乱码。原因就是Linux默认UTF-8,Windows记事本默认ANSI(本地编码)。

所以,跨平台项目,统一用UTF-8是铁律。你想想看,UTF-8既兼容ASCII,又不需要操心字节序,还能被几乎所有现代系统原生支持,何乐而不为?

8.3 编码转换实战:自己动手还是用库?

你可能会想:不就是编码转换吗?写个函数查表不就完了?

嗯,如果你只处理GBK和UTF-8,自己写确实可以。但Unicode标准有137,000多个字符,还有组合字符、代理对、规范化形式……自己写?我劝你放弃。

这里我给出两种方案:

方案一:C++17标准库 + 手动转换

C++标准库本身没有直接的编码转换函数。但你可以用std::wstring_convert(C++11引入,C++17废弃,但还能用)。

#include <locale>
#include <codecvt>
#include <string>

// UTF-8 转 UTF-16
std::wstring utf8_to_utf16(const std::string& utf8) {
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return converter.from_bytes(utf8);
}

// UTF-16 转 UTF-8
std::string utf16_to_utf8(const std::wstring& utf16) {
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return converter.to_bytes(utf16);
}

注意:std::codecvt在C++17中已被标记为deprecated。虽然大多数编译器仍然支持,但我不建议在新项目里依赖它。我曾经在一个长期维护的项目里用了这个方案,结果升级编译器后编译报错,折腾了半天才换成ICU。

方案二:ICU库集成(推荐)

ICU(International Components for Unicode)是业界标准。它稳定、全面、跨平台。唯一的缺点就是库体积有点大,但为了编码正确性,这点代价值得。

#include <unicode/ucnv.h>
#include <unicode/unistr.h>
#include <string>

// UTF-8 转 UTF-16 (使用ICU)
std::u16string utf8_to_utf16_icu(const std::string& utf8) {
    icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(
        icu::StringPiece(utf8.c_str(), utf8.size()));
    
    std::u16string result;
    result.resize(ustr.length());
    ustr.extract(0, ustr.length(), reinterpret_cast<UChar*>(&result[0]));
    return result;
}

// UTF-16 转 UTF-8
std::string utf16_to_utf8_icu(const std::u16string& utf16) {
    icu::UnicodeString ustr(
        reinterpret_cast<const UChar*>(utf16.c_str()), utf16.size());
    
    std::string result;
    ustr.toUTF8String(result);
    return result;
}

小技巧:ICU的UnicodeString内部使用UTF-16,但提供了高效的UTF-8转换接口。如果你需要频繁在两种编码间切换,建议把ICU的UnicodeString作为中间格式。

8.4 跨平台字符串处理的架构设计

说了这么多,到底怎么设计一个跨平台的字符串处理层?我画了一张图,你看完就明白了。

跨平台字符串处理架构 应用层(业务逻辑) 字符串抽象接口层(IStringConverter) 编码转换实现层 ICU实现 Win32 API实现 libiconv实现 平台适配层(Windows/Linux/macOS)

这个架构的核心思想是:业务层不直接依赖任何编码实现。你只需要定义一个抽象接口,比如class IStringConverter,里面放几个纯虚函数:ToUTF8()ToUTF16()ToWide()。然后根据不同平台,选择不同的实现。

我在实际项目中就是这么干的。Windows上用WideCharToMultiByte,Linux上用ICU,macOS上用CoreFoundation。上层代码完全不用改,换平台只需要换一个工厂函数。

8.5 避坑指南:我踩过的那些编码坑

最后,分享几个我亲身经历过的教训。

  • 不要用std::string存二进制数据——std::string以'\0'为结束符,二进制数据里很可能包含0x00。用std::vector<uint8_t>更安全。
  • 文件BOM头是个大坑——UTF-8文件带BOM(0xEF 0xBB 0xBF)和不带BOM,解析方式不同。我曾经因为一个BOM头,导致整个配置文件解析失败,排查了整整一天。
  • Windows控制台输出UTF-8——Windows的cmd.exe默认不支持UTF-8输出。你需要调用SetConsoleOutputCP(CP_UTF8),并且设置字体为支持Unicode的字体(比如Consolas)。
  • std::wstring在Linux上的陷阱——Linux的wchar_t是4字节,但很多Linux库(比如Qt)内部用UTF-8。如果你在Linux上用std::wstring,转换起来非常麻烦。

总结一下:跨平台字符串处理,没有银弹。但如果你能做到「内部统一UTF-8,边界做转换」,再配合ICU这样的成熟库,大部分问题都能解决。嗯,编码这件事,看似简单,实则暗流涌动。希望这一章能帮你少走一些弯路。

公众号:蓝海资料掘金营,微信 deep3321