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, '#'); // 长度 16,超出 SSO
s1.append("#"); // 扩容

std::string s2(s1); // 新分配,无 COW
}

典型结论:

  • 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:
// 短:memcpy 到 stack.buf;长:new[] 分配
// size() 按 isHeap_ 读取 stack.buf[SSO_CAP] 或 heap.size
~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); // C++20
std::println("{}", msg); // C++23

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); // "Hello"

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";   // sv.data() -> .rodata 中的 "hello\0"

// 即使 sv 离开作用域再回来,这块内存仍然有效
auto make_greeting() {
return std::string_view{"hello"}; // OK: 指向静态存储
}

两种构造方式的底层存储对比:

构造来源底层存储位置存储期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);        // 按值传, cheap

log(str);
log("literal"); // 无临时 string
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_; // owns the data
};

string_view 陷阱(重点)

1. 生命周期悬空(Dangling)

string_view 只是视图,指向的数据必须比它活得更长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误:临时 string 销毁后 view 悬空
std::string_view bad() {
std::string tmp = "local";
return tmp; // 编译器可能警告,但仍是 UB
}

// 错误:从 char[] 局部数组构造
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);  // "Hel",无 '\0'

// 错误
std::printf("%s", sv.data()); // 越界读到 "Hello"

// 正确:先拷贝到 string
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); // input 离开作用域后悬空

// 正确:需要保存时用 string
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); // 隐式转 view,零拷贝

std::string_view sv = s;
std::string s2 = sv; // 显式拷贝,清楚

注意:重载 string_viewconst 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); // 明确 string

5. 修改操作不直观

remove_prefix / remove_suffix 只移动指针,不释放内存,也不改变原字符串。

1
2
3
std::string_view sv = "Hello World";
sv.remove_prefix(6); // sv 现在是 "World"
// 原数据仍在,只是视图窗口变了

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; // k 必须保持存活

string_view 在容器中

核心问题仍是 生命周期

vector<string_view>

1
2
3
4
5
// 安全:views 指向 pool,pool 比 views 活得长
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 APIstd::string::c_str()

底线:string_view 是借用,不拥有数据。不确定生命周期时,用 std::string 更稳。

性能:测了再说,别猜。