背景:解决可扩展性难题
在上一课《容器与数组简介》中,我们介绍了容器与数组。本课将重点介绍本章后续将频繁使用的数组类型:std::vector
,并用它解决上一课提出的“变量可扩展性”问题。
std::vector
概述
std::vector
是 C++ 标准容器库中的一个容器类,专门实现动态数组。它定义在 <vector>
头文件中,并以类模板形式提供,其模板参数指定元素类型。例如:
std::vector<int> // 元素类型为 int 的 vector
创建 std::vector
对象非常简单:
#include <vector>
int main()
{
// 值初始化(调用默认构造函数)
std::vector<int> empty{}; // 空 vector,当前含 0 个 int 元素
return 0;
}
变量 empty
是一个元素类型为 int
的 std::vector
。由于采用值初始化,它初始为空(零元素)。空 vector 虽看似无用,但在后续课程(特别是 16.11《std::vector 与栈行为》)中常会用到。
用值列表初始化 std::vector
容器的核心用途是管理一组相关值,因此最常见的需求是用一组初始值来初始化容器。这可通过列表初始化实现:
#include <vector>
int main()
{
// 列表构造(调用列表构造函数)
std::vector<int> primes{ 2, 3, 5, 7 }; // 含 4 个 int 元素
std::vector vowels{ 'a', 'e', 'i', 'o', 'u' }; // 含 5 个 char 元素,C++17 CTAD 自动推导元素类型
return 0;
}
primes
显式指定元素类型为int
,并提供 4 个初始值。vowels
未显式指定类型,利用 C++17 的 CTAD(类模板实参推导)由初始值推导出元素类型为char
。
列表构造函数与 std::initializer_list
在第 13.8 课《结构体聚合初始化》中,我们把花括号包围的逗号分隔值称为初始化列表。
容器通常提供列表构造函数,接受 std::initializer_list
形参,完成以下三项工作:
- 按需分配足够存储空间;
- 将容器长度设为初始化列表元素个数;
- 按顺序用列表值初始化各元素。
因此,向容器提供初始化列表时,列表构造函数被调用,容器即按列表构造。
最佳实践
使用列表初始化(值列表)为容器赋予初始元素。
通过下标运算符访问数组元素
创建数组后,如何访问元素?
类比邮局信箱:一排编号为 0, 1, 2… 的邮箱,0 号即第 1 个。
C++ 中最常见的元素访问方式是下标运算符 []
:
arrayName[index]
index
称为下标或索引。- 数组零基索引:首元素索引 0,次元素 1,依此类推。
- 下标运算符返回元素引用,可直接读写。
示例:
#include <iostream>
#include <vector>
int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
std::cout << "第一个素数:" << primes[0] << '\n';
std::cout << "第二个素数:" << primes[1] << '\n';
std::cout << "前 5 个素数之和:"
<< primes[0] + primes[1] + primes[2] + primes[3] + primes[4] << '\n';
return 0;
}
输出:
第一个素数:2
第二个素数:3
前 5 个素数之和:28
越界访问
索引必须落在有效范围内:
若容器长度为 N,则合法索引为 0 到 N−1。operator[]
不做边界检查;传入非法索引将导致未定义行为。
提示
- 索引 N 并不存在,使用它将访问“尾后”位置,引发未定义行为。
- 某些编译器(如 Visual Studio)在调试模式提供运行时断言,发布模式则移除以提高性能。
数组内存连续
数组的显著特征是元素在内存中连续存储(无间隔)。
示例:
#include <iostream>
#include <vector>
int main()
{
std::vector primes{ 2, 3, 5 };
std::cout << "int 大小:" << sizeof(int) << " 字节\n";
std::cout << &primes[0] << '\n';
std::cout << &primes[1] << '\n';
std::cout << &primes[2] << '\n';
}
输出地址间隔等于 int
大小(4 字节)。
连续存储带来零额外开销,且支持随机访问(直接访问任意元素),效率极高。
指定长度的 std::vector
构造
若需用户输入 10 个值,应先创建长度为 10 的 std::vector
。
不推荐用 10 个占位符列表初始化:
std::vector<int> data{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // 冗长且难维护
std::vector
提供显式构造函数:
std::vector<int> data(10); // 含 10 个 int,值初始化为 0
元素被值初始化(int
为 0,类类型调用默认构造函数)。
该构造函数需直接初始化调用:
std::vector<int> v(10); // 正确
列表构造与单参构造的歧义
std::vector<int> v{ 10 }; // 意图?
候选:
- 列表构造:1 个元素,值为 10;
- 单参构造:长度 10,元素值初始化。
C++ 规则:非空初始化列表优先匹配列表构造函数。
因此 { 10 }
选择列表构造,创建 1 个元素 10。
各初始化形式对比:
形式 | 结果 |
---|---|
std::vector<int> v1 = 10; | 错误:拷贝初始化不匹配 explicit 构造函数 |
std::vector<int> v2(10); | 长度 10,值初始化 |
std::vector<int> v3{ 10 }; | 1 个元素 10 |
std::vector<int> v4 = { 10 }; | 同上 |
std::vector<int> v5({ 10 }); | 同上 |
std::vector<int> v6{}; | 空 vector(默认构造) |
最佳实践
若欲用非元素值参数构造容器,请使用直接初始化。
类成员默认初始化陷阱
struct Foo
{
std::vector<int> v1(8); // 错误:成员默认初始化不允许直接初始化
};
成员默认初始化只允许拷贝/列表初始化,且禁止 CTAD。
正确写法:
struct Foo
{
std::vector<int> v{ std::vector<int>(8) }; // 先构造临时 vector,再拷贝初始化
};
const
与 constexpr
std::vector
const std::vector<int>
必须在定义时初始化,之后不可修改,元素视为const
。- 禁止元素类型自身为
const
(如std::vector<const int>
)。 std::vector
不能声明为constexpr
;若需constexpr
数组,请使用std::array
。
命名由来
“vector” 一词在日常英语中指几何向量,但 std::vector
是动态数组。
Alexander Stepanov 在 From Mathematics to Generic Programming 中坦言:
“STL 中的 vector 源自 Scheme 与 Common Lisp,可惜与数学中的向量含义不符……本应叫 array,但错误一旦传播便难以更正。”
小测验
问题 1
使用 CTAD 定义 std::vector
,并用前 5 个正平方数(1, 4, 9, 16, 25)初始化。
问题 2
以下两行有何行为差异?
std::vector<int> v1{ 5 };
std::vector<int> v2(5);
问题 3
显式指定模板参数,定义 std::vector
以存储一年 365 天每日高温(精确到 0.1℃)。
问题 4
利用 std::vector
编写程序:提示用户输入 3 个整数,输出其和与积。
示例交互:
Enter 3 integers: 3 4 5
The sum is: 12
The product is: 60
(各题答案略)