标准容器与字符串
学习使用 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)是对字符数据的非拥有视图,避免了你只需要读取时的复制。
#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 算法配合使用。它不进行堆分配。
#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>&` 之间的关键区别是什么?