1.2 组织数据的新方式
在序言给出的学生成绩处理程序里我们使用固定长度数组保存学生:
#define MAXCOUNT 100
Student students[MAXCOUNT];这会把「目前输入了多少人
标准库不是附加内容
C++ 标准不仅规定语言语法,也提供容器、算法、字符串、时间、文件系统、线程等库设施。
算法竞赛和课程练习有时要求亲手实现底层数据结构,这是很有价值的训练。不过在一般工程项目中,重复实现已有的基础设施通常会增加测试和维护成本。使用标准库类型通常能同时获得:
- 明确的生命周期和所有权;
- 已经处理好的复制与移动行为;
- 与标准库算法统一的接口;
- 跨编译器和平台的可移植性。
三种常见的连续数据
| 类型 | 长度 | 典型用途 |
|---|---|---|
std::array<T, N> | 编译期固定 | 始终恰好有 N 个元素 |
std::vector<T> | 运行时可变 | 元素数量随输入或处理过程变化 |
std::string | 运行时可变 | 保存和处理文本 |
固定长度的 std::array
#include <array>
std::array<int, 4> scores{72, 85, 59, 91};它与内置数组一样连续存储元素,但知道自己的长度,也能直接参与赋值和返回值传递。我们可以使用 C++11 引入的基于范围的 for 循环(range-based for loop)遍历它:
for (int score : scores) {
std::cout << score << '\n';
}std::array 的成员函数这里不再逐一列举,可以查阅 cppreference:std::array。也可以阅读 菜鸟教程:C++ 容器类 <array>。
不过当长度来自运行时输入时,std::array 并不适合,此时考虑另外两种类型。
可变长度的 std::vector
#include <cstddef>
#include <vector>
std::vector<int> scores;
scores.push_back(72);
scores.push_back(85);std::vector 会自动管理动态存储。对象销毁时,它拥有的元素和内存也会得到正确清理。元素数量通过 size() 获取:
for (std::size_t index{0}; index < scores.size(); ++index) {
std::cout << index << ": " << scores[index] << '\n';
}关于 std::size_t
std::size_t 是容器长度和下标常用的无符号类型,它是一个基本的、与机器相关的无符号整型。一般来说 sizeof 算符返回的结果类型就是它。在不同平台上,std::size_t 可能会有不同的长度。
不过,如果根本不需要下标,范围 for 更简单:
for (int score : scores) {
std::cout << score << '\n';
}std::vector 常会预留一些空间用于追加新元素,可以使用 capacity 方法获取当前分配的存储容量。
如果预留的空间不足,则会重新分配更大的内存空间,把原有的元素复制或移动到新的位置,并归还旧内存。一旦发生重新分配,所有指向原有元素的指针、引用和迭代器都会失效。这个过程需要处理已有元素,因此如果对未来会使用的空间有大致了解,可以使用 reserve 来预留空间而不创建元素,从而减少大量追加操作中的重新分配次数。
void print_size_cap(std::vector<int> &vec) {
std::cout << "size=" << vec.size() << ", cap=" << vec.capacity()
<< ", addr=" << vec.data() << '\n';
}
std::vector<int> foo;
for (int i{0}; i < 10; i++) {
foo.push_back(1);
print_size_cap(foo);
}
foo.reserve(500);
print_size_cap(foo);
for (int i{0}; i < 400; i++) {
foo.push_back(1);
}
print_size_cap(foo);我这里得到的结果是:
size=1, cap=1, addr=0x2240ae9f840
size=2, cap=2, addr=0x2240ae9f860
size=3, cap=4, addr=0x2240aea2eb0
size=4, cap=4, addr=0x2240aea2eb0
size=5, cap=8, addr=0x2240aea21d0
size=6, cap=8, addr=0x2240aea21d0
size=7, cap=8, addr=0x2240aea21d0
size=8, cap=8, addr=0x2240aea21d0
size=9, cap=16, addr=0x2240aea5970
size=10, cap=16, addr=0x2240aea5970
size=10, cap=500, addr=0x2240aebed30
size=410, cap=500, addr=0x2240aebed30可以看到,在预留空间不足时,std::vector 会自动开辟新的内存空间,并把原本的值复制或移动到新位置。在手动预留了 500 个元素的空间后,此后的 400 次 push_back 不再触发重新分配。
可以前往 cppreference:std::vector 了解更多成员函数和迭代器失效规则。也可以阅读 菜鸟教程:C++ 容器类 <vector>。
文本使用 std::string
#include <string>
std::string name{"Hello"};
name += " world!";std::string 管理字符存储,并提供长度、查找、拼接和比较等操作,而且可以方便地与原始的 C 风格字符数组互转。
可以前往 cppreference:std::basic_string 了解更多成员函数。也可以阅读 菜鸟教程:C++ 标准库 <string>。
借用连续数据
函数有时只想查看一段数据,并不需要拥有或复制容器。C++20 的 std::span 可以表达这种借用关系。
#include <span>
double average(std::span<const int> scores) {
int sum{};
for (int score : scores) {
sum += score;
}
return scores.empty() ? 0.0 : static_cast<double>(sum) / scores.size();
}这个函数可以接收 std::array、std::vector 或内置数组中的连续整数。span 不拥有元素,也不会延长它们的生命周期。
类似地,std::string_view 可以只读地查看一段字符:
#include <iostream>
#include <string_view>
void greet(std::string_view name) {
std::cout << "Hello, " << name << "!\n";
}
span原来的名字叫做array_view,在进入标准时改成了span。
可以把这两类视图直观地理解为「对一段连续数据的轻量描述span 或 string_view 会产生悬空视图。因此,它们适合作为函数参数或用于短期观察。
其他表达意图的工具
带作用域的枚举
为了表达有限个状态,C++ 从 C 继承了传统枚举 enum。但它有一些问题:
- 命名空间污染
- 会被隐式转换为整型
考虑下面的代码:
enum Foo { bar, baz };
auto bar{1};
// 'auto bar' redeclared as different kind of entity
if (baz < 10)
std::cout << "true"; // 输出 true这里的 bar 和 baz 直接进入了当前作用域,污染了命名空间,其他变量不能再使用这些名称。而且,baz < 10 直接发生了隐式类型转换。
为此,C++11 引入了带作用域的枚举(scoped enumerations
enum class Foo { bar, baz };使用 Foo::bar 访问成员。这样避免了作用域污染。同时,enum class 声明的枚举成员不再进行隐式类型转换:
if (Foo::baz < 10)
std::cout << "true";
// no match for 'operator<' (operand types are 'main()::Foo' and 'int')空指针 nullptr
C 中常使用 0 或 NULL 表示空指针,可能参与整数重载并造成歧义。
C++11 引入了 nullptr 用于表示空指针:
int *selected{nullptr};小结
- 使用标准库容器表达连续数据
- 固定长度:
std::array - 不固定长度:
std::vector,可预留空间以避免频繁重新分配 - 字符串:
std::string - 借用连续数据:
std::span与std::string_view
- 固定长度:
- 使用
enum class取代传统enum - 使用
nullptr表示空指针,取代0或NULL