跳至内容

标准容器与字符串

学习使用 std::string、std::string_view、std::vector、std::array 以及 std::span ── 现代 C++ 中安全高效的数据处理基础构建块。

为什么使用标准容器?

C 提供了原始数组(int arr[10])和 C 字符串(char*),但这些都容易出错:原始数组不知道自己的大小,C 字符串需要手动内存管理,而且两者都不会检查越界访问。

C++ 标准库提供了容器 ── 管理自己的内存、知道自己的大小,并提供安全访问方法的类型。对于大多数代码来说,你需要:std::string 用于文本,std::vector 用于动态数组,std::array 用于固定大小的数组。这些是现代 C++ 的基础 ── 学会默认使用它们。

std::string 与 std::string_view

std::string 拥有自己的字符数据 ── 它管理内存并提供丰富的操作接口。std::string_view(C++17)是对字符数据的非拥有视图,避免了你只需要读取时的复制。

storage.cppc++23
#include <iostream>
#include <string>

// Static storage duration — lives for the entire program
int global_count = 0;                    // Initialized before main()
static int file_local = 42;              // Same, but only visible in this file

void demonstrate_storage() {
    // Automatic storage duration — created on entry, destroyed on exit
    int local = 10;                      // Born here, dies at closing brace

    // Static storage duration (local scope)
    static int call_count = 0;           // Initialized ONCE, persists across calls
    ++call_count;
    std::cout << "Call #" << call_count << '\n';

    // Thread-local storage duration (C++11)
    // thread_local int per_thread = 0;  // One copy per thread, lives until thread ends
}

int main() {
    demonstrate_storage();  // Call #1
    demonstrate_storage();  // Call #2 — call_count is still alive!
    demonstrate_storage();  // Call #3

    // Dynamic storage duration — you control the lifetime
    int* heap_int = new int{99};         // Born here
    std::cout << *heap_int << '\n';
    delete heap_int;                     // Dies here — you must not forget!
    heap_int = nullptr;                  // Good practice: nullify after delete

    // In modern C++, prefer smart pointers over raw new/delete
    // auto ptr = std::make_unique<int>(99);  // Covered in later modules
}
输出

点击“运行”即可编译并运行这个示例。

std::vector ── 动态数组

std::vector 是大多数情况下的默认容器。它管理动态数组 ── 根据需要自动增长,提供 O(1) 随机访问,并将元素连续存储在内存中(缓存友好)。

std::array ── 固定大小数组

std::array 是 C 风格数组的零开销替代品。它知道自己的大小,可以传递给函数和从函数返回,并与 STL 算法配合使用。它不进行堆分配。

init_forms.cppc++23
#include <iostream>

struct Point {
    int x;
    int y;
};

static int global_int;          // Zero-initialized → 0
static Point global_point;      // Zero-initialized → {0, 0}

int main() {
    int a;                      // Default-initialized → INDETERMINATE (UB to read!)
    int b{};                    // Value-initialized → 0
    int c = int();              // Value-initialized → 0
    int d = {};                 // Value-initialized → 0

    Point p1;                   // Default-initialized → members are INDETERMINATE
    Point p2{};                 // Value-initialized → {0, 0}
    Point p3{1, 2};             // Aggregate-initialized → {1, 2}

    // Array initialization
    int arr1[5];                // Default-initialized → INDETERMINATE values!
    int arr2[5]{};              // Value-initialized → {0, 0, 0, 0, 0}
    int arr3[5]{1, 2};          // Partially initialized → {1, 2, 0, 0, 0}

    std::cout << "global_int: " << global_int << '\n';  // Safe: 0
    std::cout << "b: " << b << '\n';                    // Safe: 0
    // std::cout << a;  // DANGER: UB — reading uninitialized variable!

    std::cout << "p2: (" << p2.x << ", " << p2.y << ")\n";  // Safe: (0, 0)
    std::cout << "p3: (" << p3.x << ", " << p3.y << ")\n";  // Safe: (1, 2)
}
输出

点击“运行”即可编译并运行这个示例。

std::span ── 非拥有视图(C++20)

std::span(C++20)是任何连续序列的非拥有视图 ── 它可以引用 std::vector、std::array、C 数组,甚至是原始指针+大小对。它是 std::string_view 对于任意类型的推广。

常见陷阱

std::vector 在需要更多空间时按指数增长(通常容量翻倍)。每次重新分配都会复制/移动所有元素到一个新的内存块。在此过程中任何迭代器、指针或对元素的引用都会失效。

在性能关键代码中,使用 reserve(n) 预分配足够空间以避免重新分配。永远不要在 push_back 之后持有指向 vector 元素的指针 ── 它可能已经被移动了。

  • push_back 可能触发重新分配,使所有迭代器和引用失效
  • 使用 reserve() 来预分配,如果你提前知道大小
  • string_view 不拥有数据 ── 确保底层字符串的生命周期超过 view 的生命周期
  • 使用 .at(i) 进行边界检查访问或在调试构建中启用检查
关键要点

这种模式通过将构造推迟到首次使用时来解决静态初始化顺序问题:

关键要点
  • 作用域(名称可见的位置)和生命周期(对象存在的时间)是两个不同的概念,它们可能不一致
  • C++ 有四种存储期:自动(栈)、静态(程序生命周期)、线程局部和动态(堆)
  • 默认初始化的内置类型具有不确定的值 ── 读取它们是未定义行为
  • 使用 {} 进行值初始化可以保证内置类型的零初始化
  • 局部变量按后进先出顺序销毁 ── 在依赖项之前声明被依赖的对象
  • 使用「首次使用时构造」惯用法来避免跨翻译单元的静态初始化顺序问题

测验 — 检验你的知识

(15 经验值)

1. `std::string_view` 相比 `const std::string&` 作为参数类型的优势是什么?

2. 在 `push_back` 之后指向 `std::vector` 元素的指针会发生什么?

3. `std::span<int>` 和 `std::vector<int>&` 之间的关键区别是什么?