文章目录
- 一、数组
- 二、STL的array
- 三、STL的vector
- 创建vector容器的几种方式
- vector容器包含的成员函数
- C++ STL vector容器迭代器用法详解
- vector容器迭代器的基本用法
- vector容器迭代器的独特之处
- C++ STL vector容器访问元素的几种方式
- 访问vector容器中单个元素
- 访问vector容器中多个元素
- C++ vector容量(capacity)和大小(size)的区别
- 修改vector容器的容量和大小
- vector容器容量和大小的数据类型
- 深度剖析C++ vector容器的底层实现机制
- vector扩大容量的本质
- C++ STL vector添加元素(push_back()和emplace_back())详解
- push_back()
- emplace_back()
- emplace_back()和push_back()的区别
- C++ STL vector插入元素(insert()和emplace())详解
- insert()
- emplace()
- C++ STL vector删除元素的几种方式(超级详细)
- 如何避免vector容器进行不必要的扩容?
- 四、三者之间的区别
一、数组
什么是数组呢?顾名思义数组就是很多数的组合!那么这些数有没有什么要求呢,是不是不管什么数组合在一起都是数组呢?同一数组中存储的数必须满足以下两个条件:
- 这些数的类型必须相同。
- 这些数在内存中必须是连续存储的。
换句话说,数组是在内存中连续存储的具有相同类型的一组数据的集合。
一维数组
一维数组的定义方式如下:
类型说明符 数组名[常量表达式];
例如:
int a[5];
它表示定义了一个整型数组,数组名为 a,定义的数组称为数组 a。数组名 a 除了表示该数组之外,还表示该数组的首地址(关于地址现在先不讨论,稍后讲指针的时候再说)。
此时数组 a 中有 5 个元素,每个元素都是 int 型变量,而且它们在内存中的地址是连续分配的。也就是说,int 型变量占 4 字节的内存空间,那么 5 个int型变量就占 20 字节的内存空间,而且它们的地址是连续分配的。
这里的元素就是变量的意思,数组中习惯上称为元素。
在定义数组时,需要指定数组中元素的个数。方括号中的常量表达式就是用来指定元素的个数。数组中元素的个数又称数组的长度。
数组中既然有多个元素,那么如何区分这些元素呢?方法是通过给每个元素进行编号。数组元素的编号又叫下标。
数组中的下标是从 0 开始的(而不是 1)。那么,如何通过下标表示每个数组元素的呢?通过“数组名[下标]”的方式。例如“int a[5];”表示定义了有 5 个元素的数组 a,这 5 个元素分别为 a[0]、a[1]、a[2]、a[3]、a[4]。其中 a[0]、a[1]、a[2]、a[3]、a[4] 分别表示这 5 个元素的变量名。
为什么下标是从 0 开始而不是从 1 开始呢?试想,如果从 1 开始,那么数组的第 5 个元素就是 a[5],而定义数组时是 int a[5],两个都是 a[5] 就容易产生混淆。而下标从 0 开始就不存在这个问题了!所以定义一个数组 a[n],那么这个数组中元素最大的下标是 n–1;而元素 a[i] 表示数组 a 中第 i+1 个元素。
另外,方括号中的常量表达式可以是“数字常量表达式”,也可以是“符号常量表达式”。但不管是什么表达式,必须是常量,绝对不能是变量。通常情况下 C 语言不允许对数组的长度进行动态定义,换句话说,数组的大小不依赖程序运行过程中变量的值。非通常的情况为动态内存分配,此种情况下数组的长度就可以动态定义,这个稍后会讲。
一维数组初始化
一维数组的初始化可以使用以下方法实现:
- 定义数组时给所有元素赋初值,这叫“完全初始化”。例如:
int a[5] = {1, 2, 3, 4, 5};
通过将数组元素的初值依次放在一对花括号中,如此初始化之后,a[0]=1;a[1]=2;a[2]=3;a[3]=4;a[4]=5,即从左到右依次赋给每个元素。需要注意的是,初始化时各元素间是用逗号隔开的,不是用分号。
- 可以只给一部分元素赋值,这叫“不完全初始化”。例如:
int a[5] = {1, 2};
定义的数组 a 有 5 个元素,但花括号内只提供两个初值,这表示只给前面两个元素 a[0]、a[1] 初始化,而后面三个元素都没有被初始化。不完全初始化时,没有被初始化的元素自动为 0。
需要注意的是,“不完全初始化”和“完全不初始化”不一样。如果“完全不初始化”,即只定义“int a[5];”而不初始化,那么各个元素的值就不是0了,所有元素都是垃圾值。
你也不能写成“int a[5]={};”。如果大括号中什么都不写,那就是极其严重的语法错误。大括号中最少要写一个数。比如“int a[5]={0};”,这时就是给数组“清零”,此时数组中每个元素都是零。此外,如果定义的数组的长度比花括号中所提供的初值的个数少,也是语法错误,如“a[2]={1,2,3,4,5};”。
- 如果定义数组时就给数组中所有元素赋初值,那么就可以不指定数组的长度,因为此时元素的个数已经确定了。编程时我们经常都会使用这种写法,因为方便,既不会出问题,也不用自己计算有几个元素,系统会自动分配空间。例如:
int a[5] = {1, 2, 3, 4, 5};
可以写成:
int a[] = {1, 2, 3, 4, 5};
第二种写法的花括号中有 5 个数,所以系统会自动定义数组 a 的长度为 5。但是要注意,只有在定义数组时就初始化才可以这样写。如果定义数组时不初始化,那么省略数组长度就是语法错误。比如:
int a[];
那么编译时就会提示错误,编译器会提示你没有指定数组的长度。
下面给大家写一个简单的程序:
# include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int i;
for (i=0; i<5; ++i)
{
printf("%d\n", a[i]);
}
return 0;
}
输出结果是:
1
2
3
4
5
a 表示数组的名字,[5] 表示这个数组有 5 个元素,并分别用 a[0]、a[1]、a[2]、a[3]、a[4] 表示。并分别把花括号内的 1、2、3、4、5 赋给变量 a[0]、a[1]、a[2]、a[3]、a[4]。再次强调,下标从 0 开始,即从 a[0] 开始,而不是 a[1]。
也可以用 scanf 手动从键盘对数组进行初始化:
# include <stdio.h>
int main(void)
{
int a[5] = {0}; //数组清零初始化
int i;
printf("请输入5个数:");
for (i=0; i<5; ++i)
{
scanf("%d", &a[i] );
}
for (i=0; i<5; ++i)
{
printf("%d\x20", a[i]);
}
printf("\n");
return 0;
}
输出结果是:
请输入5个数:1 2 3 4 5
1 2 3 4 5
同使用 scanf 给字符数组输入字符串时有所不同,输入数字时必须用 for 循环进行输入。而输入字符串时无须用循环,直接用 scanf 就可以了。
一维数组元素的引用
数组必须先定义,然后使用。C 语言规定,只能逐个引用数组元素,而不能一次引用整个数组。前面讲过,数组元素的表示形式为:
数组名[下标]
下标可以是整型常量或整型表达式,比如:
a[0] = a[5] + a[7] - a[2 * 3]
千万要注意,定义数组时用到的“数组名[常量表达式]”和引用数组元素时用到的“数组名[下标]”是有区别的,定义数组时的常量表达式表示的是数组的长度,而引用数组元素时的下标表示的是元素的编号。比如:
# include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5}; //定义长度为5的数组a
int t;
t = a[3]; /*引用数组a中下标为3的元素a[3], 此时的3不代表数组的长度*/
printf("t = %d\n", t);
return 0;
}
输出结果是:
t = 4
“int a[5];”是定义了有 5 个元素的数组,这 5 个元素分别为 a[0]、a[1]、a[2]、a[3]、a[4]。而 t=a[3] 中的 a[3] 不是数组,只是其中的元素 a[3]。
因此,下面这个程序是错的:
# include <stdio.h>
int main(void)
{
int a[5];
a[5] = {1, 2, 3, 4, 5};
return 0;
}
错误的原因是下面的 a[5] 不是数组。只有在定义的时候“a[常量]”表示的才是数组,此时方括号中的数字才表示数组长度。除此之外程序中任何地方看到“a[常量]”都不是数组,都只是数组的一个元素、一个变量,此时的“常量”表示的是元素的下标。
此外,当给元素单独赋值时不能加大括号,因为元素就是变量,即 a[5] 只是一个变量名。前面是怎么给变量赋值的现在就怎么给数组元素赋值,比如“a[5]=1;”。但是对于上面这个程序,这么写还是错误的。因为数组元素的下标是从 0 开始的,数组 a 的元素只有 a[0]~a[4],并没有 a[5] 这个元素。
二、STL的array
array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。
和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。
STL 还提供有可动态扩展或收缩存储空间的 vector 容器,后续章节会对其做详细介绍。
array 容器以类模板的形式定义在 头文件,并位于命名空间 std 中,如下所示:
namespace std{
template <typename T, size_t N>
class array;
}
因此,在使用该容器之前,代码中需引入 头文件,并默认使用 std 命令空间,如下所示:
#include <array>
using namespace std;
在 array<T,N> 类模板中,T 用于指明容器中的存储的具体数据类型,N 用于指明容器的大小,需要注意的是,这里的 N 必须是常量,不能用变量表示。
array 容器有多种初始化方式,如下代码展示了如何创建具有 10 个 double 类型元素的 array 容器:
std::array<double, 10> values;
提示,如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
由此,就创建好了一个名为 values 的 array 容器,其包含 10 个浮点型元素。但是,由于未显式指定这 10 个元素的值,因此使用这种方式创建的容器中,各个元素的值是不确定的(array 容器不会做默认初始化操作)。
通过如下创建 array 容器的方式,可以将所有的元素初始化为 0 或者和默认元素类型等效的值:
std::array<double, 10> values {};
使用该语句,容器中所有的元素都会被初始化为 0.0。
当然,在创建 array 容器的实例时,也可以像创建常规数组那样对元素进行初始化:
std::array<double, 10> values {0.5,1.0,1.5,,2.0};
可以看到,这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0。图 1 说明了这一点。
图 1 初始化 array 容器
除此之外,array 容器还提供有很多功能实用的成员函数,如表 2 所示。
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的随机访问迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的随机访问迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的随机访问迭代器。 |
rend() | 返回指向第一个元素之前一个位置的随机访问迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回容器中当前元素的数量,其值始终等于初始化 array 类的第二个模板参数 N。 |
max_size() | 返回容器可容纳元素的最大数量,其值始终等于初始化 array 类的第二个模板参数 N。 |
empty() | 判断容器是否为空,和通过 size()==0 的判断条件功能相同,但其效率可能更快。 |
at(n) | 返回容器中 n 位置处元素的引用,该函数自动检查 n 是否在有效的范围内,如果不是则抛出 out_of_range 异常。 |
front() | 返回容器中第一个元素的直接引用,该函数不适用于空的 array 容器。 |
back() | 返回容器中最后一个元素的直接应用,该函数同样不适用于空的 array 容器。 |
data() | 返回一个指向容器首个元素的指针。利用该指针,可实现复制容器中所有元素等类似功能。 |
fill(val) | 将 val 这个值赋值给容器中的每个元素。 |
array1.swap(array2) | 交换 array1 和 array2 容器中的所有元素,但前提是它们具有相同的长度和类型。 |
除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
另外,在 头文件中还重载了 get() 全局函数,该重载函数的功能是访问容器中指定的元素,并返回该元素的引用。
正是由于 array 容器中包含了 at() 这样的成员函数,使得操作元素时比普通数组更安全。
例如代码演示了表 2 中一部分成员函数的用法和功能:
#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
std::array<int, 4> values{};
//初始化 values 容器为 {0,1,2,3}
for (int i = 0; i < values.size(); i++) {
values.at(i) = i;
}
//使用 get() 重载函数输出指定位置元素
cout << get<3>(values) << endl;
//如果容器不为空,则输出容器中所有的元素
if (!values.empty()) {
for (auto val = values.begin(); val < values.end(); val++) {
cout << *val << " ";
}
}
}
注意,代码中的 auto 关键字,可以使编译器自动判定变量的类型。运行这段代码,输出结果为:
3
0 1 2 3
表 2 中其他成员函数的用法,这里不再给出具体实例,有兴趣的读者,可自行根据各个函数的功能描述编写实例代码进行测试。
当然array中的元素除了可以是 char 、int、double等基本类型,也可以是结构体等复合类型。
#include <iostream>
#include <array>
using namespace std;
struct Test {
int a;
char b;
};
int main(void) {
array<Test,5> test;
test[0].a = 1;
test[1].b= 2;
cout << test[0].a<<endl;
}
C++ STL array随机访问迭代器(精讲版)
在《C++ STL迭代器(iterator)》一节中,已经对迭代器做了详细的介绍,STL 为 array 容器配备了随机访问迭代器,该类迭代器是功能最强大的迭代器。本节将详细介绍 array 容器的迭代器的用法。
在 array 容器的模板类中,和随机访问迭代器相关的成员函数如表 1 所示。
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。 |
rbegin() | 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。 |
rend() | 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。 |
cbegin() | 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
除此之外,C++ 11 标准新增的 begin() 和 end() 函数,当操作对象为 array 容器时,也和迭代器有关,其功能分别和表 1 中的 begin()、end() 成员函数相同,具有用法本节后续会做详细介绍。
这些成员函数的具体功能如图 2 所示。
图 2 迭代器的具体功能示意图
可以看到,根据它们的功能并结合实际场景的需要,这些成员函数通常是成对使用的,即 begin()/end()、rbegin()/rend()、cbegin()/cend()、crbegin()/crend() 各自成对搭配使用。不仅如此,这 4 对中 begin()/end() 和 cbegin()/cend()、rbegin()/rend() 和 crbegin()/crend() 的功能大致是相同的(如图 2 所示),唯一的区别就在于其返回的迭代器能否用来修改元素值。
值得一提的是,以上函数在实际使用时,其返回值类型都可以使用 auto 关键字代替,编译器可以自行判断出该迭代器的类型。
begin()/end() 和 cbegin()/cend()
array 容器模板类中的 begin() 和 end() 成员函数返回的都是正向迭代器,它们分别指向「首元素」和「尾元素+1」 的位置。在实际使用时,我们可以利用它们实现初始化容器或者遍历容器中元素的操作。
例如,可以在循环中显式地使用迭代器来初始化 values 容器的值:
#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
array<int, 5>values;
int h = 1;
auto first = values.begin();
auto last = values.end();
//初始化 values 容器为{1,2,3,4,5}
while (first != last)
{
*first = h;
++first;
h++;
}
first = values.begin();
while (first != last)
{
cout << *first << " ";
++first;
}
return 0;
}
输出结果为:
1 2 3 4 5
可以看出,迭代器对象是由 array 对象的成员函数 begin() 和 end() 返回的。我们可以像使用普通指针那样上使用迭代器对象。比如代码中,在保存了元素值后,使用前缀 ++ 运算符对 first 进行自增,当 first 等于 end 时,所有的元素都被设完值,循环结束。
与此同时,还可以使用全局的 begin() 和 end() 函数来从容器中获取迭代器,因为当操作对象为 array 容器时,它们和 begin()/end() 成员函数是通用的。所以上面代码中,first 和 last 还可以像下面这样定义:
auto first = std::begin(values);
auto last = std::end (values);
这样,容器中的一段元素可以由迭代器指定,这让我们有了对它们使用算法的可能。
需要注意的是,STL 标准库,不是只有 array 容器,当迭代器指向容器中的一个特定元素时,它们不会保留任何关于容器本身的信息,所以我们无法从迭代器中判断,它是指向 array 容器还是指向 vector 容器(该容器后续会讲)。
除此之外,array 模板类还提供了 cbegin() 和 cend() 成员函数,它们和 begin()/end() 唯一不同的是,前者返回的是 const 类型的正向迭代器,这就意味着,有 cbegin() 和 cend() 成员函数返回的迭代器,可以用来遍历容器内的元素,也可以访问元素,但是不能对所存储的元素进行修改。
举个例子:
#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
array<int, 5>values{1,2,3,4,5};
int h = 1;
auto first = values.cbegin();
auto last = values.cend();
//由于 *first 为 const 类型,不能用来修改元素
//*first = 10;
//遍历容器并输出容器中所有元素
while (first != last)
{
//可以使用 const 类型迭代器访问元素
cout << *first << " ";
++first;
}
return 0;
}
此程序的第 14 行代码中,我们尝试使用 first 迭代器修改 values 容器中的值,如果取消注释并运行此程序,编译器会提示你“不能给常量赋值”,即 *first 是 const 类型常量,所以这么做是不对的。但 17~22 行代码遍历并访问容器的行为,是允许的。
rbegin()/rend() 和 crbegin()/crend()
array 模板类中还提供了 rbegin()/rend() 和 crbegin()/crend() 成员函数,它们每对都可以分别得到指向最一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器(如图 2 所示)。
需要注意的是,在使用反向迭代器进行 ++ 或 – 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。
反向迭代器用于以逆序的方式处理元素。例如:
#include <iostream>
//需要引入 array 头文件
#include <array>
using namespace std;
int main()
{
array<int, 5>values;
int h = 1;
auto first = values.rbegin();
auto last = values.rend();
//初始化 values 容器为 {5,4,3,2,1}
while (first != last)
{
*first = h;
++first;
h++;
}
//重新遍历容器,并输入各个元素
first = values.rbegin();
while (first != last)
{
cout << *first << " ";
++first;
}
return 0;
}
运行结果为:
1 2 3 4 5
可以看到,从最后一个元素开始循环,不仅完成了容器的初始化,还遍历输出了容器中的所有元素。结束迭代器指向第一个元素之前的位置,所以当 first 指向第一个元素并 +1 后,循环就结朿了。
在反向迭代器上使用 ++ 递增运算符,会让迭代器用一种和普通正向迭代器移动方向相反的方式移动。
当然,在上面程序中,我们也可以使用 for 循环:
for (auto first = values.rbegin(); first != values.rend(); ++first) {
cout << *first << " ";
}
crbegin()/crend() 组合和 rbegin()/crend() 组合的功能唯一的区别在于,前者返回的迭代器为 const 类型,即不能用来修改容器中的元素,除此之外在使用上和后者完全相同。
有关 crbegin()/crend() 成员函数,这里不再给出具体实例,有兴趣的读者,可自行编写代码进行测试。
C++ STL array容器访问元素的几种方式
当 array 容器创建完成之后,最常做的操作就是获取其中的元素,甚至有时还会通过循环结构获取多个元素。本节就对获取容器中元素的方法做个汇总。
访问array容器中单个元素
首先,可以通过容器名[]
的方式直接访问和使用容器中的元素,这和 C++ 标准数组访问元素的方式相同,例如:
values[4] = values[3] + 2.O*values[1];
此行代码中,第 5 个元素的值被赋值为右边表达式的值。需要注意的是,使用如上这样方式,由于没有做任何边界检查,所以即便使用越界的索引值去访问或存储元素,也不会被检测到。
为了能够有效地避免越界访问的情况,可以使用 array 容器提供的 at() 成员函数,例如 :
values.at (4) = values.at(3) + 2.O*values.at(1);
这行代码和前一行语句实现的功能相同,其次当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常。因此当需要访问容器中某个指定元素时,建议大家使用 at(),除非确定索引没有越界。
读者可能有这样一个疑问,即为什么 array 容器在重载 [] 运算符时,没有实现边界检查的功能呢?答案很简单,因为性能。如果每次访问元素,都去检查索引值,无疑会产生很多开销。当不存在越界访问的可能时,就能避免这种开销。
除此之外,array 容器还提供了 get 模板函数,它是一个辅助函数,能够获取到容器的第 n 个元素。需要注意的是,该模板函数中,参数的实参必须是一个在编译时可以确定的常量表达式,所以它不能是一个循环变量。也就是说,它只能访问模板参数指定的元素,编译器在编译时会对它进行检查。
下面代码展示了如何使用 get 模板函数:
#include <iostream>
#include <array>
#include <string>
using namespace std;
int main()
{
array<string, 5> words{ "one","two","three","four","five" };
cout << get<3>(words) << endl; // Output words[3]
//cout << get<6>(words) << std::endl; //越界,会发生编译错误
return 0;
}
运行结果为:
four
另外,array 容器提供了 data() 成员函数,通过调用该函数可以得到指向容器首个元素的指针。通过该指针,我们可以获得容器中的各个元素,例如:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<int, 5> words{1,2,3,4,5};
cout << *( words.data()+1);
return 0;
}
运行结果为:
2
访问array容器中多个元素
我们知道,array 容器提供的 size() 函数能够返回容器中元素的个数(函数返回值为 size_t 类型),所以能够像下面这样去逐个提取容器中的元素,并计算它们的和:
double total = 0;
for(size_t i = 0 ; i < values.size() ; ++i)
{
total += values[i];
}
size() 函数的存在,为 array 容器提供了标准数组所没有的优势,即能够知道它包含多少元素。
并且,接受数组容器作为参数的函数,只需要通过调用容器的成员函数 size(),就能得到元素的个数。除此之外,通过调用 array 容器的 empty() 成员函数,即可知道容器中有没有元素(如果容器中没有元素,此函数返回 true),如下所示:
if(values.empty())
std::cout << "The container has no elements.\n";
else
std::cout << "The container has "<< values.size()<<"elements.\n";
然而,很少会创建空的 array 容器,因为当生成一个 array 容器时,它的元素个数就固定了,而且无法改变,所以生成空 array 容器的唯一方法是将模板的第二个参数指定为 0,但这种情况基本不可能发生。
array 容器之所以提供 empty() 成员函数的原因,对于其他元素可变或者元素可删除的容器(例如 vector、deque 等)来说,它们使用 empty() 时的机制是一样的,因此为它们提供了一个一致性的操作。
除了借助 size() 外,对于任何可以使用迭代器的容器,都可以使用基于范围的循环,因此能够更加简便地计算容器中所有元素的和,比如:
double total = 0;
for(auto&& value : values)
total += value;
下面是一个示例,展示了本节关于如何获取 array 容器中元素所讲到的知识:
#include <iostream>
#include <iomanip>
#include <array>
using namespace std;
int main()
{
array<int, 5> values1;
array<int, 5> values2;
//初始化 values1 为 {0,1,2,3,4}
for (size_t i = 0; i < values1.size(); ++i)
{
values1.at(i) = i;
}
cout << "values1[0] is : " << values1[0] << endl;
cout << "values1[1] is : " << values1.at(1) << endl;
cout << "values1[2] is : " << get<2>(values1) << endl;
//初始化 values2 为{10,11,12,13,14}
int initvalue = 10;
for (auto& value : values2)
{
value = initvalue;
initvalue++;
}
cout << "Values1 is : ";
for (auto i = values1.begin(); i < values1.end(); i++) {
cout << *i << " ";
}
cout << endl << "Values2 is : ";
for (auto i = values2.begin(); i < values2.end(); i++) {
cout << *i << " ";
}
return 0;
}
运行结果为:
values1[0] is : 0
values1[1] is : 1
values1[2] is : 2
Values1 is : 0 1 2 3 4
Values2 is : 10 11 12 13 14
C++ array容器:普通数组的“升级版”
和 C++ 普通数组存储数据的方式一样,C++ 标准库保证使用 array 容器存储的所有元素一定会位于连续且相邻的内存中,通过如下代码也可以验证这一点:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<int, 5>a{1,2,3};
cout << &a[2] << " " << &a[0] + 2 << endl;
return 0;
}
输出结果为:
004FFD58 004FFD58
可以看到,a 容器中 &a[2] 和 &a[0] + 2 是相等的。因此在实际编程过程中,我们完全有理由去尝试,在原本使用普通数组的位置,改由 array 容器去实现。
用 array 容器替换普通数组的好处是,array 模板类中已经封装好了大量实用的方法,在提高开发效率的同时,代码的运行效率也会大幅提高。
举个例子,我们完全可以使用 array 容器去存储 char* 或 const char* 类型的字符串:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<char, 50>a{1,2,3};
strcpy(&a[0], "http://c.biancheng.net/stl");
printf("%s", &a[0]);
return 0;
}
输出结果为:
http://c.biancheng.net/stl
注意,array 容器的大小必须保证能够容纳复制进来的数据,而且如果是存储字符串的话,还要保证在存储整个字符串的同时,在其最后放置一个\0
作为字符串的结束符。此程序中,strcpy() 在拷贝字符串的同时,会自动在最后添加\0
。
其实,代码中的 &a[0] 还可以用 array 模板类提供的 data() 成员函数来替换:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<char, 50>a{1,2,3};
strcpy(a.data(), "http://c.biancheng.net/stl");
printf("%s", a.data());
return 0;
}
此程序和上面程序的输出结果完全相同。
注意,容器的迭代器和指针是不能混用的,即上面代码中不能用 a.begin() 来代替 &a[0] 或者 a.data[],这可能会引发错误。
文章前面提到,使用 array 容器代替普通数组,最直接的好处就是 array 模板类中已经为我们写好了很多实用的方法,可以大大提高我们编码效率。例如,array 容器提供的 at() 成员函数,可以有效防止越界操纵数组的情况;fill() 函数可以实现数组的快速初始化;swap() 函数可以轻松实现两个相同数组(类型相同,大小相同)中元素的互换。
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<char, 50>addr1{"http://c.biancheng.net"};
array<char, 50>addr2{ "http://c.biancheng.net/stl" };
addr1.swap(addr2);
printf("addr1 is:%s\n", addr1.data());
printf("addr2 is:%s\n", addr2.data());
return 0;
}
运行结果为:
addr1 is:http://c.biancheng.net/stl
addr2 is:http://c.biancheng.net
另外,当两个 array 容器满足大小相同并且保存元素的类型相同时,两个 array 容器可以直接直接做赋值操作,即将一个容器中的元素赋值给另一个容器。比如:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<char, 50>addr1{ "http://c.biancheng.net" };
array<char, 50>addr2{ "http://c.biancheng.net/stl" };
addr1 = addr2;
printf("%s", addr1.data());
return 0;
}
运行结果为:
http://c.biancheng.net/stl
不仅如此,在满足以上 2 个条件的基础上,如果其保存的元素也支持比较运算符,就可以用任何比较运算符直接比较两个 array 容器。示例如下:
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<char, 50>addr1{ "http://c.biancheng.net" };
array<char, 50>addr2{ "http://c.biancheng.net/stl" };
if (addr1 == addr2) {
std::cout << "addr1 == addr2" << std::endl;
}
if (addr1 < addr2) {
std::cout << "addr1 < addr2" << std::endl;
}
if (addr1 > addr2) {
std::cout << "addr1 > addr2" << std::endl;
}
return 0;
}
运行结果为:
addr1 < addr2
两个容器比较大小的原理,和两个字符串比较大小是一样的,即从头开始,逐个取两容器中的元素进行大小比较(根据 ASCII 码表),直到遇到两个不相同的元素,那个元素的值大,则该容器就大。
总之,读者可以这样认为,array 容器就是普通数组的“升级版”,使用普通数组能实现的,使用 array 容器都可以实现,而且无论是代码功能的实现效率,还是程序执行效率,都比普通数组更高。
三、STL的vector
vector 容器是 STL 中最常用的容器之一,它和 array 容器非常类似,都可以看做是对 C++ 普通数组的“升级版”。不同之处在于,array 实现的是静态数组(容量固定的数组),而 vector 实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector 会动态调整所占用的内存空间,整个过程无需人工干预。
vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1)
;而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)
。
有关复杂度,可阅读《大O表示法》一节详细了解。
vector 容器以类模板 vector( T 表示存储元素的类型)的形式定义在 头文件中,并位于 std 命名空间中。因此,在创建该容器之前,代码中需包含如下内容:
#include <vector>using namespace std;
注意,std 命名空间也可以在使用 vector 容器时额外注明,两种方式都可以。
创建vector容器的几种方式
创建 vector 容器的方式有很多,大致可分为以下几种。
- 如下代码展示了如何创建存储 double 类型元素的一个 vector 容器:
std::vector<double> values;
如果程序中已经默认指定了 std 命令空间,这里可以省略 std::。
注意,这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存。
在创建好空容器的基础上,还可以像下面这样通过调用 reserve() 成员函数来增加容器的容量:
values.reserve(20);
这样就设置了容器的内存分配,即至少可以容纳 20 个元素。注意,如果 vector 的容量在执行此语句之前,已经大于或等于 20 个元素,那么这条语句什么也不做;另外,调用 reserve() 不会影响已存储的元素,也不会生成任何元素,即 values 容器内此时仍然没有任何元素。
还需注意的是,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。所以后续再使用这些迭代器时,最好重新生成一下。
- 除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数,比如:
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};
这样就创建了一个含有 8 个素数的 vector 容器。
- 在创建 vector 容器时,也可以指定元素个数:
std::vector<double> values(20);
如此,values 容器开始时就有 20 个元素,它们的默认初始值都为 0。
注意,圆括号 () 和大括号 {} 是有区别的,前者(例如 (20) )表示元素的个数,而后者(例如 {20} ) 则表示 vector 容器中只有一个元素 20。
如果不想用 0 作为默认值,也可以指定一个其它值,例如:
std::vector<double> values(20, 1.0);
第二个参数指定了所有元素的初始值,因此这 20 个元素的值都是 1.0。
值得一提的是,圆括号 () 中的 2 个参数,既可以是常量,也可以用变量来表示,例如:
int num=20;
double value =1.0;
std::vector<double> values(num, value);
- 通过存储元素类型相同的其它 vector 容器,也可以创建新的 vector 容器,例如:
std::vector<char>value1(5, 'c');
std::vector<char>value2(value1);
由此,value2 容器中也具有 5 个字符 ‘c’。在此基础上,如果不想复制其它容器中所有的元素,可以用一对指针或者迭代器来指定初始值的范围,例如:
int array[]={1,2,3};
std::vector<int>values(array, array+2);//values 将保存{1,2}
std::vector<int>value1{1,2,3,4,5};
std::vector<int>value2(std::begin(value1),std::begin(value1)+3);//value2保存{1,2,3}
由此,value2 容器中就包含了 {1,2,3} 这 3 个元素。
vector容器包含的成员函数
相比 array 容器,vector 提供了更多了成员函数供我们使用,它们各自的功能如表 1 所示。
函数成员 | 函数功能 |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回实际元素个数。 |
max_size() | 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。 |
resize() | 改变实际元素的个数。 |
capacity() | 返回当前容量。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
reserve() | 增加容器的容量。 |
shrink _to_fit() | 将内存减少到等于当前元素实际所使用的大小。 |
operator[ ] | 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。 |
at() | 使用经过边界检查的索引访问元素。 |
front() | 返回第一个元素的引用。 |
back() | 返回最后一个元素的引用。 |
data() | 返回指向容器中第一个元素的指针。 |
assign() | 用新元素替换原有内容。 |
push_back() | 在序列的尾部添加一个元素。 |
pop_back() | 移出序列尾部的元素。 |
insert() | 在指定的位置插入一个或多个元素。 |
erase() | 移出一个元素或一段元素。 |
clear() | 移出所有的元素,容器大小变为 0。 |
swap() | 交换两个容器的所有元素。 |
emplace() | 在指定的位置直接生成一个元素。 |
emplace_back() | 在序列尾部生成一个元素。 |
除此之外,C++ 11 标准库还新增加了 begin() 和 end() 这 2 个函数,和 vector 容器包含的 begin() 和 end() 成员函数不同,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)。
vector 容器还有一个 std::swap(x , y) 非成员函数(其中 x 和 y 是存储相同类型元素的 vector 容器),它和 swap() 成员函数的功能完全相同,仅使用语法上有差异。
如下代码演示了表 1 中部分成员函数的用法:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
//初始化一个空vector容量
vector<char>value;
//向value容器中的尾部依次添加 S、T、L 字符
value.push_back('S');
value.push_back('T');
value.push_back('L');
//调用 size() 成员函数容器中的元素个数
printf("元素个数为:%d\n", value.size());
//使用迭代器遍历容器
for (auto i = value.begin(); i < value.end(); i++) {
cout << *i << " ";
}
cout << endl;
//向容器开头插入字符
value.insert(value.begin(), 'C');
cout << "首个元素为:" << value.at(0) << endl;
return 0;
}
输出结果为:
元素个数为:3
S T L
首个元素为:C
表 1 中这些成员函数的具体用法,后续学习用到时会具体讲解,感兴趣的读者,也可以通过查阅 STL手册做详细了解。
C++ STL vector容器迭代器用法详解
在《STL array随机访问迭代器》一节中,详细介绍了 array 容器迭代器,vector 容器迭代器和前者有很多相同之处。比如,vector 容器的迭代器也是随机访问迭代器,并且 vector 模板类提供的操作迭代器的成员函数也和 array 容器一样(如表 1 所示)。
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。 |
rbegin() | 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。 |
rend() | 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。 |
cbegin() | 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
除此之外,C++ 11 新添加的 begin() 和 end() 全局函数也同样适用于 vector 容器。即当操作对象为 vector 容器时,其功能分别和表 1 中的 begin()、end() 成员函数相同,具体用法本节后续会做详细介绍。
表 1 中这些成员函数的具体功能如图 2 所示。
图 2 迭代器的具体功能示意图
从图 2 可以看出,这些成员函数通常是成对使用的,即 begin()/end()、rbegin()/rend()、cbegin()/cend()、crbegin()/crend() 各自成对搭配使用。其中,begin()/end() 和 cbegin/cend() 的功能是类似的,同样 rbegin()/rend() 和 crbegin()/crend() 的功能是类似的。
值得一提的是,以上函数在实际使用时,其返回值类型都可以使用 auto 关键字代替,编译器可以自行判断出该迭代器的类型。
vector容器迭代器的基本用法
vector 容器迭代器最常用的功能就是遍历访问容器中存储的元素。
首先来看 begin() 和 end() 成员函数,它们分别用于指向「首元素」和「尾元素+1」 的位置,下面程序演示了如何使用 begin() 和 end() 遍历 vector 容器并输出其中的元素:
#include <iostream>
//需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
vector<int>values{1,2,3,4,5};
auto first = values.begin();
auto end = values.end();
while (first != end)
{
cout << *first << " ";
++first;
}
return 0;
}
输出结果为:
1 2 3 4 5
可以看到,迭代器对象是由 vector 对象的成员函数 begin() 和 end() 返回的。我们可以像使用普通指针那样上使用它们。比如代码中,在保存了元素值后,使用前缀++
运算符对 first 进行自增,当 first 等于 end 时,所有的元素都被设完值,循环结束。
与此同时,还可以使用全局的 begin() 和 end() 函数来从容器中获取迭代器,比如将上面代码中第 8、9 行代码用如下代码替换:
auto first = std::begin(values);
auto end = std::end (values);
cbegin()/cend() 成员函数和 begin()/end() 唯一不同的是,前者返回的是 const 类型的正向迭代器,这就意味着,由 cbegin() 和 cend() 成员函数返回的迭代器,可以用来遍历容器内的元素,也可以访问元素,但是不能对所存储的元素进行修改。
举个例子:
#include <iostream>
//需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
vector<int>values{1,2,3,4,5};
auto first = values.cbegin();
auto end = values.cend();
while (first != end)
{
//*first = 10;不能修改元素
cout << *first << " ";
++first;
}
return 0;
}
程序第 12 行,由于 first 是 const 类型的迭代器,因此不能用于修改容器中元素的值。
vector 模板类中还提供了 rbegin() 和 rend() 成员函数,分别表示指向最后一个元素和第一个元素前一个位置的随机访问迭代器,又称它们为反向迭代器(如图 2 所示)。
需要注意的是,在使用反向迭代器进行 ++ 或 – 运算时,++ 指的是迭代器向左移动一位,-- 指的是迭代器向右移动一位,即这两个运算符的功能也“互换”了。
反向迭代器用于以逆序的方式遍历容器中的元素。例如:
#include <iostream>
//需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
vector<int>values{1,2,3,4,5};
auto first = values.rbegin();
auto end = values.rend();
while (first != end)
{
cout << *first << " ";
++first;
}
return 0;
}
运行结果为:
5 4 3 2 1
可以看到,从最后一个元素开始循环,遍历输出了容器中的所有元素。结束迭代器指向第一个元素之前的位置,所以当 first 指向第一个元素并 +1 后,循环就结朿了。
当然,在上面程序中,我们也可以使用 for 循环:
for (auto first = values.rbegin(); first != values.rend(); ++first) {
cout << *first << " ";
}
crbegin()/crend() 组合和 rbegin()/crend() 组合唯一的区别在于,前者返回的迭代器为 const 类型,即不能用来修改容器中的元素,除此之外在使用上和后者完全相同。
有关 crbegin()/crend() 成员函数,这里不再给出具体实例,有兴趣的读者,可自行编写代码进行测试。
vector容器迭代器的独特之处
和 array 容器不同,vector 容器可以随着存储元素的增加,自行申请更多的存储空间。因此,在创建 vector 对象时,我们可以直接创建一个空的 vector 容器,并不会影响后续使用该容器。
但这会产生一个问题,即在初始化空的 vector 容器时,不能使用迭代器。也就是说,如下初始化 vector 容器的方法是不行的:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>values;
int val = 1;
for (auto first = values.begin(); first < values.end(); ++first, val++) {
*first = val;
//初始化的同时输出值
cout << *first;
}
return 0;
}
运行程序可以看到,什么也没有输出。这是因为,对于空的 vector 容器来说,begin() 和 end() 成员函数返回的迭代器是相等的,即它们指向的是同一个位置。
所以,对于空的 vector 容器来说,可以通过调用 push_back() 或者借助 resize() 成员函数实现初始化容器的目的。
除此之外,vector 容器在申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效。
举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>values{1,2,3};
cout << "values 容器首个元素的地址:" << values.data() << endl;
auto first = values.begin();
auto end = values.end();
//增加 values 的容量
values.reserve(20);
cout << "values 容器首个元素的地址:" << values.data() << endl;
while (first != end) {
cout << *first;
++first;
}
return 0;
}
运行程序,显示如下信息并崩溃:
values 容器首个元素的地址:0096DFE8
values 容器首个元素的地址:00965560
可以看到,values 容器在增加容量之后,首个元素的存储地址发生了改变,此时再使用先前创建的迭代器,显然是错误的。因此,为了保险起见,每当 vector 容器的容量发生变化时,我们都要对之前创建的迭代器重新初始化一遍:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>values{1,2,3};
cout << "values 容器首个元素的地址:" << values.data() << endl;
auto first = values.begin();
auto end = values.end();
//增加 values 的容量
values.reserve(20);
cout << "values 容器首个元素的地址:" << values.data() << endl;
first = values.begin();
end = values.end();
while (first != end) {
cout << *first ;
++first;
}
return 0;
}
运行结果为:
values 容器首个元素的地址:0164DBE8
values 容器首个元素的地址:01645560
123
C++ STL vector容器访问元素的几种方式
学会如何创建并初始化 vector 容器之后,本节继续来学习如何获取(甚至修改)容器中存储的元素。
访问vector容器中单个元素
首先,vector 容器可以向普通数组那样访问存储的元素,甚至对指定下标处的元素进行修改,比如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
cout << "values 首元素为:" << values.front() << endl;
cout << "values 尾元素为:" << values.back() << endl;
//修改首元素
values.front() = 10;
cout <<"values 新的首元素为:" << values.front() << endl;
//修改尾元素
values.back() = 20;
cout << "values 新的尾元素为:" << values.back() << endl;
return 0;
}
运行结果为:
1
14
显然,vector 的索引从 0 开始,这和普通数组一样。通过使用索引,总是可以访问到 vector 容器中现有的元素。
值得一提的是,容器名[n]
这种获取元素的方式,需要确保下标 n 的值不会超过容器的容量(可以通过 capacity() 成员函数获取),否则会发生越界访问的错误。幸运的是,和 array 容器一样,vector 容器也提供了 at() 成员函数,当传给 at() 的索引会造成越界时,会抛出std::out_of_range
异常。
举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
//输出容器中第 3 个元素的值
cout << *(values.data() + 2) << endl;
//修改容器中第 2 个元素的值
*(values.data() + 1) = 10;
cout << *(values.data() + 1) << endl;
return 0;
}
运行结果为:
1
14
读者可能有这样一个疑问,即为什么 vector 容器在重载 [] 运算符时,没有实现边界检查的功能呢?答案很简单,因为性能。如果每次访问元素,都去检查索引值,无疑会产生很多开销。当不存在越界访问的可能时,就能避免这种开销。
除此之外,vector 容器还提供了 2 个成员函数,即 front() 和 back(),它们分别返回 vector 容器中第一个和最后一个元素的引用,通过利用这 2 个函数返回的引用,可以访问(甚至修改)容器中的首尾元素。
举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
//从下标 0 一直遍历到 size()-1 处
for (int i = 0; i < values.size(); i++) {
cout << values[i] << " ";
}
return 0;
}
输出结果为:
values 首元素为:1
values 尾元素为:5
values 新的首元素为:10
values 新的尾元素为:20
另外,vector 容器还提供了 data() 成员函数,该函数的功能是返回指向容器中首个元素的指针。通过该指针也可以访问甚至修改容器中的元素。比如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
for (auto first = values.begin(); first < values.end(); ++first) {
cout << *first << " ";
}
return 0;
}
运行结果为:
3
10
访问vector容器中多个元素
如果想访问 vector 容器中多个元素,可以借助 size() 成员函数,该函数可以返回 vector 容器中实际存储的元素个数。例如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
//从下标 0 一直遍历到 size()-1 处
for (int i = 0; i < values.size(); i++) {
cout << values[i] << " ";
}
return 0;
}
运行结果为:
1 2 3 4 5
注意,这里不要使用 capacity() 成员函数,因为它返回的是 vector 容器的容量,而不是实际存储元素的个数,这两者是有差别的。
关于 vector 容器 capacity() 和 size() 的差别,可以阅读 《STL vector容量(capacity)和大小(size)的区别》一文。
或者也可以使用基于范围的循环,此方式将会逐个遍历容器中的元素。比如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
for (auto&& value : values)
cout << value << " ";
return 0;
}
运行结果为:
1 2 3 4 5
另外还可以使用 vector 迭代器遍历 vector 容器,这里以 begin()/end() 为例:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{1,2,3,4,5};
for (auto first = values.begin(); first < values.end(); ++first) {
cout << *first << " ";
}
return 0;
}
运行结果为:
1 2 3 4 5
当然,这里也可以使用 rbegin()/rend()、cbegin()/cend()、crbegin()/crend() 以及全局函数 begin()/end() ,它们都可以实现对容器中元素的访问。
C++ vector容量(capacity)和大小(size)的区别
很多初学者分不清楚 vector 容器的容量(capacity)和大小(size)之间的区别,甚至有人认为它们表达的是一个意思。本节将对 vector 容量和大小各自的含义做一个详细的介绍。
vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数;而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数。
对于一个 vector 对象来说,通过该模板类提供的 capacity() 成员函数,可以获得当前容器的容量;通过 size() 成员函数,可以获得容器当前的大小。例如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
std::vector<int>value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 };
value.reserve(20);
cout << "value 容量是:" << value.capacity() << endl;
cout << "value 大小是:" << value.size() << endl;
return 0;
}
程序输出结果为:
value 容量是:20
value 大小是:15
结合该程序的输出结果,图 1 可以更好的说明 vector 容器容量和大小之间的关系。
图 1 vector 容量和大小的区别
显然,vector 容器的大小不能超出它的容量,在大小等于容量的基础上,只要增加一个元素,就必须分配更多的内存。注意,这里的“更多”并不是 1 个。换句话说,当 vector 容器的大小和容量相等时,如果再向其添加(或者插入)一个元素,vector 往往会申请多个存储空间,而不仅仅只申请 1 个。
一旦 vector 容器的内存被重新分配,则和 vector 容器中元素相关的所有引用、指针以及迭代器,都可能会失效,最稳妥的方法就是重新生成。
举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 };
cout << "value 容量是:" << value.capacity() << endl;
cout << "value 大小是:" << value.size() << endl;
printf("value首地址:%p\n", value.data());
value.push_back(53);
cout << "value 容量是(2):" << value.capacity() << endl;
cout << "value 大小是(2):" << value.size() << endl;
printf("value首地址: %p", value.data());
return 0;
}
运行结果为:
value 容量是:15
value 大小是:15
value首地址:01254D40
value 容量是(2):22
value 大小是(2):16
value首地址: 01254E80
可以看到,向“已满”的 vector 容器再添加一个元素,整个 value 容器的存储位置发生了改变,同时 vector 会一次性申请多个存储空间(具体多少,取决于底层算法的实现)。这样做的好处是,可以很大程度上减少 vector 申请空间的次数,当后续再添加元素时,就可以节省申请空间耗费的时间。
因此,对于 vector 容器而言,当增加新的元素时,有可能很快完成(即直接存在预留空间中);也有可能会慢一些(扩容之后再放新元素)。
修改vector容器的容量和大小
另外,通过前面的学习我们知道,可以调用 reserve() 成员函数来增加容器的容量(但并不会改变存储元素的个数);而通过调用成员函数 resize() 可以改变容器的大小,并且该函数也可能会导致 vector 容器容量的增加。比如说:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int>value{ 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 };
cout << "value 容量是:" << value.capacity() << endl;
cout << "value 大小是:" << value.size() << endl;
value.reserve(20);
cout << "value 容量是(2):" << value.capacity() << endl;
cout << "value 大小是(2):" << value.size() << endl;
//将元素个数改变为 21 个,所以会增加 6 个默认初始化的元素
value.resize(21);
//将元素个数改变为 21 个,新增加的 6 个元素默认值为 99。
//value.resize(21,99);
//当需要减小容器的大小时,会移除多余的元素。
//value.resize(20);
cout << "value 容量是(3):" << value.capacity() << endl;
cout << "value 大小是(3):" << value.size() << endl;
return 0;
}
运行结果为:
value 容量是:15
value 大小是:15
value 容量是(2):20
value 大小是(2):15
value 容量是(3):30
value 大小是(3):21
程序中给出了关于 resize() 成员函数的 3 种不同的用法,有兴趣的读者可自行查看不同用法的运行结果。
可以看到,仅通过 reserve() 成员函数增加 value 容器的容量,其大小并没有改变;但通过 resize() 成员函数改变 value 容器的大小,它的容量可能会发生改变。另外需要注意的是,通过 resize() 成员函数减少容器的大小(多余的元素会直接被删除),不会影响容器的容量。
vector容器容量和大小的数据类型
在实际场景中,我们可能需要将容器的容量和大小保存在变量中,要知道 vector 对象的容量和大小类型都是 vector::size_type 类型。因此,当定义一个变量去保存这些值时,可以如下所示:
vector<int>::size_type cap = value.capacity();
vector<int>::size_type size = value.size();
size_type 类型是定义在由 vector 类模板生成的 vecotr 类中的,它表示的真实类型和操作系统有关,在 32 位架构下普遍表示的是 unsigned int 类型,而在 64 位架构下普通表示 unsigned long 类型。
当然,我们还可以使用 auto 关键字代替 vector::size_type,比如:
auto cap = value.capacity();
auto size = value.size();
深度剖析C++ vector容器的底层实现机制
STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。
通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:
//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
...
protected:
pointer _Myfirst;
pointer _Mylast;
pointer _Myend;
};
其中,_Myfirst 指向的是 vector 容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_myend 指向整个 vector 容器所占用内存空间的末尾字节。
图 1 演示了以上这 3 个迭代器分别指向的位置。
图 1 vector实现原理示意图
如图 1 所示,通过这 3 个迭代器,就可以表示出一个已容纳 2 个元素,容量为 5 的 vector 容器。
在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:
- _Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
- _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
- _Myfirst 和 _Myend 可以用表示 vector 容器的容量。
对于空的 vector 容器,由于没有任何元素的空间分配,因此 _Myfirst、_Mylast 和 _Myend 均为 null。
通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能,比如:
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
public:
iterator begin() {return _Myfirst;}
iterator end() {return _Mylast;}
size_type size() const {return size_type(end() - begin());}
size_type capacity() const {return size_type(_Myend - begin());}
bool empty() const {return begin() == end();}
reference operator[] (size_type n) {return *(begin() + n);}
reference front() { return *begin();}
reference back() {return *(end()-1);}
...
};
vector扩大容量的本质
另外需要指明的是,当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放。
这也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
由此可见,vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
vector 容器扩容时,不同的编译器申请更多内存空间的量是不同的。以 VS 为例,它会扩容现有容器容量的 50%。
C++ STL vector添加元素(push_back()和emplace_back())详解
要知道,向 vector 容器中添加元素的唯一方式就是使用它的成员函数,如果不调用成员函数,非成员函数既不能添加也不能删除元素。这意味着,vector 容器对象必须通过它所允许的函数去访问,迭代器显然不行。
在 《STL vector容器详解》一节中,已经给大家列出了 vector 容器提供的所有成员函数,在这些成员函数中,可以用来给容器中添加元素的函数有 2 个,分别是 push_back() 和 emplace_back() 函数。
有读者可能认为还有 insert() 和 emplace() 成员函数,严格意义上讲,这 2 个成员函数的功能是向容器中的指定位置插入元素,后续章节会对它们做详细的介绍。
push_back()
该成员函数的功能是在 vector 容器尾部添加一个元素,用法也非常简单,比如:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{};
values.push_back(1);
values.push_back(2);
for (int i = 0; i < values.size(); i++) {
cout << values[i] << " ";
}
return 0;
}
程序中,第 7 行代码表示向 values 容器尾部添加一个元素,但由于当前 values 容器是空的,因此新添加的元素 1 无疑成为了容器中首个元素;第 8 行代码实现的功能是在现有元素 1 的后面,添加元素 2。
运行程序,输出结果为:
1 2
emplace_back()
该函数是 C++ 11 新增加的,其功能和 push_back() 相同,都是在 vector 容器的尾部添加一个元素。
emplace_back() 成员函数的用法也很简单,这里直接举个例子:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> values{};
values.emplace_back(1);
values.emplace_back(2);
for (int i = 0; i < values.size(); i++) {
cout << values[i] << " ";
}
return 0;
}
运行结果为:
1 2
读者可能会发现,以上 2 段代码,只是用 emplace_back() 替换了 push_back(),既然它们实现的功能是一样的,那么 C++ 11 标准中为什么要多此一举呢?
emplace_back()和push_back()的区别
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
为了让大家清楚的了解它们之间的区别,我们创建一个包含类对象的 vector 容器,如下所示:
#include <vector>
#include <iostream>
using namespace std;
class testDemo
{
public:
testDemo(int num):num(num){
std::cout << "调用构造函数" << endl;
}
testDemo(const testDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" << endl;
}
testDemo(testDemo&& other) :num(other.num) {
std::cout << "调用移动构造函数" << endl;
}
private:
int num;
};
int main()
{
cout << "emplace_back:" << endl;
std::vector<testDemo> demo1;
demo1.emplace_back(2);
cout << "push_back:" << endl;
std::vector<testDemo> demo2;
demo2.push_back(2);
}
运行结果为:
emplace_back:
调用构造函数
push_back:
调用构造函数
调用移动构造函数
在此基础上,读者可尝试将 testDemo 类中的移动构造函数注释掉,再运行程序会发现,运行结果变为:
emplace_back:
调用构造函数
push_back:
调用构造函数
调用拷贝构造函数
由此可以看出,push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。
显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。因此,在实际使用时,建议大家优先选用 emplace_back()。
由于 emplace_back() 是 C++ 11 标准新增加的,如果程序要兼顾之前的版本,还是应该使用 push_back()。
C++ STL vector插入元素(insert()和emplace())详解
vector容器提供了 insert() 和 emplace() 这 2 个成员函数,用来实现在容器指定位置处插入元素,本节将对它们的用法做详细的讲解。
另外,如果想实现在 vector 容器尾部添加元素,可阅读《vector添加元素》一节。
insert()
insert() 函数的功能是在 vector 容器的指定位置插入一个或多个元素。该函数的语法格式有多种,如表 1 所示。
语法格式 | 用法说明 |
---|---|
iterator insert(pos,elem) | 在迭代器 pos 指定的位置之前插入一个新元素elem,并返回表示新插入元素位置的迭代器。 |
iterator insert(pos,n,elem) | 在迭代器 pos 指定的位置之前插入 n 个元素 elem,并返回表示第一个新插入元素位置的迭代器。 |
iterator insert(pos,first,last) | 在迭代器 pos 指定的位置之前,插入其他容器(不仅限于vector)中位于 [first,last) 区域的所有元素,并返回表示第一个新插入元素位置的迭代器。 |
iterator insert(pos,initlist) | 在迭代器 pos 指定的位置之前,插入初始化列表(用大括号{}括起来的多个元素,中间有逗号隔开)中所有的元素,并返回表示第一个新插入元素位置的迭代器。 |
下面的例子,演示了如何使用 insert() 函数向 vector 容器中插入元素。
#include <iostream>
#include <vector>
#include <array>
using namespace std;
int main()
{
std::vector<int> demo{1,2};
//第一种格式用法
demo.insert(demo.begin() + 1, 3);//{1,3,2}
//第二种格式用法
demo.insert(demo.end(), 2, 5);//{1,3,2,5,5}
//第三种格式用法
std::array<int,3>test{ 7,8,9 };
demo.insert(demo.end(), test.begin(), test.end());//{1,3,2,5,5,7,8,9}
//第四种格式用法
demo.insert(demo.end(), { 10,11 });//{1,3,2,5,5,7,8,9,10,11}
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
return 0;
}
运行结果为:
1 3 2 5 5 7 8 9 10 11
emplace()
emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素。
再次强调,emplace() 每次只能插入一个元素,而不是多个。
该函数的语法格式如下:
iterator emplace (const_iterator pos, args…);
其中,pos 为指定插入位置的迭代器;args… 表示与新插入元素的构造函数相对应的多个参数;该函数会返回表示新插入元素位置的迭代器。
简单的理解 args…,即被插入元素的构造函数需要多少个参数,那么在 emplace() 的第一个参数的后面,就需要传入相应数量的参数。
举个例子:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
std::vector<int> demo1{1,2};
//emplace() 每次只能插入一个 int 类型元素
demo1.emplace(demo1.begin(), 3);
for (int i = 0; i < demo1.size(); i++) {
cout << demo1[i] << " ";
}
return 0;
}
运行结果为:
3 1 2
既然 emplace() 和 insert() 都能完成向 vector 容器中插入新元素,那么谁的运行效率更高呢?答案是 emplace()。在说明原因之前,通过下面这段程序,就可以直观看出两者运行效率的差异:
#include <vector>
#include <iostream>
using namespace std;
class testDemo
{
public:
testDemo(int num) :num(num) {
std::cout << "调用构造函数" << endl;
}
testDemo(const testDemo& other) :num(other.num) {
std::cout << "调用拷贝构造函数" << endl;
}
testDemo(testDemo&& other) :num(other.num) {
std::cout << "调用移动构造函数" << endl;
}
testDemo& operator=(const testDemo& other);
private:
int num;
};
testDemo& testDemo::operator=(const testDemo& other) {
this->num = other.num;
return *this;
}
int main()
{
cout << "insert:" << endl;
std::vector<testDemo> demo2{};
demo2.insert(demo2.begin(), testDemo(1));
cout << "emplace:" << endl;
std::vector<testDemo> demo1{};
demo1.emplace(demo1.begin(), 1);
return 0;
}
运行结果为:
insert:
调用构造函数
调用移动构造函数
emplace:
调用构造函数
注意,当拷贝构造函数和移动构造函数同时存在时,insert() 会优先调用移动构造函数。
可以看到,通过 insert() 函数向 vector 容器中插入 testDemo 类对象,需要调用类的构造函数和移动构造函数(或拷贝构造函数);而通过 emplace() 函数实现同样的功能,只需要调用构造函数即可。
简单的理解,就是 emplace() 在插入元素时,是在容器的指定位置直接构造元素,而不是先单独生成,再将其复制(或移动)到容器中。因此,在实际使用中,推荐大家优先使用 emplace()。
C++ STL vector删除元素的几种方式(超级详细)
前面提到,无论是向现有 vector 容器中访问元素、添加元素还是插入元素,都只能借助 vector 模板类提供的成员函数,但删除 vector 容器的元素例外,完成此操作除了可以借助本身提供的成员函数,还可以借助一些全局函数。
基于不同场景的需要,删除 vecotr 容器的元素,可以使用表 1 中所示的函数(或者函数组合)。
函数 | 功能 |
---|---|
pop_back() | 删除 vector 容器中最后一个元素,该容器的大小(size)会减 1,但容量(capacity)不会发生改变。 |
erase(pos) | 删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。 |
swap(beg)、pop_back() | 先调用 swap() 函数交换要删除的目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素。 |
erase(beg,end) | 删除 vector 容器中位于迭代器 [beg,end)指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。 |
remove() | 删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量。 |
clear() | 删除 vector 容器中所有的元素,使其变成空的 vector 容器。该函数会改变 vector 的大小(变为 0),但不是改变其容量。 |
下面就表 1 中罗列的这些函数,一一讲解它们的具体用法。
pop_back() 成员函数的用法非常简单,它不需要传入任何的参数,也没有返回值。举个例子:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int>demo{ 1,2,3,4,5 };
demo.pop_back();
//输出 dmeo 容器新的size
cout << "size is :" << demo.size() << endl;
//输出 demo 容器新的容量
cout << "capacity is :" << demo.capacity() << endl;
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
return 0;
}
运行结果为:
size is :4
capacity is :5
1 2 3 4
可以发现,相比原 demo 容器,新的 demo 容器删除了最后一个元素 5,容器的大小减了 1,但容量没变。
如果想删除 vector 容器中指定位置处的元素,可以使用 erase() 成员函数,该函数的语法格式为:
iterator erase (pos);
其中,pos 为指定被删除元素位置的迭代器,同时该函数会返回一个指向删除元素所在位置下一个位置的迭代器。
下面的例子演示了 erase() 函数的具体用法:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int>demo{ 1,2,3,4,5 };
auto iter = demo.erase(demo.begin() + 1);//删除元素 2
//输出 dmeo 容器新的size
cout << "size is :" << demo.size() << endl;
//输出 demo 容器新的容量
cout << "capacity is :" << demo.capacity() << endl;
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
//iter迭代器指向元素 3
cout << endl << *iter << endl;
return 0;
}
运行结果为:
size is :4
capacity is :5
1 3 4 5
3
通过结果不能看出,erase() 函数在删除元素时,会将删除位置后续的元素陆续前移,并将容器的大小减 1。
另外,如果不在意容器中元素的排列顺序,可以结合 swap() 和 pop_back() 函数,同样可以实现删除容器中指定位置元素的目的。
注意,swap() 函数在头文件
<algorithm>
和<utility>
中都有定义,使用时引入其中一个即可。
例如:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
vector<int>demo{ 1,2,3,4,5 };
//交换要删除元素和最后一个元素的位置
swap(*(std::begin(demo)+1),*(std::end(demo)-1));//等同于 swap(demo[1],demo[4])
//交换位置后的demo容器
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
demo.pop_back();
cout << endl << "size is :" << demo.size() << endl;
cout << "capacity is :" << demo.capacity() << endl;
//输出demo 容器中剩余的元素
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
return 0;
}
运行结果为:
1 5 3 4 2
size is :4
capacity is :5
1 5 3 4
当然,除了删除容器中单个元素,还可以删除容器中某个指定区域内的所有元素,同样可以使用 erase() 成员函数实现。该函数有 2 种基本格式,前面介绍了一种,这里使用另一种:
iterator erase (iterator first, iterator last);
其中 first 和 last 是指定被删除元素区域的迭代器,同时该函数会返回指向此区域之后一个位置的迭代器。
举个例子:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
std::vector<int> demo{ 1,2,3,4,5 };
//删除 2、3
auto iter = demo.erase(demo.begin()+1, demo.end() - 2);
cout << "size is :" << demo.size() << endl;
cout << "capacity is :" << demo.capacity() << endl;
for (int i = 0; i < demo.size(); i++) {
cout << demo[i] << " ";
}
return 0;
}
运行结果为:
size is :3
capacity is :5
1 4 5
可以看到,和删除单个元素一样,删除指定区域内的元素时,也会将该区域后续的元素前移,并缩小容器的大小。
如果要删除容器中和指定元素值相同的所有元素,可以使用 remove() 函数,该函数定义在 <algorithm>
头文件中。例如:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
vector<int>demo{ 1,3,3,4,3,5 };
//交换要删除元素和最后一个元素的位置
auto iter = std::remove(demo.begin(), demo.end(), 3);
cout << "size is :" << demo.size() << endl;
cout << "capacity is :" << demo.capacity() << endl;
//输出剩余的元素
for (auto first = demo.begin(); first < iter;++first) {
cout << *first << " ";
}
return 0;
}
运行结果为:
size is :6
capacity is :6
1 4 5
注意,在对容器执行完 remove() 函数之后,由于该函数并没有改变容器原来的大小和容量,因此无法使用之前的方法遍历容器,而是需要向程序中那样,借助 remove() 返回的迭代器完成正确的遍历。
remove() 的实现原理是,在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做上标记,等待找到新的非目标元素将其覆盖。因此,如果将上面程序中 demo 容器的元素全部输出,得到的结果为
1 4 5 4 3 5
。
另外还可以看到,既然通过 remove() 函数删除掉 demo 容器中的多个指定元素,该容器的大小和容量都没有改变,其剩余位置还保留了之前存储的元素。我们可以使用 erase() 成员函数删掉这些 “无用” 的元素。
比如,修改上面的程序:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
vector<int>demo{ 1,3,3,4,3,5 };
//交换要删除元素和最后一个元素的位置
auto iter = std::remove(demo.begin(), demo.end(), 3);
demo.erase(iter, demo.end());
cout << "size is :" << demo.size() << endl;
cout << "capacity is :" << demo.capacity() << endl;
//输出剩余的元素
for (int i = 0; i < demo.size();i++) {
cout << demo[i] << " ";
}
return 0;
}
运行结果为:
size is :3
capacity is :6
1 4 5
remove()用于删除容器中指定元素时,常和 erase() 成员函数搭配使用。
如果想删除容器中所有的元素,则可以使用 clear() 成员函数,例如:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
vector<int>demo{ 1,3,3,4,3,5 };
//交换要删除元素和最后一个元素的位置
demo.clear();
cout << "size is :" << demo.size() << endl;
cout << "capacity is :" << demo.capacity() << endl;
return 0;
}
运行结果为:
size is :0
capacity is :6
如何避免vector容器进行不必要的扩容?
前面提到,我们可以将 vector 容器看做是一个动态数组。换句话说,在不超出 vector 最大容量限制(max_size() 成员方法的返回值)的前提下,该类型容器可以自行扩充容量来满足用户存储更多元素的需求。
值得一提的是,vector 容器扩容的整个过程,和 realloc() 函数的实现方法类似,大致分为以下 4 个步骤:
- 分配一块大小是当前 vector 容量几倍的新存储空间。注意,多数 STL 版本中的 vector 容器,其容器都会以 2 的倍数增长,也就是说,每次 vector 容器扩容,它们的容量都会提高到之前的 2 倍;
- 将 vector 容器存储的所有元素,依照原有次序从旧的存储空间复制到新的存储空间中;
- 析构掉旧存储空间中存储的所有元素;
- 释放旧的存储空间。
通过以上分析不难看出,vector 容器的扩容过程是非常耗时的,并且当容器进行扩容后,之前和该容器相关的所有指针、迭代器以及引用都会失效。因此在使用 vector 容器过程中,我们应尽量避免执行不必要的扩容操作。
要实现这个目标,可以借助 vector 模板类中提供的 reserve() 成员方法。不过在讲解如何用 reserve() 方法避免 vector 容器进行不必要的扩容操作之前,vector 模板类中还提供有几个和 reserve() 功能类似的成员方法,很容易混淆,这里有必要为读者梳理一下,如表 1 所示。
成员方法 | 功能 |
---|---|
size() | 告诉我们当前 vector 容器中已经存有多少个元素,但仅通过此方法,无法得知 vector 容器有多少存储空间。 |
capacity() | 告诉我们当前 vector 容器总共可以容纳多少个元素。如果想知道当前 vector 容器有多少未被使用的存储空间,可以通过 capacity()-size() 得知。注意,如果 size() 和 capacity() 返回的值相同,则表明当前 vector 容器中没有可用存储空间了,这意味着,下一次向 vector 容器中添加新元素,将导致 vector 容器扩容。 |
resize(n) | 强制 vector 容器必须存储 n 个元素,注意,如果 n 比 size() 的返回值小,则容器尾部多出的元素将会被析构(删除);如果 n 比 size() 大,则 vector 会借助默认构造函数创建出更多的默认值元素,并将它们存储到容器末尾;如果 n 比 capacity() 的返回值还要大,则 vector 会先扩增,在添加一些默认值元素。 |
reserve(n) | 强制 vector 容器的容量至少为 n。注意,如果 n 比当前 vector 容器的容量小,则该方法什么也不会做;反之如果 n 比当前 vector 容器的容量大,则 vector 容器就会扩容。 |
通过对以上几个成员方法功能的分析,我们可以总结出一点,即只要有新元素要添加到 vector 容器中而恰好此时 vector 容器的容量不足时,该容器就会自动扩容。
因此,避免 vector 容器执行不必要的扩容操作的关键在于,在使用 vector 容器初期,就要将其容量设为足够大的值。换句话说,在 vector 容器刚刚构造出来的那一刻,就应该借助 reserve() 成员方法为其扩充足够大的容量。
举个例子,假设我们想创建一个包含 1~1000 的 vector,通常会这样实现:
vector<int>myvector;
for (int i = 1; i <= 1000; i++) {
myvector.push_back(i);
}
值得一提的是,上面代码的整个循环过程中,vector 容器会进行 2~10 次自动扩容(多数的 STL 标准库版本中,vector 容器通常会扩容至当前容量的 2 倍,而这里 1000≈2 10),程序的执行效率可想而知。
在上面程序的基础上,下面代码演示了如何使用 reserve() 成员方法尽量避免 vector 容器执行不必要的扩容操作:
vector<int>myvector;
myvector.reserve(1000);
cout << myvector.capacity();
for (int i = 1; i <= 1000; i++) {
myvector.push_back(i);
}
相比前面的代码实现,整段程序在运行过程中,vector 容器的容量仅扩充了 1 次,执行效率大大提高。
当然在实际场景中,我们可能并不知道 vector 容器到底要存储多少个元素。这种情况下,可以先预留出足够大的空间,当所有元素都存储到 vector 容器中之后,再去除多余的容量。
关于怎样去除 vector 容器多余的容量,可以借助该容器模板类提供的 shrink_to_fit() 成员方法,另外后续还会讲解如何使用 swap() 成员方法去除 vector 容器多余的容量,两种方法都可以。
四、三者之间的区别
在c++11中,STL中提拱了一个新的容器std::array,该容器在某些程度上替代了之前版本的std::vector的使用,更可以替代之前的自建数组的使用。那针对这三种不同的使用方式,先简单的做个比较:
相同点:
-
三者均可以使用下表运算符对元素进行操作,即vector和array都针对下标运算符[]进行了重载
-
三者在内存的方面都使用连续内存,即在vector和array的底层存储结构均使用数组
不同点:
-
vector属于变长容器,即可以根据数据的插入删除重新构建容器容量;但array和数组属于定长容量。
-
vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。而数组只能通过下标访问,在程序的设计过程中,更容易引发访问 错误。
-
vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种
-
vector和array提供了size和判空的获取机制,而数组只能通过遍历或者通过额外的变量记录数组的size
-
vector和array提供了两个容器对象的内容交换,即swap的机制,而数组对于交换只能通过遍历的方式,逐个元素交换的方式使用
-
array提供了初始化所有成员的方法fill
-
vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成
-
由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否失效的问题。
总结:如果只是需要固定大小的数组,那么应该使用std::array,我们可以使用很多成员函数;
如果需要的是支持插入,删除,扩展的数组,那么建议用std::vector。
至于原生数组,那就别用了吧,要拥抱更好的东西!