动态数组
在本课中,我们将学习如何使用 C++ 中的动态数组,即 std::vector
。
关于 vector 的更多信息
您可能想知道为什么在 C++ 中表示动态数组数据结构的类型使用“vector”这个名称。对于那些将该术语与数学向量概念联系起来的人来说,这可能会令人困惑。有趣的是,这个名称是由 STL(标准模板库)的原始作者 Alexander Stepanov 选择的。然而,事后看来,Stepanov 承认将“vector”用作数据结构的名称是一个错误。
vector
是标准库中众多容器之一。您将在课程中逐步了解其他容器。在本课中,我将经常互换使用数组和vector这两个术语。
完成本课程后,如果您想了解更多关于 vector
的信息,可以查看文档。请注意,文档不是教程,而是参考资料,一开始可能会有点让人不知所措。
创建 vector
让我们回到上一课的动机部分中展示的代码。它是一个很好的候选对象,可以转换为数组。以下变量的类型相同(std::string
),只在名称末尾的数字不同。
std::string player_name1;
std::string player_name2;
std::string player_name3;
我们可以创建一个包含三个元素的数组,而不是创建三个单独的变量。
要使用 std::vector
,我们首先必须包含其头文件
#include <vector>
正如我们从简介中了解到的,要创建一个 vector
,我们必须知道其中将存储的元素的类型。vector 中的所有元素都具有相同的类型。
#include <iostream>
#include <string> // do not forget about string
#include <vector>
int main()
{
std::vector<std::string> player_names(3);
// ...
}
上面的示例展示了如何创建存储 std::string
的 vector。
在我们继续之前,让我们在 main
函数块的开头放置一个适当的using
语句,以跳过 std::
前缀。
int main() {
using std::vector, std::string;
vector<string> player_names(3);
}
稍后我们将使用标准库中的更多元素,例如 std::cout
和 std::cin
。您也可以将这些添加到 using
语句中。
“ vector 是一个模板,这意味着它可以与不同的类型一起使用。我们通过将其放在 vector
后面的尖括号中来告诉它使用哪种类型。就像这样
vector< int > array_of_ints;
vector< float > array_of_floats;
vector< char > array_of_chars;
vector< string > array_of_strings;
vector< /*other type*/ > array_of_xyz;
此代码定义了存储文本元素(std::string
)的向量 player_names
。
vector<string> player_names(3);
在 player_names 后面写 (3) 使得它在创建时就有足够的空间来存储三个 string
类型的元素。
player_names(3)
请注意,这是 vector 特有的,不是通用规则。如果你想创建一个初始为空的 vector,只需完全不写括号即可
vector<string> player_names;
初学者常犯的一个错误是在创建 vector(或实际上任何其他类型)时写空括号
vector< string > player_names();
在课程的后面,您将学习函数以及声明它们的语法。
// prism-push-types:AnyType
AnyType function_name();
这就是为什么空括号将其变成了函数声明,这不是我们想要的。
访问元素
有两种我们感兴趣的访问 vector 元素的方法:
两种方式非常相似,唯一的区别是 []
运算符不检查索引是否越界。我们稍后会向您展示这意味着什么。现在让我们看看如何使用它们 - 我们将以下名称分配给玩家
玩家索引 | 名称 |
---|---|
0 | 快乐香蕉 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
我们可以在代码中这样设置它们:
#include <iostream>
#include <string>
#include <vector>
int main()
{
using std::vector, std::string, std::cout;
vector<string> player_names(3);
// Setting names of the players
player_names[0] = "HappyBanana";
player_names[1] = "AngryCrab";
player_names[2] = "SadWolf";
// Printing the name of the first player:
cout << "Name of the first player: " << player_names[0];
}
Name of the first player: HappyBanana
要访问数组的元素,我们将其索引放在数组名称后面的方括号中。
arrayName[ index ]
一个非空数组,元素数量为 N
,其索引范围始终为 0
到 N-1
(包含)。三元素数组 player_names
的索引为 0
、1
和 2
。
越界访问
一个非常常见的错误是尝试访问索引越界的元素。如果我们试图重命名索引为 3
的玩家,程序很可能会崩溃。
player_names[3] = "NewPlayer"; // !
这段代码将正确编译(我们可能会收到警告),但当程序执行时,它将尝试访问未为程序分配的内存。这被称为缓冲区溢出,这是一个非常严重的问题。
另一种更安全的访问元素的方法是使用 at()
方法。
player_names.at( 3 ) = "NewPlayer";
不同之处在于 at()
方法会检查索引是否越界,如果越界则抛出异常。请注意,它本身并不会使您的程序有效。主要好处是它会显示有用的错误消息,并且不允许程序执行潜在危险的操作。
我们稍后将学习更多关于用于调用 at()
方法的语法。
示例
我们可以这样要求用户输入某个玩家的名字:
cout << "Enter the name of the second player: ";
cin >> player_names[1];
现在让我们输出所提供名称中的字符数。
cout << "The name of the second player has " << player_names[1].size() << " characters.\n";
Enter the name of the second player: FuriousFlamingo
The name of the second player has 15 characters.
提供初始值
还有一种方法可以从一开始就为 vector
的元素提供初始值
vector<string> player_names = {
"HappyBanana",
"AngryCrab",
"SadWolf",
};
替代语法
在初始化 vector
的情况下,你也可以省略 =
符号。
vector<string> player_names { // note the lack of '='
"HappyBanana",
"AngryCrab",
"SadWolf",
};
结果是一样的。
从现在开始,我们将在本课中使用这种初始化方法。
在末尾添加元素
要向数组末尾添加项目(在本例中为 player_names
),我们需要像这样使用 push_back
player_names.push_back("WickedWitch");
// Printing the name of player with index 3
cout << "Name of the player with index 3: " << player_names[3];
Name of the player with index 3: WickedWitch
从执行 .push_back(...)
指令的那一刻起,player_names
数组就已经有四个元素了。我们称之为调用 push_back(...)
方法。我们将在未来更多地讨论调用和方法。目前,只需记住:
- 我们在数组名称后面加一个点
- 我们写上方法的名称,在本例中是
push_back
- 然后,在括号中输入我们想要添加的内容(例如值或变量)。
请注意,添加新元素后,数组有四个元素,索引分别为 0
、1
、2
和 3
。
之前
索引 | 名称 |
---|---|
0 | 快乐香蕉 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
之后
索引 | 名称 |
---|---|
0 | 快乐香蕉 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
3 | 邪恶女巫 |
为了让用户添加新玩家,我们可以让他们输入名字,然后将其添加到数组中。
std::cout << "Enter the name of the new player: ";
std::string new_player_name;
std::cin >> new_player_name;
player_names.push_back(new_player_name);
在特定位置插入元素
在这一点上,您必须稍微相信我。我现在不会深入细节,因为它太复杂了。要在 vector(本例中为 player_names
)的索引 n
之前插入,我们使用以下符号:
player_names.insert(player_names.begin() + n, elementToInsert);
简单来说,insert
在指定位置之前添加一个元素。
begin()
并将索引添加到它上面。
知道了这一点,我们现在将在 AngryCrab
(索引 1
)之前插入一个新玩家。
player_names.insert(player_names.begin() + 1, "BadPenguin");
这是插入后数组的变化情况
之前
索引 | 名称 |
---|---|
0 | 快乐香蕉 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
3 | 邪恶女巫 |
之后
索引 | 名称 |
---|---|
0 | 快乐香蕉 |
1 | 坏企鹅 |
2 | 愤怒螃蟹 |
3 | 悲伤狼 |
4 | 邪恶女巫 |
移除元素
这与插入元素的情况类似。我们再次需要指定一个位置。因此,要从 vector(例如从 player_names
)中移除第 n
个元素,我们将使用以下符号:
player_names.erase( player_names.begin() + n );
我们现在将从数组中删除第一个玩家。
player_names.erase( player_names.begin() + 0 );
请注意,在这种情况下,+ 0
实际上什么也没做,但我把它放在那里是为了更清楚:player_names.begin()
等同于 player_names.begin() + 0
。
删除前后的数组内容
之前
索引 | 名称 |
---|---|
0 | 快乐香蕉 <-- 被删除 |
1 | 坏企鹅 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
3 | 邪恶女巫 |
之后
索引 | 名称 |
---|---|
0 | 坏企鹅 |
1 | 愤怒螃蟹 |
2 | 悲伤狼 |
3 | 邪恶女巫 |
正如您所看到的,第一个元素被删除,其余元素向后移动了一个索引。这是因为数组是内存中的连续块,vector
确保删除元素后不会留下空白空间。
在从数组中删除项目之前,请确保它存在(即在范围 [0, N)
中)。
int index;
cin >> index; // don't forget to add using std::cin;
if (index >= 0 && index < player_names.size())
{
player_names.erase( player_names.begin() + index );
}
else
cout << "Index " << index << " does not exist!";
清空内容
要清除 vector 的内容,我们可以使用 clear()
方法
player_names.clear();
调用后,数组将变空。
读取大小
因为 vector
可以随时更改大小,所以您有时可能需要读取它当前包含的元素数量。可以使用 size()
方法读取当前大小。
vector<string> player_names = {
"HappyBanana",
"AngryCrab",
"SadWolf",
};
cout << "The array contains " << player_names.size() << " elements\n";
// Adding a new player:
player_names.push_back("WickedWitch");
cout << "Added new player.\n";
cout << "The array contains " << player_names.size() << " elements\n";
The array contains 3 elements
Added new player.
The array contains 4 elements
就像我们之前对 push_back
方法所做的那样,我们在点号后面写入名称。然后我们加上括号,在 .size
的情况下,我们让它们为空。
检查 vector 是否为空
要检查 vector 是否为空,我们可以像这样使用 empty()
方法
if (player_names.empty())
cout << "The array is empty!\n";
else
cout << "The array is not empty!\n";
}
显示元素
如果我们要显示数组中的所有元素,就必须使用循环。我们以后会更多地谈论循环。它们允许您多次执行同一段代码。
for (string name : player_names)
{
cout << "Player name: " << name << '\n';
}
Player name: HappyBanana
Player name: AngryCrab
Player name: SadWolf
Player name: WickedWitch
为了理解这一点,让我向你展示如何“阅读”它
对于 (
for
) 数组player_names
中类型为std::string
的每个玩家名称 (name
) 执行以下代码块...
此代码块中只有一条语句
cout << "Player name: " << name << '\n';
循环会将玩家的昵称逐一写入变量 name
,并为每个昵称执行显示指令 (cout
)。
总结
我们学习了如何在 std::vector
类型上执行基本操作。请务必查看子课程,因为还有很多需要练习的地方。如果您展开“动态数组”项,可以在侧边栏中找到它们。