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;
}

image


容器的元素是无名的

虽然容器对象本身通常有名称(否则我们如何使用它?),但容器的元素是无名的。这样我们就能在容器中放入任意数量的元素,而无需为每个元素指定唯一名称!这种无命名元素的特性至关重要,正是容器区别于其他数据结构的关键。这也解释了为何普通结构体(仅由数据成员组成的集合,如上文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;
}

这将输出:

image

在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 arraysC 风格数组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!


posted @ 2026-01-03 23:17  游翔  阅读(26)  评论(0)    收藏  举报