在C++中处理字符串时,我们经常遇到一个基础但容易被忽视的问题:字符编码。最初的ASCII码表只能表示128个字符(扩展版256个),这对于英文和基本符号已经足够,但面对全球各种语言就显得力不从心。
ASCII的局限性主要体现在:
Unicode的出现解决了这个根本问题。它采用统一的编码空间,为全球所有字符分配唯一码点(Code Point)。比如汉字"中"的Unicode码点是U+4E2D。但要注意的是,Unicode只是字符到数字的映射标准,实际存储还需要具体的编码方案。
| 编码方案 | 字节长度 | 兼容性 | 适用场景 |
|---|---|---|---|
| UTF-8 | 1-4字节 | 完美兼容ASCII | 网络传输、文件存储 |
| UTF-16 | 2/4字节 | 不兼容ASCII | Windows系统内部 |
| UTF-32 | 固定4字节 | 直接对应码点 | 需要固定宽度字符处理 |
在C++中处理中文字符时,一个常见误区是误判字符串长度:
cpp复制std::string s("中国");
std::cout << s.size(); // 输出4而非2
这是因为UTF-8编码下,一个中文字符通常占用3个字节。理解这点对正确处理字符串截取、索引等操作至关重要。
乱码的本质是编码解码规则不匹配。就像用英语发音规则读中文肯定会出错。在实际开发中,乱码通常由以下原因导致:
典型场景示例:
cpp复制// 文件编码为UTF-8,但控制台使用GBK编码
std::ofstream file("test.txt");
file << "你好世界"; // 写入UTF-8
file.close();
// 用GBK编码读取时会出现乱码
解决方案:
实际经验:在跨平台项目中,建议在CMake中显式设置编译器编码选项:
cmake复制add_compile_options(/utf-8) # MSVC set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finput-charset=UTF-8 -fexec-charset=UTF-8") # GCC/Clang
传统C++字符串类的拷贝构造通常采用深拷贝策略,确保每个对象拥有独立的内存空间。这种实现虽然安全,但代码较为冗长:
cpp复制class String {
public:
String(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 传统深拷贝构造函数
String(const String& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
// 传统赋值运算符
String& operator=(const String& s) {
if (this != &s) {
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
return *this;
}
~String() { delete[] _str; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
这种实现存在几个潜在问题:
现代C++提倡使用"copy-and-swap"惯用法来简化实现:
cpp复制class String {
public:
// 构造函数保持不变
String(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 交换辅助函数
void swap(String& other) noexcept {
std::swap(_str, other._str);
std::swap(_size, other._size);
std::swap(_capacity, other._capacity);
}
// 现代拷贝构造函数
String(const String& s) : String(s._str) {}
// 现代赋值运算符
String& operator=(String s) noexcept {
swap(s);
return *this;
}
~String() { delete[] _str; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
这种实现的优势在于:
性能提示:虽然现代实现看起来多了一次拷贝,但编译器优化(RVO/NRVO)通常会消除这个额外开销。
写时拷贝(Copy-On-Write)是一种延迟拷贝的技术,在资源被修改前共享同一份数据:
cpp复制class CowString {
public:
CowString(const char* str = "") :
_data(new SharedData(str)) {}
// 浅拷贝
CowString(const CowString& other) :
_data(other._data) {
_data->refCount++;
}
// 写时拷贝
char& operator[](size_t pos) {
if (_data->refCount > 1) {
SharedData* newData = new SharedData(_data->str);
_data->refCount--;
_data = newData;
}
return _data->str[pos];
}
private:
struct SharedData {
char* str;
int refCount;
SharedData(const char* s) :
str(new char[strlen(s)+1]), refCount(1) {
strcpy(str, s);
}
~SharedData() { delete[] str; }
};
SharedData* _data;
};
COW技术的优缺点:
处理UTF-8字符串时需要特别注意:
推荐做法:
cpp复制#include <unicode/unistr.h> // ICU库
void processUTF8(const std::string& utf8str) {
icu::UnicodeString ustr = icu::UnicodeString::fromUTF8(utf8str);
int32_t length = ustr.countChar32(); // 正确获取字符数
// 安全遍历
for(int32_t i = 0; i < length; ++i) {
UChar32 c = ustr.char32At(i);
// 处理单个字符...
}
}
cpp复制// 不好的做法:多次重分配
std::string result;
for (const auto& item : items) {
result += item; // 可能多次重分配
}
// 优化做法:预先保留足够空间
std::string result;
result.reserve(totalLength); // 一次性分配
for (const auto& item : items) {
result += item; // 无重分配
}
// 最佳实践:使用ostringstream
std::ostringstream oss;
for (const auto& item : items) {
oss << item; // 自动管理缓冲区
}
std::string result = oss.str();
不同平台对字符串处理的差异:
解决方案:
cpp复制// 跨平台宽字符转换
std::string wideToUTF8(const std::wstring& wstr) {
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
return conv.to_bytes(wstr);
}
std::wstring utf8ToWide(const std::string& str) {
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
return conv.from_bytes(str);
}
注意:C++17弃用了codecvt,推荐使用第三方库如ICU或Boost.Locale
cpp复制String s1("hello");
String s2 = s1; // 如果没有正确实现拷贝构造...
// 析构时会导致同一内存被释放两次
cpp复制class String {
char* _str; // 未初始化为nullptr
// 如果构造函数失败,析构时delete[]未定义行为
};
cpp复制String s("short");
strcpy(s._str, "a very long string..."); // 缓冲区溢出
解决方案:
处理不同编码的文件读取:
cpp复制#include <fstream>
#include <string>
#include <codecvt>
std::string readFile(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) throw std::runtime_error("无法打开文件");
// 读取BOM头判断编码
char bom[3] = {0};
file.read(bom, 3);
// 回退到文件开始
file.seekg(0);
if (bom[0] == '\xEF' && bom[1] == '\xBB' && bom[2] == '\xBF') {
// UTF-8 with BOM
file.seekg(3); // 跳过BOM
return std::string(std::istreambuf_iterator<char>(file), {});
}
else {
// 假设是本地编码(如GBK)
std::string content(std::istreambuf_iterator<char>(file), {});
// 转换为UTF-8
std::wstring_convert<std::codecvt_byname<wchar_t, char, mbstate_t>>
conv(new std::codecvt_byname<wchar_t, char, mbstate_t>("zh_CN.gbk"));
std::wstring wstr = conv.from_bytes(content);
std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8conv;
return utf8conv.to_bytes(wstr);
}
}
cpp复制// C++17 string_view用法
void processString(std::string_view sv) {
// 可以接受std::string、char数组等而不产生拷贝
if (sv.starts_with("prefix")) {
// ...
}
}
// UTF-8字面量处理
auto utf8str = u8"中文"; // C++11
auto utf8sv = u8"中文"sv; // C++17
// 用户定义字面量
constexpr auto operator""_s(const char* str, size_t len) {
return std::string(str, len);
}
auto s = "hello"_s; // 直接构造std::string
在实际项目中,字符串处理看似简单实则暗藏许多陷阱。理解字符编码原理、掌握现代C++的字符串管理技术、遵循RAII原则,才能写出健壮可靠的字符串处理代码。特别是在处理多语言、多平台环境时,提前规划好编码策略可以避免后期的许多麻烦。