16-1 容器与数组介绍
变量可扩展性挑战
设想这样一个场景:我们需要记录30名学生的测试成绩并计算班级平均分。为此,我们需要30个变量。我们可以这样定义它们:
// allocate 30 integer variables (each with a different name)
int testScore1 {};
int testScore2 {};
int testScore3 {};
// ...
int testScore30 {};
要定义的变量可真不少!而要计算班级的平均分,我们需要这样操作:
int average { (testScore1 + testScore2 + testScore3 + testScore4 + testScore5
+ testScore6 + testScore7 + testScore8 + testScore9 + testScore10
+ testScore11 + testScore12 + testScore13 + testScore14 + testScore15
+ testScore16 + testScore17 + testScore18 + testScore19 + testScore20
+ testScore21 + testScore22 + testScore23 + testScore24 + testScore25
+ testScore26 + testScore27 + testScore28 + testScore29 + testScore30)
/ 30; };
这不仅需要大量输入,而且非常重复(而且很容易在输入数字时出错却浑然不觉)。如果我们想对这些值进行任何操作(比如将它们打印到屏幕上),就必须重新输入所有这些变量名。
现在假设我们需要修改程序以适应新加入班级的学生。必须手动扫描整个代码库,在所有相关位置添加 testScore31。每次修改现有代码都可能引入新错误——比如在计算平均分时,很容易忘记将除数从30更新为31!
这还是仅有30个变量的情况。试想当对象数量达到数百甚至数千时,若需处理多个同类型对象,单独定义变量显然无法扩展。
我们可以将数据封装在结构体中:
struct testScores
{
// allocate 30 integer variables (each with a different name)
int score1 {};
int score2 {};
int score3 {};
// ...
int score30 {};
}
虽然这为我们的分数提供了额外的组织结构(并使我们能够更轻松地将分数传递给函数),但它并未解决核心问题:我们仍然需要单独定义和访问每个测试分数对象。
正如你可能猜到的,C++ 提供了应对上述挑战的解决方案。在本章中,我们将介绍其中一种解决方案。而在后续章节中,我们将探索该解决方案的其他变体。
容器
当你去杂货店买一打鸡蛋时,你(大概)不会逐个挑选12枚鸡蛋放进购物车(你不会这样做,对吧?)。相反,你很可能会选择一盒鸡蛋。鸡蛋盒就是一种容器,装有预先确定数量的鸡蛋(通常是6枚、12枚或24枚)。现在想想早餐麦片,里面装满许多小颗粒。你肯定不想把这些颗粒单独存放在食品柜里!麦片通常装在盒子里,盒子就是另一种容器。现实生活中我们时刻都在使用容器,因为它们能轻松管理物品集合。
编程中也存在容器,用于更便捷地创建和管理(可能规模庞大的)对象集合。在通用编程中,容器container是一种数据类型,用于存储未命名的对象集合(称为元素elements)。
核心要点:
当需要处理相关联的值集合时,我们通常会使用容器。
事实上,你已经使用过一种容器类型:字符串!字符串容器为字符集合提供存储空间,这些字符可输出为文本:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" }; // strings are a container for characters
std::cout << name; // output our string as a sequence of characters
return 0;
}

容器的元素是无名的
虽然容器对象本身通常有名称(否则我们如何使用它?),但容器的元素是无名的。这样我们就能在容器中放入任意数量的元素,而无需为每个元素指定唯一名称!这种无命名元素的特性至关重要,正是容器区别于其他数据结构的关键。这也解释了为何普通结构体(仅由数据成员组成的集合,如上文testScores结构体)通常不被视为容器——其数据成员需要唯一命名。
在上例中,字符串容器本身有名称(name),但容器内的字符(‘A’、'l'、‘e’、'x')则没有。
既然元素本身没有名称,我们如何访问它们?每个容器都提供一种或多种访问元素的方法——具体方式取决于容器类型。我们将在下一课中看到首个示例。
关键要点:
容器元素本身不具备独立名称,这使得容器能容纳任意数量的元素,且无需为每个元素指定唯一名称。各类容器均提供访问元素的方法,但具体实现方式取决于容器的具体类型。
容器的长度
在编程中,容器中元素的数量通常称为其长度(有时也称计数)。
在第5.7节——std::string简介中,我们展示了如何使用std::string的length成员函数获取字符串容器中的字符元素数量:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" };
std::cout << name << " has " << name.length() << " characters\n";
return 0;
}
这将输出:

在C++中,“size”一词也常用于表示容器中的元素数量。这种命名方式并不理想,因为“size”同时也可指代对象占用的内存字节数(由sizeof运算符返回)。
在描述容器元素数量时,我们更倾向使用“length”一词;而指代对象所需存储空间时,则使用“size”。
容器操作
让我们回到鸡蛋纸盒的例子。你可以对这个纸盒做什么操作呢?首先,你可以获取一个鸡蛋纸盒。你可以打开纸盒取出一个鸡蛋,然后对这个鸡蛋进行任意操作。你可以从纸盒中移除现有鸡蛋,或在空位添加新鸡蛋。你还可以统计纸盒中的鸡蛋数量。
同理,容器通常实现以下操作的重要子集:
- 创建容器(例如:创建空容器、为初始数量元素分配存储空间、从值列表创建容器)。
- 访问元素(例如:获取首元素、获取末元素、获取任意元素)。
- 插入和移除元素。
- 获取容器中元素的数量。
容器还可能提供其他操作(或上述操作的变体),以协助管理元素集合。
现代编程语言通常提供多种不同的容器类型。这些容器类型在实际支持的操作及其性能方面存在差异。例如,一种容器类型可能提供快速访问容器中任意元素的功能,但不支持插入或删除元素。另一种容器可能支持快速插入和删除元素,但仅允许按顺序访问元素。
每种容器都有其优势和局限性。为特定任务选择合适的容器类型,对代码可维护性和整体性能都至关重要。我们将在后续课程中深入探讨这一主题。
元素类型
在大多数编程语言(包括C++)中,容器是同构的homogenous,这意味着容器中的元素必须具有相同类型。
某些容器采用预设元素类型(例如字符串容器通常存储字符元素),但更常见的是由容器使用者自行设定元素类型。在C++中,容器通常以类模板形式实现,用户可通过模板类型参数指定所需元素类型。下节课我们将看到具体示例。
这种设计赋予容器高度灵活性——无需为每种元素类型创建新容器类型,只需根据所需元素类型实例化模板类即可投入使用。
顺带一提...:
与同构容器相对的是异构容器heterogenous,其允许元素采用不同类型。异构容器通常由脚本语言(如Python)提供支持。
C++中的容器
容器库是C++标准库的一部分,包含多种实现常见容器类型的类。实现容器功能的类有时被称为容器类。容器库中所有容器的完整列表在此处有详细说明。
在C++中,“容器”的定义比通用编程定义更为狭窄。仅容器库中的类类型才被视为C++中的容器。我们将使用“容器”一词泛指各类容器,而“容器类”特指容器库中的容器类类型。
进阶读者须知:
以下类型符合通用编程定义中的容器概念,但未被C++标准视为容器:
- C风格数组
- std::string
- std::vector
要在C++中成为容器,必须实现此处列出的全部要求。需注意这些要求包含特定成员函数的实现——这意味着C++容器必须是类类型!上述类型并未实现全部要求。
然而由于std::string和std::vector
实现了大部分要求,它们在多数情况下表现得像容器。因此有时被称为“伪容器”。
在提供的容器类中,std::vector 和 std::array 的使用频率远超其他类型,我们将重点关注这两者。其余容器类通常仅用于更特殊的情境。
数组简介
数组array是一种容器数据类型,用于连续contiguously存储一组值(即每个元素都位于相邻的内存位置,没有间隔)。数组允许快速、直接地访问任何元素。它们在概念上简单且易于使用,因此当我们需要创建和处理一组相关值时,它们是首选方案。
C++包含三种主要数组类型:(C风格)数组、std::vector容器类和std::array容器类。
(C 风格) 数组继承自 C 语言。为保持向后兼容性,这些数组作为 C++ 核心语言的一部分被定义(类似于基本数据类型)。C++ 标准称其为“数组”,但在现代 C++ 中,为区别于同名的 std::array,通常称其为 C 数组C arrays或 C 风格数组C-style arrays。C 风格数组有时也被称为“裸数组naked arrays”、“固定大小数组fixed-sized arrays”、“固定数组fixed arrays”或“内置数组built-in arrays”。我们将优先使用“C 风格数组C-style array”这一术语,并在讨论数组类型时使用“数组array”一词。按现代标准来看,C 风格数组的行为怪异且存在危险性。我们将在后续章节探讨其原因。
为提升C++中数组的安全性和易用性,C++03引入了std::vector容器类。作为三种数组类型中最灵活的容器,它具备其他类型所不具备的诸多实用特性。
最后,C++11引入了std::array容器类,作为C风格数组的直接替代方案。它虽比std::vector功能更受限,但在处理小型数组时效率更高。
这些数组类型在现代C++中仍以不同方式并存,因此我们将对三者进行不同程度的讲解。
继续前进
在下一课中,我们将介绍首个容器类 std::vector,并开始展示它如何高效解决本课开头提出的挑战。我们将花大量时间讲解 std::vector,因为需要引入不少新概念,并解决过程中遇到的额外难题。
值得庆幸的是,所有容器类都具有相似的接口。因此,一旦掌握了某类容器(如std::vector)的使用方法,学习其他容器(如std::array)就会简单得多。对于后续将介绍的容器(如std::array),我们将重点说明显著差异(并重申核心要点)。
作者注:
术语说明:
- 当讨论适用于大多数或全部标准库容器类的情况时,我们将使用“容器类”这一统称。
- 当讨论普遍适用于所有数组类型(包括其他编程语言实现的数组)的内容时,我们将使用“数组”一词。
std::vector同时属于这两类范畴,因此即使术语不同,相关知识仍适用于std::vector。
准备好了吗?
开始吧Let’s goooooooooooooooooooooooooooooooooooooooooooooooo!

浙公网安备 33010602011771号