std::string 的内存布局
std::string 通常用 SSO(Small String Optimization):短字符串直接存在对象内部,长字符串才堆分配。
观察分配阈值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <cstdlib> #include <iostream> #include <string>
void* operator new(std::size_t size) { std::cout << "[alloc " << size << "]\n"; return std::malloc(size); }
int main() { std::string s1(16, '#'); s1.append("#");
std::string s2(s1); }
|
典型结论:
- GCC/Clang SSO 阈值约 15 字符,MSVC 约 7。
- C++11 起禁止 COW,拷贝必分配新内存(异常安全)。
简化布局(libc++ 风格):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class SsoString { static constexpr size_t SSO_CAP = 15;
union { struct { char* ptr; size_t size; size_t cap; } heap; struct { char buf[16]; } stack; } data_; bool isHeap_;
public: ~SsoString() { if (isHeap_) delete[] data_.heap.ptr; } };
|
要点:union 复用空间、末字节存长度、阈值由实现决定。
字符串拼接
| 方式 | 评价 |
|---|
operator+ | 产生多次临时对象,避免在循环中使用 |
+= / append() | 简单追加首选 |
std::format (C++20) | 复杂格式化首选 |
std::println (C++23) | 直接输出,无需先拼 string |
stringstream | 性能差,仅复杂转换场景 |
1 2 3 4 5 6
| std::string s = "Hello"; s += " "; s.append("World");
auto msg = std::format("{} v{}", "Linus", 6); std::println("{}", msg);
|
std::string_view (C++17)
string_view = 指针 + 长度,不拥有数据、零拷贝。
1 2 3 4 5 6 7 8 9
| #include <string_view>
std::string str = "Hello World"; std::string_view sv1(str); std::string_view sv2("literal"); std::string_view sv3(str.data(), 5);
using namespace std::string_view_literals; auto sv4 = "Hello"sv;
|
从字面量构造:底层存储在哪?
最常见的写法之一:
1
| std::string_view sv = "hello";
|
这里 "hello" 是字符串字面量(string literal),它不是运行时在栈或堆上分配的临时对象。根据 C++ 标准,字符串字面量具有静态存储期(static storage duration):
- 编译时写入可执行文件的
.rodata 段(只读数据段),程序运行期间始终存在。 - 生存期从程序启动到程序结束,不会"过期"。
- 多次引用同一个字面量,编译器可能合并为同一份存储(合并字符串常量池)。
所以 sv 指向的数据是安全的——它指向一块程序生命周期内都有效的只读内存:
1 2 3 4 5 6
| std::string_view sv = "hello";
auto make_greeting() { return std::string_view{"hello"}; }
|
两种构造方式的底层存储对比:
| 构造来源 | 底层存储位置 | 存储期 | string_view 是否安全 |
|---|
字面量 "text" | .rodata(只读数据段) | 静态 | ✅ 安全 |
std::string | 堆 | 自动 / 动态 | ⚠️ 取决于 string 生存期 |
char buf[N] | 栈 | 自动 | ⚠️ 取决于 buf 生存期 |
new char[] | 堆 | 动态 | ⚠️ 需手动管理 |
经验规则:从字面量构造的 string_view 可以安全返回和长期保存;从 std::string、栈数组等构造的 string_view 只适合短期使用,不要跨作用域保存。
正确用法
- 函数只读参数:
void parse(std::string_view data) - 子串/切片:
sv.substr(0, 5) 不分配 - 容器 key(字面量或池化后)
1 2 3 4 5
| void log(std::string_view msg);
log(str); log("literal"); log(str.substr(0, 4));
|
成员变量保存拥有者 std::string,访问器返回 std::string_view,这是常见且安全的设计:
1 2 3 4 5 6 7 8 9 10 11
| class User { public: explicit User(std::string name) : name_(std::move(name)) {}
[[nodiscard]] std::string_view name() const { return name_; }
private: std::string name_; };
|
string_view 陷阱(重点)
1. 生命周期悬空(Dangling)
string_view 只是视图,指向的数据必须比它活得更长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| std::string_view bad() { std::string tmp = "local"; return tmp; }
std::string_view bad2() { char buf[] = "local"; return buf; }
std::string_view ok(const std::string& s) { return s; }
|
常见触发场景:
- 从
std::string 的临时对象构造 string_view - 把
string_view 存到容器后,源数据被释放 - 函数返回
string_view 指向局部变量
2. 非空终止
string_view 不保证 以 \0 结尾,直接传给需要 C 字符串的 API 会越界。
1 2 3 4 5 6 7 8
| std::string_view sv = "Hello"sv.substr(0, 3);
std::printf("%s", sv.data());
std::string s(sv); std::printf("%s", s.c_str());
|
3. 误当作拥有型容器
string_view 不管理内存,不能用来“保存”字符串。
1 2 3 4 5 6 7
| std::vector<std::string_view> cache; std::string input = get_input(); cache.push_back(input);
std::vector<std::string> cache;
|
4. 隐式转换陷阱
std::string 可隐式转 string_view,但反向不行。混用时容易意外构造临时对象。
1 2 3 4 5 6 7
| void foo(std::string_view sv);
std::string s = "abc"; foo(s);
std::string_view sv = s; std::string s2 = sv;
|
注意:重载 string_view 和 const string& 时,字面量调用可能产生歧义。
1 2 3 4 5 6
| void print(std::string_view); void print(const std::string&);
print("hello"); print("hello"sv); print("hello"s);
|
5. 修改操作不直观
remove_prefix / remove_suffix 只移动指针,不释放内存,也不改变原字符串。
1 2 3
| std::string_view sv = "Hello World"; sv.remove_prefix(6);
|
6. 哈希与相等比较
std::hash<std::string_view> 按内容哈希,可与 string 混用查找。但 不要 用指向不同生命的 string_view 做长期 key。
1 2 3 4 5
| std::unordered_map<std::string_view, int> m; m["literal"] = 1;
std::string k = "dynamic"; m[k] = 2;
|
string_view 在容器中
核心问题仍是 生命周期。
vector<string_view>
1 2 3 4 5
| std::vector<std::string> pool = {"a", "b", "c"}; std::vector<std::string_view> views; views.reserve(pool.size()); for (const auto& s : pool) views.emplace_back(s);
|
关联容器 key
用池保证 key 存活:
1 2 3 4 5 6 7 8 9 10
| class StringKeyedMap { std::vector<std::string> keys_; std::unordered_map<std::string_view, int> map_;
public: void insert(std::string_view k, int v) { keys_.emplace_back(k); map_[keys_.back()] = v; } };
|
配置解析示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class ConfigParser { std::vector<std::string> key_pool_, val_pool_; std::unordered_map<std::string_view, std::string_view> config_;
public: void parseLine(std::string_view line) { auto pos = line.find('='); if (pos == std::string_view::npos) return;
key_pool_.emplace_back(line.substr(0, pos)); val_pool_.emplace_back(line.substr(pos + 1)); config_[key_pool_.back()] = val_pool_.back(); }
std::string_view get(std::string_view k) const { auto it = config_.find(k); return it != config_.end() ? it->second : ""; } };
|
选择 string 还是 string_view
| 场景 | 推荐 |
|---|
| 只读函数参数 | std::string_view |
| 子串/切片 | std::string_view |
| 容器 key(字面量/池) | std::string_view |
| 需要拥有/修改 | std::string |
| 生命周期不确定 | std::string |
| 传给 C API | std::string::c_str() |
底线:string_view 是借用,不拥有数据。不确定生命周期时,用 std::string 更稳。
性能:测了再说,别猜。