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

发现

  1. size < 16:SSO,栈上,无 heap 分配。
  2. 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;
    }
};

关键设计

  • unionHeapStack 共享 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::string

sv (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);    // 明确 string

UTF-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 稳。

性能:实际测,别猜。