C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++中文字符处理

C++如何正确使用std::string处理中文字符并避免乱码

作者:fpcc

对于开发者来说,哪种语言对中文的支持都不是特别友好,本文将从不同的层面上对C++如何进行中文字符串的处理进行分析说明,感兴趣的小伙伴可以了解下

一、说明

对于开发者来说,哪种语言对中文的支持都不是特别友好,毕竟人家都是英文起家。但在C++中,表现的更差。在学习计算机开始时,可能都接触过各种的编码标准,但那些标准如何落到语言层面上,则又是一个问题。从早期的宽字符串到普通字符串的来回转换,就已经让很多开发者头疼不已。

而c++标准库对Unicode的完整支持比较有限。处理中文时,不管是源文件处理还是程序内部,尽量使用统一的UTF-8编码。尽量只使用标准库中提供的std::string。只有在特殊的场景下才使用其它形式的编码形式,这样基本就可以保证中文处理的安全性。

也就是说,编程开发时,对中文处理分成三个层面:

  1. 源文件编码:编写cpp源文件时文件本身采用的编码
  2. 执行字符编码:编译器将普通字符串字面量转换成的编码
  3. 运行时数据编码:文件、终端、网络或数据库中的实际编码

这三种不一致是中文处理复杂的主要原因。常见的比如在Windows上的源文件拷贝到Linux下编译就报有各种问题,其实就是编码问题的原因。另外一个就是开发的机器上有一种中文字库而实际运行的机器上没有,就会出现中文的显示异常问题。

下面就从不同的层面上对C++如何进行中文字符串的处理进行分析说明。

二、编码层面

C++17中更好的提供了对UTF-8的支持。C++17中,u8字符串字面量的元素类型是char,可以用std::string保存:

#include <iostream>
#include <string>

int main() {
  std::string str = u8"朋友你好!";

  std::cout << str << std::endl;
  std::cout << "字节数:" << str.size() << std::endl;

  return 0;
}

需要指出的是std::string本身并不知道内容采用哪种编码,它只是保存字节。只要程序约定其中保存UTF-8,便可以安全地用于文件、网络和大部分跨平台接口。

字节数和字符数

很多开发者认为,字符数和字节数是一样的,这在英文场景下或许是对的。但对于Unicode类型则不会如此。比如UTF-8是一种变长编码。一般来说ASCII字符通常占1字节而常用汉字通常占多个字节。

可以使用前面的例子进行说明:

std::string str = u8"朋友";
std::cout << str << std::endl;
std::cout << "字节数:" << str.size() << std::endl;

通常输出6他字节,它表明这两人个汉字占用了六个字节的空间。

不要直接按字节截取汉字

下面的代码只取得“朋”的第一个UTF-8字节:

std::string str = u8"朋友";
char firstByte = str[0];

下面的操作会产生不完整的UTF-8序列:

std::string broken = str.substr(0, 1);

即使使用substr(0, 3)暂时能够截取某个常用汉字,也不能作为通用方案,因为字符串中可能同时包含ASCII、四字节字符、组合字符和Emoji(表情符号)。

c++20的演进

不过随着标准的演进,到了c++20又有了变化,C++20中提供了char8_t。即从C++20开始,u8"中文"的元素类型变为char8_t,对应容器为std::u8string:

#include <iostream>
#include <string>

int main() {
  std::u8string str = u8"朋友";

  std::cout.write(
      reinterpret_cast<const char *>(str.data()),
      static_cast<std::streamsize>(str.size()));
  std::cout << std::endl;

  return 0;
}

C++20中不能直接写成:

std::string str = u8"朋友"; // 编译错误

需要与接收UTF-8字节的旧接口配合时,可以集中提供一个转换函数:

#include <string>
#include <string_view>

std::string toUtf8Bytes(std::u8string_view str) {
  return {
      reinterpret_cast<const char *>(str.data()),
      str.size()
  };
}

这里转换的是相同UTF-8数据的字节表示,不是字符集转码。

三、编译层面

设置编译器编码可以更好的处理中文字符的问题。源文件应使用UTF-8保存,并让编译器按UTF-8读取和生成字符串字面量。

GCC和Clang

g++ -std=c++17 \
  -finput-charset=UTF-8 \
  -fexec-charset=UTF-8 \
  main.cpp

其中:

MSVC

MSVC建议使用:

/utf-8

它相当于同时指定源文件字符集和执行字符集为UTF-8。

CMake配置

if(MSVC)
    target_compile_options(yourTarget PRIVATE /utf-8)
else()
    target_compile_options(yourTarget PRIVATE
        -finput-charset=UTF-8
        -fexec-charset=UTF-8
    )
endif()

如果第三方源文件仍然使用GBK或其他本地编码,最好先把源文件转换为UTF-8,而不是让同一工程长期混用多种源文件编码。

四、不同字符串类型的含义

std::string:std::string保存字节,不自带编码信息。推荐约定其内容始终为UTF-8。

std::u8stringstd::u8string从C++20开始使用char8_t表达UTF-8编码单元,类型含义比std::string更明确,但与大量传统C接口和std::ostream配合不够方便。

std::u16string:std::u16string使用16位编码单元,通常用于保存UTF-16。一个Unicode码点可能需要两个char16_t代理项。

std::u32string:std::u32string使用32位编码单元,通常一个编码单元对应一个Unicode码点,但一个码点仍不一定等于一个用户可见字符。

std::wstring:std::wstring依赖平台:

因此不能把std::wstring的原始内存直接写入跨平台文件、网络协议或数据库字段。

五、具体的场景

中文文件读写

UTF-8文本文件可以直接用std::string读写:

#include <fstream>
#include <iostream>
#include <iterator>
#include <string>

int main() {
  const std::string outputstr = u8"第一行中文\n第二行中文\n";

  {
    std::ofstream output("chinese.txt", std::ios::binary);
    if (!output) {
      std::cerr << "无法打开输出文件" << std::endl;
      return 1;
    }

    output.write(
        outputstr.data(),
        static_cast<std::streamsize>(outputstr.size()));
  }

  {
    std::ifstream input("chinese.txt", std::ios::binary);
    if (!input) {
      std::cerr << "无法打开输入文件" << std::endl;
      return 1;
    }

    std::string content(
        std::istreambuf_iterator<char>(input),
        std::istreambuf_iterator<char>());

    std::cout << content;
  }

  return 0;
}

二进制模式可以避免Windows对换行符进行自动转换,文件内容仍然是UTF-8文本。是否写入UTF-8 BOM应由外部文件格式决定,大多数现代程序和协议不需要BOM。

读取外部文本时还需要确认:

中文文件路径

Linux文件路径本质上是字节序列,通常约定使用UTF-8。Windows文件API原生使用UTF-16。

在Qt中可以直接使用QString路径:

QString filePath = QStringLiteral("/tmp/中文文件名.txt");
QFile file(filePath);

Windows标准C++程序可以在转换为UTF-16后构造路径:

std::filesystem::path path(utf8ToWide(utf8Path));

不要在Windows中把未知本地代码页的std::string直接当成Unicode文件路径。

Linux终端显示和输入中文

Linux终端通常使用UTF-8,可以检查当前环境:

locale

常见配置包括:

LANG=zh_CN.UTF-8

或者:

LANG=en_US.UTF-8

临时设置UTF-8环境:

export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8

终端、字体和程序输入输出均支持UTF-8时,可以直接使用:

std::string input;
std::getline(std::cin, input);
std::cout << input << std::endl;

std::getline()读入的仍然是字节序列,不会验证数据是否为合法UTF-8。

Windows控制台显示中文

使用MSVC时先启用/utf-8。C++17程序可以把Windows控制台输入输出代码页设置为UTF-8:

#ifdef _WIN32
#include <windows.h>
#endif

#include <iostream>

int main() {
#ifdef _WIN32
  SetConsoleOutputCP(CP_UTF8);
  SetConsoleCP(CP_UTF8);
#endif

  std::cout << u8"你好,Friends!" << std::endl;
  return 0;
}

现代Windows Terminal对UTF-8支持较好,旧版控制台还可能受到字体和C运行库行为影响。

调用Windows系统API时优先使用以W结尾的Unicode版本:

#include <windows.h>

int main() {
  MessageBoxW(
      nullptr,
      L"中文信息",
      L"题⽬",
      MB_OK);

  return 0;
}

Windows UTF-8和UTF-16转换

std::wstring_convert和<codecvt>在C++17中已经被弃用,不适合新代码。Windows上应使用系统API进行严格转换:

#ifdef _WIN32

#include <stdexcept>
#include <string>
#include <string_view>
#include <windows.h>

std::wstring utf8ToWide(std::string_view str) {
  if (str.empty()) {
    return {};
  }

  const int length = MultiByteToWideChar(
      CP_UTF8,
      MB_ERR_INVALID_CHARS,
      str.data(),
      static_cast<int>(str.size()),
      nullptr,
      0);

  if (length <= 0) {
    throw std::runtime_error("invalid UTF-8");
  }

  std::wstring result(static_cast<std::size_t>(length), L'\0');

  const int converted = MultiByteToWideChar(
      CP_UTF8,
      MB_ERR_INVALID_CHARS,
      str.data(),
      static_cast<int>(str.size()),
      result.data(),
      length);

  if (converted != length) {
    throw std::runtime_error("UTF-8 conversion failed");
  }

  return result;
}

std::string wideToUtf8(std::wstring_view str) {
  if (str.empty()) {
    return {};
  }

  const int length = WideCharToMultiByte(
      CP_UTF8,
      WC_ERR_INVALID_CHARS,
      str.data(),
      static_cast<int>(str.size()),
      nullptr,
      0,
      nullptr,
      nullptr);

  if (length <= 0) {
    throw std::runtime_error("invalid UTF-16");
  }

  std::string result(static_cast<std::size_t>(length), '\0');

  const int converted = WideCharToMultiByte(
      CP_UTF8,
      WC_ERR_INVALID_CHARS,
      str.data(),
      static_cast<int>(str.size()),
      result.data(),
      length,
      nullptr,
      nullptr);

  if (converted != length) {
    throw std::runtime_error("UTF-16 conversion failed");
  }

  return result;
}

#endif

生产代码还应在转换前检查输入长度是否超过INT_MAX,避免从std::size_t转换为int时发生溢出。

Qt处理中文

一般来说,在Qt中推荐统一使用QString,这也算是平台的一种基础支持:

#include <QDebug>
#include <QString>

int main() {
  QString text = QStringLiteral("朋友你好!");

  qDebug().noquote() << text;
  qDebug() << text.size();

  return 0;
}

不过需要注意的是,在Qt中有着丰富的转换机制,这需要在处理中文时根据实际情况进行处理,如QString和UTF-8转换、QString和std::string转换、QByteArray的处理,以及fromUtf8和fromLocal8Bit的选择等。

六、常见问题和解决

中文乱码通常来自以下问题:

排查乱码时应找到数据第一次发生变化的位置,并记录每个边界的实际字节。例如UTF-8编码的“朋”对应十六进制字节通常为:

E6 9C 8B

如果内存和文件中仍然是这些字节,问题通常发生在解析或显示阶段;如果字节已经改变,则需要继续向数据来源方向排查错误转码的位置。也就是说分段检查,逐次逼近。

七、实践选择

对于开发者来说,实际的c++开发中,一般推荐如下处理:

  1. 源代码统一保存为UTF-8
  2. GCC和Clang显式配置输入及执行字符集,MSVC启用/utf-8
  3. 标准C++项目将std::string明确约定为UTF-8
  4. Qt项目内部使用QString,边界处显式调用fromUtf8()和toUtf8()
  5. 不使用字节下标、size()或普通substr()表示汉字数量和边界
  6. Windows Unicode API使用UTF-16宽字符版本
  7. 不使用已弃用的std::wstring_convert编写新代码
  8. 文件、网络、JSON、数据库和日志等统一使用UTF-8编码格式
  9. 对外部输入验证编码合法性,并明确非法序列处理策略
  10. 复杂Unicode处理使用工具框架(Qt或ICU),尽量不自行实现不完整的字符算法

八、总结

对于字符的处理,看上去不是什么问题。但在实践中经常让开发者头疼。特别是在不同的团队合作时,这种问题更容易出现。所以统一开发和运行标准,是合作开发的前提。不要总想着最后适配,那会带来无穷的“灾难”。

以上就是C++如何正确使用std::string处理中文字符并避免乱码的详细内容,更多关于C++中文字符处理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文