字符串与原始字符串字面量
定义与作用
C++ 支持两种字符串操作方式:
| 方式 | 来源 | 特点 |
|---|---|---|
C 风格 char* / char[] | C 语言 | 手动内存管理,易缓冲区溢出 |
std::string | C++ 标准库 | 自动内存管理,安全便捷 |
C++11 新增原始字符串字面量 R"(...)" 解决了转义字符地狱问题——在写正则表达式、Windows 路径、多行文本时不再需要层层转义。
核心原理
字符串存储方式对比
原始字符串语法
R"delimiter( 内容 )delimiter"
| 组件 | 说明 |
|---|---|
R"( | 开始标记 |
delimiter | 可选的自定义定界符(最多 16 字符),解决内容含 )" 的问题 |
内容 | 字符串主体,任何字符都不转义 |
)delimiter" | 结束标记 |
完整示例
示例一:转义地狱与原始字符串的救赎
场景说明:在配置解析和正则匹配中,传统字符串需要大量转义,原始字符串让代码可读性大幅提升。
#include <iostream>
#include <string>
#include <regex>
int main() {
// ---- 场景 1: Windows 文件路径 ----
std::string escaped_path = "C:\\Users\\张三\\Documents\\论文.docx";
std::string raw_path = R"(C:\Users\张三\Documents\论文.docx)";
std::cout << "===== 文件路径 =====" << std::endl;
std::cout << "转义写法: " << escaped_path << std::endl;
std::cout << "原始字符串: " << raw_path << std::endl;
// ---- 场景 2: 正则表达式 ----
// 匹配 3 位数字-4 位数字(如 010-1234)
std::string escaped_regex = "\\d{3}-\\d{4}";
std::string raw_regex = R"(\d{3}-\d{4})";
std::cout << "\n===== 正则表达式 =====" << std::endl;
std::cout << "转义写法: " << escaped_regex << std::endl;
std::cout << "原始字符串: " << raw_regex << std::endl;
std::regex pattern(raw_regex);
std::string test1 = "电话: 010-5678";
std::string test2 = "电话: 010-567";
std::cout << "\"" << test1 << "\" → "
<< std::boolalpha << std::regex_search(test1, pattern) << std::endl;
std::cout << "\"" << test2 << "\" → "
<< std::regex_search(test2, pattern) << std::endl;
// ---- 场景 3: JSON / HTML 内嵌 ----
std::string json = R"({
"name": "张三",
"age": 20,
"courses": ["C++", "数据结构"]
})";
std::cout << "\n===== 内嵌 JSON =====" << std::endl;
std::cout << json << std::endl;
// ---- 场景 4: 多行 SQL 查询 ----
std::string sql = R"SQL(
SELECT id, name, score
FROM students
WHERE score >= 60
AND enrollment_year = 2024
ORDER BY score DESC
)SQL";
std::cout << "===== SQL 查询 =====" << std::endl;
std::cout << sql << std::endl;
// ---- 场景 5: 自定义定界符 ----
// 内容本身包含 )" 时需要自定义定界符
std::string tricky = R"END(这里的字符串包含)"这个序列,但不会提前终止)END";
std::cout << "===== 自定义定界符 =====" << std::endl;
std::cout << tricky << std::endl;
return 0;
}
预期输出:
===== 文件路径 =====
转义写法: C:\Users\张三\Documents\论文.docx
原始字符串: C:\Users\张三\Documents\论文.docx
===== 正则表达式 =====
转义写法: \d{3}-\d{4}
原始字符串: \d{3}-\d{4}
"电话: 010-5678" → true
"电话: 010-567" → false
===== 内嵌 JSON =====
{
"name": "张三",
"age": 20,
"courses": ["C++", "数据结构"]
}
===== SQL 查询 =====
SELECT id, name, score
FROM students
WHERE score >= 60
AND enrollment_year = 2024
ORDER BY score DESC
===== 自定义定界符 =====
这里的字符串包含)"这个序列,但不会提前终止
逐段分析:
- 传统写法
"\\d{3}-\\d{4}"每个\都需要双重转义(C++ 字符串一次,正则引擎一次) R"(\d{3}-\d{4})"中所有字符原样保留,正则表达式和代码中的写法完全一致- JSON 内嵌时,双引号、换行、缩进都不需要转义
- 自定义定界符
R"END(...)END"应对"内容本身就包含)""的极端场景 - 原始字符串内的空白(包括前导空格)会被保留
示例二:字符串操作与短字符串优化
场景说明:演示 std::string 的常用操作及 SSO(短字符串优化)对性能的影响。
#include <iostream>
#include <string>
#include <cstring>
int main() {
// ---- 常用操作 ----
std::string s1 = "Hello";
std::string s2 = "World";
// 拼接
std::string s3 = s1 + ", " + s2 + "!";
std::cout << "拼接: " << s3 << std::endl;
// 查找
size_t pos = s3.find("World");
if (pos != std::string::npos) {
std::cout << "\"World\" 在位置 " << pos << std::endl;
}
// 子串
std::string sub = s3.substr(7, 5); // 从位置 7 取 5 个字符
std::cout << "子串: " << sub << std::endl;
// 替换
std::string s4 = "C++ is hard";
s4.replace(7, 4, "fun");
std::cout << "替换: " << s4 << std::endl;
// ---- C 风格字符串处理 ----
char buf[50];
// strcpy(buf, s3.c_str()); // 不安全:buf 可能不够大
strncpy(buf, s3.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
std::cout << "\nC 风格拷贝: " << buf << std::endl;
// ---- 短字符串优化 (SSO) 观察 ----
std::string short_str = "hi";
std::string long_str = "this is a very long string that exceeds the SSO buffer";
std::cout << "\n===== 字符串内存布局 =====" << std::endl;
std::cout << "短字符串 '" << short_str << "':" << std::endl;
std::cout << " sizeof: " << sizeof(short_str) << " 字节" << std::endl;
std::cout << " data() == 栈? " << std::boolalpha
<< (short_str.data() == reinterpret_cast<const char*>(&short_str))
<< " (可能由实现决定)" << std::endl;
std::cout << "长字符串: sizeof " << sizeof(long_str)
<< " 字节" << std::endl;
// 长字符串的 data() 肯定不在栈上
// ---- 字符类型 ----
std::cout << "\n===== 字符类型 =====" << std::endl;
const wchar_t* wide = L"宽字符";
const char16_t* u16 = u"UTF-16 字符串";
const char32_t* u32 = U"UTF-32 字符串";
std::wcout << L"宽字符输出: " << wide << std::endl;
std::cout << "char16_t 字符串长度: " << std::char_traits<char16_t>::length(u16) << std::endl;
std::cout << "char32_t 字符串长度: " << std::char_traits<char32_t>::length(u32) << std::endl;
return 0;
}
预期输出:
拼接: Hello, World!
"World" 在位置 7
子串: World
替换: C++ is fun
C 风格拷贝: Hello, World!
===== 字符串内存布局 =====
短字符串 'hi':
sizeof: 32 字节
data() == 栈? false (可能由实现决定)
长字符串: sizeof 32 字节
===== 字符类型 =====
宽字符输出: 宽字符
char16_t 字符串长度: 8
char32_t 字符串长度: 6
逐段分析:
std::string的sizeof在 64 位 libstdc++ 上通常为 32 字节,不随字符串长度变化——因为它内部存储指针 + size + capacity- SSO(Short String Optimization)让短字符串直接存储在
std::string对象内部,避免堆分配 - C 风格字符串操作(
strncpy/strcat等)需要手动管理缓冲区大小,容易出错——优先用std::string - C++11 新增
char16_t/char32_t+u/U前缀,支持 UTF-16/UTF-32 字符串字面量
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
用 strcpy 拷贝不可信来源的字符串 | 缓冲区溢出 | 用 std::string 或 strncpy |
std::string::data() 返回的指针当 C 字符串 | 可能不以 \0 结尾(C++11 前) | 用 .c_str() |
原始字符串中嵌套 )" | 提前终止 | 使用自定义定界符 R"DELIM(...)DELIM" |
| 原始字符串前导空白 | 被意外保留 | 注意原始字符串会保留所有字符,包括缩进 |
char 有无符号不确定 | 位运算结果不一致 | 明确用 signed char / unsigned char |
常见面试问题
std::string的 SSO 是什么?——短字符串优化,短字符串直接存储在对象内部(栈上)而非堆上,避免动态内存分配。SSO 阈值通常为 15 字符左右(取决于实现)。c_str()和data()的区别?——C++11 起两者等价,都返回以\0结尾的字符串。C++11 之前data()不保证\0结尾。原始字符串字面量解决了什么问题?——避免正则表达式、文件路径、JSON、HTML 中的大量反斜杠转义。核心原理是"所见即所得"。
如何安全地将
std::string传给 C 函数?——使用.c_str()获取const char*。如果 C 函数需要修改字符串,先拷贝到std::vector<char>再传&vec[0]。char、wchar_t、char16_t、char32_t的区别?——char8 位(ASCII/UTF-8)、wchar_t平台相关(Windows 16 位/Linux 32 位)、char16_t16 位(UTF-16)、char32_t32 位(UTF-32)。跨平台项目应明确选择。
小结
- C 风格字符串易出错,日常开发统一使用
std::string - 原始字符串
R"(...)"解决转义地狱,配合自定义定界符应对极端场景 - SSO 让短字符串零堆分配,是
std::string的重要性能优化 - 涉及 C API 时用
.c_str()获取const char*,不要直接修改 - C++11 新增
char16_t/char32_t提供 UTF-16/UTF-32 的标准化支持