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。任何时候修改现有代码,我们都有引入新 bug 的风险。例如,很容易忘记将平均分计算中的除数从 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 个)。现在考虑早餐麦片,里面有许多小块麦片。您肯定不想把所有这些麦片单独储存在您的食品储藏室里!麦片通常装在盒子里,那是另一种容器。我们在现实生活中一直使用容器,因为它们使管理物品集合变得容易。

容器在编程中也存在,以便更容易地创建和管理(可能很大的)对象集合。在一般编程中,**容器**是一种数据类型,它为未命名对象(称为**元素**)的集合提供存储。

关键见解

我们通常在需要处理一组相关值时使用容器。

事实证明,您已经在使用一种容器类型:字符串!字符串容器为字符集合提供存储,然后可以将其作为文本输出。

#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::stringlength 成员函数来获取字符串容器中字符元素的数量

#include <iostream>
#include <string>

int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << name.length() << " characters\n";

    return 0;
}

这会打印

Alex has 4 characters

在 C++ 中,术语**大小**也常用于表示容器中元素的数量。这是一个不幸的命名选择,因为术语“大小”也可以指对象使用的内存字节数(由 sizeof 运算符返回)。

我们更喜欢用“长度”来指容器中元素的数量,并用“大小”来指对象所需的存储空间量。

容器操作

我们暂时回到那盒鸡蛋。你如何处理这样一盒鸡蛋?首先,你可以买一盒鸡蛋。你可以打开那盒鸡蛋并选择一个鸡蛋,然后用那个鸡蛋做任何你想做的事。你可以从纸箱中取出已有的鸡蛋,或者向空位添加一个新鸡蛋。你还可以数出纸箱中鸡蛋的数量。

类似地,容器通常实现以下操作的一个重要子集

  • 创建一个容器(例如,空容器,具有一定初始元素存储空间,或从值列表创建)。
  • 访问元素(例如,获取第一个元素、获取最后一个元素、获取任意元素)。
  • 插入和删除元素。
  • 获取容器中的元素数量。

容器还可以提供其他操作(或上述操作的变体),以帮助管理元素集合。

现代编程语言通常提供各种不同的容器类型。这些容器类型在它们实际支持的操作以及这些操作的性能方面有所不同。例如,一种容器类型可能提供对容器中任何元素的快速访问,但不支持元素的插入或删除。另一种容器类型可能提供元素的快速插入和删除,但只允许按顺序访问元素。

每个容器都有一套优点和局限性。为要解决的任务选择正确的容器类型,可以对代码的可维护性和整体性能产生巨大影响。我们将在未来的课程中进一步讨论这个主题。

元素类型

在大多数编程语言(包括 C++)中,容器是**同构的**,这意味着容器的元素必须具有相同的类型。

某些容器使用预设的元素类型(例如,字符串通常具有 `char` 元素),但更常见的是,元素类型可以由容器的用户设置。在 C++ 中,容器通常实现为类模板,以便用户可以将所需的元素类型作为模板类型参数提供。我们将在下一课中看到一个示例。

这使得容器具有灵活性,因为我们不需要为每种不同的元素类型创建新的容器类型。相反,我们只需使用所需的元素类型实例化类模板,即可开始使用。

题外话…

与同构容器相反的是**异构**容器,它允许元素具有不同的类型。异构容器通常由脚本语言(如 Python)支持。

C++ 中的容器

**容器库**是 C++ 标准库的一部分,它包含各种实现某些常见容器类型的类类型。实现容器的类类型有时称为**容器类**。容器库中容器的完整列表记录在此处

在 C++ 中,“容器”的定义比一般编程定义要狭窄。只有容器库中的类类型才在 C++ 中被视为容器。我们在泛指容器时会使用术语“容器”,而在特指容器库中的容器类类型时会使用“容器类”。

致进阶读者

以下类型在一般编程定义下是容器,但在 C++ 标准中不被视为容器

  • C 风格数组
  • std::string
  • std::vector<bool>

要成为 C++ 中的容器,容器必须实现此处列出的所有要求。请注意,这些要求包括某些成员函数的实现——这意味着 C++ 容器必须是类类型!上面列出的类型并未实现所有这些要求。

然而,由于 std::stringstd::vector 实现了大部分要求,它们在大多数情况下表现得像容器。因此,它们有时被称为“伪容器”。

在提供的容器类中,`std::vector` 和 `std::array` 的使用率最高,也将是我们重点关注的对象。其他容器类通常仅在更专业的情况下使用。

数组简介

**数组**是一种容器数据类型,它**连续**存储一系列值(这意味着每个元素都放置在相邻的内存位置,没有间隙)。数组允许对任何元素进行快速直接访问。它们在概念上简单且易于使用,这使得它们成为我们需要创建和处理一组相关值时的首选。

C++ 包含三种主要的数组类型:(C 风格)数组、std::vector 容器类和 std::array 容器类。

(C 风格)数组继承自 C 语言。为了向后兼容,这些数组被定义为核心 C++ 语言的一部分(很像基本数据类型)。C++ 标准称它们为“数组”,但在现代 C++ 中,为了将它们与名称相似的 `std::array` 区分开来,它们通常被称为 **C 数组** 或 **C 风格数组**。C 风格数组有时也被称为“裸数组”、“固定大小数组”、“固定数组”或“内置数组”。我们更喜欢使用术语“C 风格数组”,在泛指数组类型时使用“数组”。按照现代标准,C 风格数组行为怪异且危险。我们将在未来的章节中探讨其原因。

为了使 C++ 中的数组更安全、更易于使用,C++03 引入了 std::vector 容器类。std::vector 是三种数组类型中最灵活的一种,并且拥有其他数组类型所不具备的许多有用功能。

最后,`std::array` 容器类在 C++11 中引入,作为 C 风格数组的直接替代品。它比 `std::vector` 更受限制,但也可能更高效,特别是对于较小的数组。

所有这些数组类型在现代 C++ 中仍以不同能力使用,因此我们将不同程度地介绍这三种类型。

展望未来

在下一课中,我们将介绍我们的第一个容器类 `std::vector`,并开始我们的旅程,展示它如何有效地解决本课开头提出的挑战。我们将花大量时间在 `std::vector` 上,因为我们需要引入许多新概念,并在此过程中解决一些额外的挑战。

一个优点是所有容器类都具有相似的接口。因此,一旦您学会了如何使用一种容器(例如 `std::vector`),学习其他容器(例如 `std::array`)就会简单得多。对于未来的容器(例如 `std::array`),我们将介绍显著的区别(并重申最重要的要点)。

作者注

关于术语的快速说明

  • 当我们谈论适用于大多数或所有标准库容器类的事物时,我们将使用**容器类**。
  • 当我们谈论通常适用于所有数组类型,甚至在其他编程语言中实现的数组类型时,我们将使用**数组**。

`std::vector` 属于这两个类别,因此即使我们使用不同的术语,它仍然适用于 `std::vector`。

好了,准备好了吗?

我们走吧!

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
51 评论
最新
最早 最多投票
内联反馈
查看所有评论