C++ 字符串
C++ std::string 的内存分配与 SSO
std::string 是 C++ STL 最常用容器。先搞清它在哪里存数据。
重载全局 new/delete 观察:
#include <string>
#include <iostream>
#include <cstdlib>
using namespace std;
void* operator new(std::size_t size) {
cout << "[Allocating " << size << " bytes]\n";
return malloc(size);
}
int main() {
for (int i = 1; i <= 17; ++i) {
cout << i << ":" << string(i, '*') << "\n";
}
string(16, '*').append("*").append("**");
cout << "---------------------------\n";
string s1(16, '#');
s1.append("#").append("##");
string s2(s1);
return 0;
}运行输出:
1:*
2:**
3:***
4:****
5:*****
6:******
7:*******
8:********
9:*********
10:**********
11:***********
12:************
13:*************
14:**************
15:***************
16:[Allocating 17 bytes]
****************
17:[Allocating 18 bytes]
*****************
[Allocating 17 bytes]
[Allocating 33 bytes]
---------------------------
[Allocating 17 bytes]
[Allocating 33 bytes]
[Allocating 20 bytes] // 无 COW发现:
- size < 16:SSO,栈上,无 heap 分配。
- s2(s1):新分配,无 COW。C++11 禁 COW(异常安全,Never break userspace)。
SSO 简化实现
SSO 的核心是:短字符串存在对象内部,长字符串才堆分配。典型布局(libc++):
class SsoString {
static constexpr size_t SSO_CAP = 15; // 预留1字节给长度/标记
union Data {
struct Heap {
char* ptr;
size_t size;
size_t cap;
} heap;
struct Stack {
char buf[16]; // 16字节,末字节复用存储长度
} stack;
} data_;
bool isHeap_; // 标记当前使用哪种存储
public:
SsoString(const char* s) : isHeap_(false) {
size_t len = strlen(s);
if (len <= SSO_CAP) {
// SSO:拷到栈缓冲区
memcpy(data_.stack.buf, s, len);
data_.stack.buf[len] = len; // 末字节存长度
} else {
// 堆分配
isHeap_ = true;
data_.heap.cap = len * 2;
data_.heap.size = len;
data_.heap.ptr = new char[data_.heap.cap];
memcpy(data_.heap.ptr, s, len + 1);
}
}
size_t size() const {
if (!isHeap_) {
return static_cast<unsigned char>(data_.stack.buf[SSO_CAP]);
}
return data_.heap.size;
}
const char* c_str() const {
return isHeap_ ? data_.heap.ptr : data_.stack.buf;
}
~SsoString() {
if (isHeap_) delete[] data_.heap.ptr;
}
};关键设计:
- union:
Heap和Stack共享 24 字节(3×8 或 16+8) - 末字节复用:短字符串时,buf[15] 既作为终止符位置,又存储长度
- 阈值 15:GCC/Clang 通常 15,MSVC 通常 7(指针压缩历史遗留)
字符串拼接
多种方式,选对才“好品味”。
1. operator+ (性能陷阱)
std::string s1 = "Hello";
std::string s2 = "World";
std::string s3 = s1 + " " + s2; // 多次临时,内存拷贝2. += / append() (推荐)
std::string s = "Hello";
s += " "; // 原地高效
s.append("World"); // 追加子串3. std::stringstream (过时)
复杂转换用,但性能差、代码长。
4. std::format (C++20,推荐)
#include <format>
std::string s = std::format("Hello {}, v{}", "Linus", 6);优雅、高效、类型安全。
5. std::println (C++23,输出专用)
#include <print>
std::println("Hello {}, kernel v{}", "Linus", 6);无需拼接 string。
总结选择:
- 简单追加:
+= - 复杂格式:
std::format - 输出:
std::println - 禁:循环
+
std::string_view 详解 (C++17)
std::string_view:轻量、非拥有字符串视图。指针+长度。
基本概念与优点
- 零拷贝:无分配
- 安全:边界检查
- 灵活:兼容 string/char*/字面量
用法示例
#include <iostream>
#include <string>
#include <string_view>
int main() {
const char* cstr = "Hello World";
std::string_view sv1(cstr);
std::string str = "Hello World";
std::string_view sv2(str);
std::string_view sv3("Hello World");
std::string_view sv4(cstr, 5); // "Hello"
using namespace std::string_view_literals;
auto sv5 = "Hello World"sv;
std::cout << sv1 << '\n' << sv4 << '\n' << sv5 << '\n';
}字面量后缀
s (C++14, std::string)
using namespace std::string_literals;
auto str = "Hello"s; // std::stringsv (C++17, string_view)
using namespace std::string_view_literals;
auto sv = "Hello"sv; // string_view,零成本性能:函数参数用 sv,字面量零分配;string 临时构造。
避免歧义:
void print(string_view); // sv
void print(const string&); // string
print("hello"); // 歧义?
print("hello"sv); // 明确 sv
print("hello"s); // 明确 stringUTF-8:
auto utf8 = u8"你好"sv;常用操作
string_view sv = "Hello, World!";
cout << sv.length() << '\n'; // 13
cout << sv[0] << sv.back() << '\n';
auto sub = sv.substr(0,5); // "Hello"
auto pos = sv.find("World"); // 7
sv.remove_prefix(7); // "World!"
sv.remove_suffix(1); // "World"string ↔ string_view 转换
- string → view:零成本
- view → string:拷贝
string(sv)
应用:
void process(string_view data) {
if (data.find("modify") != npos) {
string str(data); // 需改时拷贝
str += " [mod]";
}
}函数参数对比
const string&:字面量构造临时string_view:零成本,按值传
string_view 在容器中
核心:生命周期管理。
vector<string_view>
vector<string> pool = {"a","b","c"};
vector<string_view> views;
views.reserve(pool.size());
for (const auto& s : pool) views.emplace_back(s); // 安全陷阱:
vector<string_view> bad() {
string tmp = "bad";
return {string_view(tmp)}; // 悬空 UB
}字符串池类:
class StringPool {
vector<string> pool_;
vector<string_view> views_;
public:
string_view add(string_view sv) {
pool_.emplace_back(sv);
views_.emplace_back(pool_.back());
return views_.back();
}
auto& getViews() const { return views_; }
};性能:vector<string_view> + 池 << vector
关联容器 key
unordered_map<string_view, int> m;
m["key1"] = 1; // 字面量安全动态用池。
陷阱:
unordered_map<string_view,int> bad() {
string tmp="bad";
unordered_map m{{tmp,42}}; // 悬空
return m;
}池管理:
class StringKeyedMap {
vector<string> keys_;
unordered_map<string_view,int> map_;
public:
void insert(string_view k, int v) {
keys_.emplace_back(k);
map_[keys_.back()] = v;
}
};配置解析示例:
class ConfigParser {
vector<string> key_pool_, val_pool_;
unordered_map<string_view, string_view> config_;
public:
void parseLine(string_view line) {
auto pos = line.find('=');
if (pos == npos) return;
auto k = line.substr(0,pos), v=line.substr(pos+1);
key_pool_.emplace_back(k);
val_pool_.emplace_back(v);
config_[key_pool_.back()] = val_pool_.back();
}
string_view get(string_view k) const {
auto it = config_.find(k); return it!=end(config_)?it->second:"";
}
};何时用 string_view
✅ 用
- 函数参数
- 容器 key (字面量/池)
- 子串
❌ 不用
- 拥有/修改:string
- 不确定生命周期
- C API (需 null-term)
好工具,但懂生命周期再用。不然 string 稳。
性能:实际测,别猜。



