章节目录

容器和数组简介

本节阅读量:

变量个数不定的挑战

考虑一个场景:要记录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::vector是最常见的,也将是我们关注的焦点。其他容器类通常只在更专门的场景中使用。


数组简介

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

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和列表构造函数简介

下一节