c++ string 的allocate和cow

string 可以说是c++ stl 中最常用的容器了,首先弄明白 cpp 中的string 到底是在哪里存储的,下面的代码我们重载了全局的new和delete

#include <string>
#include <iostream>
#include <cstdlib>
#include <vector>
using namespace std;

// 重载全局 new
void* operator new(std::size_t size) {
    cout << "[Allocating " << size << " bytes]\n";
    return malloc(size);
}

// 重载全局的 delete
// void operator delete(void* ptr) noexcept {
//     cout << "[Deallocating]\n";
//     return free(ptr);
// }

int main(int argc, char *argv[]) {
    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]  // no copy on write

通过上面的代码发现有下面2点:

  1. 发现在size小于16的短小string并没有调用new,而是直接分配在栈上的
  2. 使用s1来初始化s2的时候直接分配了内存,并没有使用cow, cpp 高版本取消了cow

string_view

string_view 是 C++17 引入的一个重要类,它提供了一个轻量级的、非拥有的字符串视图。它本质上是一个指向字符序列的指针加上长度信息。

基本概念

string_view 不拥有字符串数据,它只是引用已存在的字符串数据。这使得它非常轻量,可以高效地传递和操作字符串,而无需复制数据。

主要优点

  1. 性能: 无需复制字符串数据,避免了内存分配
  2. 安全性: 有边界检查,避免缓冲区溢出
  3. 灵活性: 可以安全地引用各种字符串类型(C 风格字符串、std::string 等)

基本用法示例

#include <iostream>
#include <string>
#include <string_view>

int main() {
    // 从 C 风格字符串创建
    const char* cstr = "Hello World";
    std::string_view sv1(cstr);
    
    // 从 std::string 创建
    std::string str = "Hello World";
    std::string_view sv2(str);
    
    // 直接字面量
    std::string_view sv3("Hello World");
    
    // 指定长度
    std::string_view sv4(cstr, 5); // "Hello"
    
    // 使用字面量后缀 (C++17)
    using namespace std::string_view_literals;
    auto sv5 = "Hello World"sv;
    
    std::cout << sv1 << std::endl; // 输出: Hello World
    std::cout << sv4 << std::endl; // 输出: Hello
    std::cout << sv5 << std::endl; // 输出: Hello World
    
    return 0;
}

常用操作

#include <iostream>
#include <string_view>

int main() {
    std::string_view sv = "Hello, World!";
    
    // 获取长度
    std::cout << "Length: " << sv.length() << std::endl;
    
    // 访问字符
    std::cout << "First char: " << sv[0] << std::endl;
    std::cout << "Last char: " << sv.at(sv.length()-1) << std::endl;
    
    // 子串操作
    std::string_view sub = sv.substr(0, 5); // "Hello"
    std::cout << "Substring: " << sub << std::endl;
    
    // 查找
    size_t pos = sv.find("World"); // 7
    if (pos != std::string_view::npos) {
        std::cout << "Found 'World' at position: " << pos << std::endl;
    }
    
    // 比较
    std::string_view sv2 = "Hello";
    if (sv.substr(0, 5) == sv2) {
        std::cout << "First 5 chars match 'Hello'" << std::endl;
    }
    
    // 修改视图边界(不修改底层数据)
    sv.remove_prefix(7); // 移除 "Hello, "
    std::cout << "After remove_prefix: " << sv << std::endl; // "World!"
    
    sv.remove_suffix(1); // 移除 "!"
    std::cout << "After remove_suffix: " << sv << std::endl; // "World"
    
    return 0;
}

与 std::string 的区别

特性std::stringstd::string_view
数据拥有权
内存分配可能需要不需要
大小sizeof(std::string)sizeof(指针+长度)
修改能力可修改只读
性能较低较高

作为函数参数:string_view vs const std::string&

这是 string_view 最核心的使用场景。

  • const std::string&:
    • 如果传入 std::string,性能尚可(仅引用)。
    • 如果传入 "literal"char*,会触发隐式转换,产生一个临时的 std::string 对象,导致不必要的堆内存分配和释放。
  • std::string_view:
    • 无论传入 std::string"literal" 还是 char*,都只是简单的指针赋值和长度计算,零内存分配
    • 建议按值传递string_view 足够小(通常是两个指针的大小),按值传递比按引用传递更利于编译器优化(寄存器传参)。

使用注意事项

  1. 生命周期 (Dangling View): 必须确保被引用的字符串数据在 string_view 使用期间保持有效。
    std::string_view get_view() {
        std::string s = "temporary";
        return std::string_view(s); // 危险!返回了局部变量的视图
    }
  2. 只读: string_view 不允许修改底层数据。如果需要修改,请使用 std::string
  3. 非空终止 (Null-termination): string_view 不保证以 \0 结尾。
    • 绝不要把 sv.data() 直接传给需要 C 风格字符串的函数(如 printf, strlen, open)。
    • 如果必须调用这类 API,请先将其转换为 std::stringstd::string(sv).c_str()

实际应用场景

// 函数参数:避免不必要的字符串复制
void process_text(std::string_view text) {
    std::cout << "Processing: " << text << " (length: " << text.length() << ")" << std::endl;
}

int main() {
    // 可以传入不同类型的字符串而无需转换
    process_text("C-style string");           // 字面量
    process_text(std::string("std::string")); // std::string
    
    const char* ptr = "Pointer to string";
    process_text(std::string_view(ptr, 6));   // 自定义视图
    
    return 0;
}

使用 string_view 可以显著提高程序性能,特别是在需要频繁传递字符串参数但不需要修改字符串的场景中。