std::vector 简介与列表构造函数

背景:解决可扩展性难题

在上一课《容器与数组简介》中,我们介绍了容器与数组。本课将重点介绍本章后续将频繁使用的数组类型: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 是一个元素类型为 intstd::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 形参,完成以下三项工作:

  1. 按需分配足够存储空间;
  2. 将容器长度设为初始化列表元素个数;
  3. 按顺序用列表值初始化各元素。

因此,向容器提供初始化列表时,列表构造函数被调用,容器即按列表构造。

最佳实践
使用列表初始化(值列表)为容器赋予初始元素。


通过下标运算符访问数组元素

创建数组后,如何访问元素?
类比邮局信箱:一排编号为 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. 列表构造:1 个元素,值为 10;
  2. 单参构造:长度 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,再拷贝初始化
};

constconstexpr 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

(各题答案略)

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"