章节目录

容器和数组简介

本节阅读量:

变量个数不定的挑战

考虑一个场景,其中要记录30名学生的考试成绩,并计算班级的平均分数。为此,需要30个变量。可以这样定义:

1
2
3
4
5
6
// 分配 30 个 int 变量 (每个都是单独的名字)
int testScore1 {};
int testScore2 {};
int testScore3 {};
// ...
int testScore30 {};

这需要定义许多变量!为了计算班级的平均分数,需要这样做:

1
2
3
4
5
6
7
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个变量。想想有成百上千个物体的情况。当需要多个相同类型的对象时,定义单个变量根本无法可能处理这种情况。

我们可以将数据放在结构体中:

1
2
3
4
5
6
7
8
9
struct testScores
{
// 设置 30 个 int 变量 (每一个名称都不同)
int score1 {};
int score2 {};
int score3 {};
// ...
int score30 {};
}

虽然这提供了一些额外的组织(并允许更容易地将它们传递给函数),但并不能解决核心问题:仍然需要单独定义和访问每个score对象。

正如您可能已经猜到的,C++有解决上述挑战的方案。在本章中,将介绍一种这样的解决方案。在接下来的章节中,将探索该解决方案的一些其他变体。


容器

当你去杂货店买一打鸡蛋时,(可能)并没有单独挑选12个鸡蛋并将它们放在你的购物车里。相反,可能会选择一盒鸡蛋。盒子是一种容器,容纳一些预定义数量的鸡蛋(可能是6个、12个或24个)。现在考虑早餐麦片,含有许多小块麦片。你肯定不想把这些东西单独存放在你的餐具室里!麦片通常装在盒子里,这是另一个容器。在现实生活中我们一直使用容器,因为它们使管理项目物品变得容易。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <string>

int main()
{
    std::string name{ "Alex" }; // 字符串 是 字母 的容器
    std::cout << name; // 将字符串按字母打印

    return 0;
}

容器中的元素未命名

虽然容器本身通常具有名称(否则我们将如何使用它?),但容器中的元素是未命名的。这样,可以根据需要在容器中放置任意多的元素,而不必为每个元素指定唯一的名称!缺少命名元素很重要,这也是容器与其他类型的数据结构的区别。这就是为什么普通结构体(那些只是数据成员的集合,如上面的testScores)通常不被视为容器的原因——它们的数据成员需要唯一的名称。

在上面的示例中,字符串容器有一个名称,但容器内的字符(‘a’,’l’,’e’,‘x’)没有。

但如果元素本身是未命名的,如何访问它们?每个容器都提供一个或多个方法来访问其元素——但具体方式取决于容器的类型。将在下一课中看到这方面的第一个例子。


容器的长度

在编程中,容器中元素的数量通常称为它的长度(有时也称 大小)。

在前面,我们展示了如何使用std::string的length成员函数来获取字符串容器中字符元素的数量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <string>

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

    return 0;
}

这将打印:

1
Alex has 4 characters

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

当提到容器中的元素数量时,优先使用术语“长度”,并使用术语“大小”来表示对象所需的存储量。


容器相关操作

回到上面鸡蛋纸箱上一会儿。你能用这样一个纸箱做什么?嗯,首先你可以买一盒鸡蛋。可以打开鸡蛋盒,选择一个鸡蛋,然后对这个鸡蛋做任何你想做的事情。您可以从纸箱中移除现有的鸡蛋,或将新鸡蛋添加到空白区域。你也可以数一数纸箱里的鸡蛋数量。

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

  1. 创建一个容器(例如,空的容器,或包含一些初始数量的元素)。
  2. 访问元素(例如,获取第一个元素、获取最后一个元素、获得指定元素)。
  3. 插入和删除元素。
  4. 获取容器中的元素数。

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

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

每个容器都有一组优势和限制。为您试图解决的任务选择正确的容器类型会对代码可维护性和总体性能产生巨大影响。我们将在以后的课程中进一步讨论这个主题。


元素类型

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

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

这使得容器变得灵活,因为我们不需要为希望保存的每个元素类型创建新的容器类。相反,只需用所需的元素类型实例化类模板,就可以开始了。


C++中的容器

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

在C++中,“容器”的定义比一般编程语言定义窄。在C++中,只有容器库中的类类型被认为是容器。当我们通常谈论容器时,将使用术语“容器”,当具体谈论作为容器库一部分的容器类类型时,使用术语“容器类”。

在提供的容器类中,std::vectors是最常见的,并且将是我们关注的焦点。其他容器类通常仅在更专门的情况下使用。


数组简介

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

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

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

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

在C++11中引入了std::array容器类,作为C样式数组的直接替代。它比std::vector功能有限,但性能更好,特别是对于较小的数组。

所有这些数组类型在现代C++中仍然可以同时使用,因此我们将在不同的程度上涵盖这三种类型。


下节前瞻

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

一件好事是,所有容器类都具有相似的接口。因此,一旦您学会了如何使用一个容器(例如,std::vector),学习其他容器(例如std::array)就简单得多。对于未来的容器(例如,std::array),我们将讨论显著的差异(并重申最重要的几点)。

好了,准备好了吗?

让我们一起加油加油加油加油。


15.10 第15章总结

上一节

16.1 std::vector和列表构造函数简介

下一节