栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++要笑着学:vector 常用接口介绍

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++要笑着学:vector 常用接口介绍

   ​​​​​​ 藍 爆笑教程   《C++要笑着学》  火速订阅  

 写在前面:

本章开始讲解 vector,首先对 vector 进行介绍,然后讲解 vector 常用的接口。像 emplace 等涉及右值引用的接口,我们等后期讲C++11的时候再作讲解。话不多说,直接开讲。


Ⅰ. vector 的介绍及使用

0x00 vector 的介绍

 vector 文档介绍:vector - C++ Reference

① vector 是表示可变大小数组的序列容器,我们说 vector 像数组,但是又不像数组。

说它像数组体现在:vector 就像是数组一样,它也是采用连续存储空间来存储元素的。这也就意味着我们可以用下标访问 vector 的元素,和数组一样的高效。

说它不像数组体现在:vector 的大小是可以动态改变的,而且它的大小会被容器自动处理。

② 本质上来说,vector 使用动态分配数组来存储它的元素。

当新元素插入时,为了增加存储空间,这个数组就需要被重新分配大小。具体做法是分配一个新的数组,然后将全部元素转移到这个新的数组。就时间而言,这是一个相对代价较高的任务,因为每当一个新的元素加入后,vector 并不会每次都重新分配大小。

③ vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间会比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于末尾插入一个元素时是在常数时间的复杂度完成的。

因此,vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且由一种有效的方式去动态增长。

④ 与其它动态序列容器相比(deques, lists and forward_lists):

 vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。

  但是对于其它不在末尾的删除和插入操作,效率更低(这个我们下面会详细讲解)。比起 lists 和 forward_lists 统一的迭代器和引用更好。

vector 的底层就是一个动态的顺序表,一个可改变 _size 的数组,不就是动态的顺序表吗,hhh。  

0x01 vector 的初始化

 头文件:

#include 
using namespace std;   // 日常练习,我们可以无顾虑地展开

① 无参构造:一般用的最多的就是无参:

vector v1;   // 无参构造,创建一个int类型的,空的vector对象

② 构造并初始化:用  个 value 初始化:

vector v2(10, 3);   // 10个3

③ 迭代器区间初始化:begin  end:

vector v3(v2.begin(), v2.end());

 还用什么迭代器,直接用拷贝构造不香吗?

④ 拷贝构造:

vector v4(v2);

但是不乏有这种情况,我不要拷贝对象的头尾数据呢?即,不要它的 begin 和 end 。

vector v3(++v2.begin(), --v2.end());

 这样就少了 v2 的头尾数据了。看,还是有点用处的吧。

对于迭代器区间初始化,它这里的 InputInerator 不一定是 vector Inerator。

它是一个模板,所以你传的是谁的迭代器,它就可以实例化出谁的迭代器,

去遍历  和  区间,进行初始化。

 所以有些场景我们可以这么初始化:

string s("hello world");
vector v5(s.begin(), s.end());

0x02 调试观察 vector

 我们打个断点进入调试,打开监视窗口看看。

#include 
#include 
#include 
using namespace std;

void test_vector1() {
	vector v1;
	vector v2(10, 3);
	vector v3(++v2.begin(), --v2.end());
	vector v4(v2);

	string s("hello world");
	vector v5(s.begin(), s.end());
}

int main(void)
{
	test_vector1();

	return 0;
}

 具体细节如下:

0x03 刍议 string 和 vector 的区别

string 和 vector 有什么区别呢?刚才通过监视观察,感觉 vector char 已经很像 string 了。

❓ 我们可以思考一个问题:能不能让 vector char 去替代 string 呢?这合适吗?

 答案是否定的。因为 vector char 没有 ,而 string 结尾是有 的。

那我给 vector char 后面手动装一个 行不?

你要硬是想这么玩,也不是不可以。但是 "术业有专攻" ,他们俩的体系是不一样的!

vector 是顺序表,存的是任意类型,是针对可动态增长的数组的。

而 string 就只是字符串,我随便举两个例子:

① vector 支持正常的增删查改,但是不支持 += (本身也没必要+=),也不支持比较大小。

② vector 也没有 c_str 这些东西,因为 string 作为字符串专用的类,能提供专有的接口(比如 +=,find),所以这就是 string 存在的意义。

0x04 关于 vector 的析构、拷贝构造和赋值构造

 至于析构函数,一般情况下我们不需要管,因为它会自动调用。

拷贝构造和赋值构造,vector 的拷贝构造和赋值其实就是深拷贝。

这些我们放在 vector 模拟实现的章节里详细探讨。

Ⅱ. vector 的遍历

0x00 引入

 我们先简单介绍 3 种 vector 的遍历方式,然后再介绍一些访问元素的接口。

其中为了讲解 "下标 + 方括号" 的遍历方式,我们先提前介绍一下 vector 的 push_back 。

除此之外还有反向迭代器、const 迭代器……我们就不壹壹介绍了,具体可以看文档。

0x01 push_back()

  vector 不能用爽到飞起地 operator+= 

string 能用 += 主要是 string 不仅可以尾插一个字符还可以追加一个字符串。

但是 vector 就只支持一个一个数据的插入和删除,push_back 和 pop_back。

我们发现,vector 和 string 一样,是没有提供 push_front 和 pop_front 的,因为挪动数据效率低。

 如果你硬是要去头插头删,也没人拦你,你可以用 insert 和 erase 去操作。

所以我们用 push_back 尾插接口去操作!

vector.push_back(内容);

 举个例子:

void vector_Traversal_test() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
}

0x02 下标 + 方括号遍历

 vector 是连续的空间,又支持 operator[] 和 size() ,所以可以用下标+方括号遍历。

 下标 + 方括号:遍历

void vector_Traversal_test() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 遍历
	for (size_t i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << endl;
}

 运行结果如下:

 当然也是可以修改的:

void vector_Traversal_test() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 遍历
	for (size_t i = 0; i < v.size(); i++) {
		v[i] += 1;    // 令每个元素+1
		cout << v[i] << " ";
	}
	cout << endl;
}

 运行结果如下:

0x03 访问 vector 的 at() 

顺便再讲一下 at() ,它和 operator[] 一样,也是用来访问 vector 的,但是 at() 会进行边界检查。

c.at(idx)传回索引 idx 所指的数据,如果 idx 越界,抛异常 out_of_range 

 at 用法演示:

void test_vector4() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << v.at(0) << endl;
	cout << v.at(3) << endl;
}

 运行结果如下:

 注意事项:operator[] 会用断言检查越界,而 at() 会抛异常。

void test_vector4() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << v.at(5) << endl;  // 故意越界看看
}

0x04 用迭代器遍历vector
iterator 的使用接口说明
begin + end (重点)

获取第一个数据位置的 iterator/const_iterator,

获取最后一个数据的下一个位置的 iterator/const_iterator

rbegin + rend

获取最后一个数据位置的 reverse_iterator,

获取第一个数据前一个位置的 reverse_iterator

 迭代器的读和写:

void vector_Traversal_test() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
    
    // 用迭代器遍历
	vector::iterator it = v.begin();
	while (it != v.end()) {
		*it -= 1;             // 令每个元素-1
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

 运行结果如下:

0x05 范围 for

vector 支持迭代器,也就支持范围 for,这个我们在模拟实现 string 的时候已经验证过了。

范围 for 的本质就是编译器在编译时自动替换成迭代器,这里也一样。

 范围 for 的读和写:

void vector_Traversal_test() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 范围for
	for (auto e : v) {
		cout << e << " ";
	}
	cout << endl;

	for (auto& e : v) {
		e += 10;
		cout << e << " ";
	}
	cout << endl;
}

  运行结果如下:

记得范围 for 的写要加引用,这时老生常谈的问题了。

顺便说一点,原生指针就是天然的迭代器,数组支持范围 for,会被替换成指针:

for (int* p = arr; p < arr + sizeof(arr) / sizeof(int); p++) {}

Ⅲ. vector 空间
容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize     (重点)改变 vector 的 size
reserve   (重点)

改变 vector 放入 capacity

0x00 获取数据个数的 size()

 和 string 里的一样,是用来获取数据的个数的。

void test_vector2() {
	vector v(6, 6);        // 生成6个6
	cout << v.size() << endl;
}

 运行结果如下:

0x01 获取 vector 最大存储的 max_size()

 我们来用 max_size 看看有多大:

void test_vector3() {
	vector v(6, 6);   // 生成6个6
	cout << v.max_size() << endl;
}

 运行结果如下:

0x02 改变 vector 容量的 reserve()

 reserve:

void test_vector3() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.reserve(100);
}

 调试结果如下:

reserve 会扩容,但是不会影响数据个数。[capacity] 4  → [capacity]100

0x03 改变 vector 大小的 resize()

 " reserve 和 resize 都是卖房子的(开空间的),reserve 只是把房子卖给你(开空间),而 resize 是一条龙服务,房子卖给你(开空间)之后还帮你搞装修(初始化),你还可以指定装修风格(初始化内容),如果不指定会按默认的简约风格装修(按类型的缺省值初始化)"

 resize:

void test_vector4() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(100);
}

string 的 resize 如果不指定 "填充值" ,默认给的是

而 vector 的 resize 如果不指定,默认给的是其对应类型的缺省值作为 "填充值"

这里是 int 就是 0,如果是指针,对应的缺省值就是空指针。

 我们来试着给 resize 提供指定 "填充值":

void test_vector5() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(100, 6);   // 开100个空间,用6补
}

 注意事项:如果开的数据比之前更小,还会删除数据!

void test_vector4() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(2);
}

当然,正如我们 string 章节所说,它的容量并不会因此改变。

这里虽然大小变成 2,数据也只有 [0]1 和 [1]2 了,但是容量仍然为 4。

0x04 vector 空间增长问题

capacity 的代码在 VS 和 g++下分别运行会发现:VS下 capacity 是按 1.5 倍增长的,而 g++ 下 capacity 是按 2 倍增长的。 这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。VS 是 PJ 版本 STL,g++ 是 SGI 版本 STL。

② reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。

③ resize 在开空间的同时还会进行初始化,影响 size

 测试:

// vector::capacity
#include 
#include 

int main(void)
{
	size_t sz;
	std::vector foo;
	sz = foo.capacity();
	std::cout << "making foo grow:n";
	for (int i = 0; i < 100; ++i) {
		foo.push_back(i);
		if (sz != foo.capacity()) {
			sz = foo.capacity();
			std::cout << "capacity changed: " << sz << 'n';
		}
	}
}

 VS运行结果如下:

making foo grow :
capacity changed : 1
capacity changed : 2
capacity changed : 3
capacity changed : 4
capacity changed : 6
capacity changed : 9
capacity changed : 13
capacity changed : 19
capacity changed : 28
capacity changed : 42
capacity changed : 63
capacity changed : 94
capacity changed : 141

 g++ 运行结果如下:

making foo grow :
capacity changed : 1
capacity changed : 2
capacity changed : 4
capacity changed : 8
capacity changed : 16
capacity changed : 32
capacity changed : 64
capacity changed : 128

Ⅳ. vector 增删查改
vector 增删查改接口说明
push_back(重点)尾插
pop_back  (重点)尾删
find            (#include algorithm)查找(注意这个是算法模块实现,不是 vector 的成员接口)
insert在 pos 之前插入 val
erase删除 pos 位置的数据
swap交换两个 vector 的数据空间
operator[]  (重点)像数组一样访问

  刚才讲 "下标 + 方括号" 的遍历方式的时候已经讲过 push_back 了,

也顺便解释了为什么不提供 push_front 和 pop_front ,所以这里就不多bb了。

0x00 pop_back() 尾删

现在我们把 pop_back 简单介绍一下,pop_back 就是尾删,可用来删除 vector 中的数据。

 pop_back:

void test_vector5() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v) cout << e << " "; cout << endl;

	v.pop_back();   // 尾删:3
	for (auto e : v) cout << e << " "; cout << endl;
	
	v.pop_back();   // 尾删:2
	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:

0x01  assign() 赋值

assign 可以把 vector 的内容覆盖掉。允许给一段区间覆盖,也可以给  个 value 去覆盖。

 用  个 value 覆盖:

void test_vector6() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.assign(10, 5);   // 原来是1到4,现在改成10个5
}

 调试结果如下:

0x02 探讨:为什么 vector 不提供 find 接口

string、map、set 都有 find() 用,凭什么 vector 和 list 没有?

太不公平了吧!歧视这是赤裸裸的容器歧视??

其实,我们应该考虑的是 —— 为什么 string、map、set 能有 find 操作。

而 vector 之所以不提供 find ,是因为如果去查找元素效率就会是  …… 

 呵呵,凭什么不让我 vector 用 find() ,我偏要用!

可以的,"algorithm库" 里有通用的 find 操作,我们来一睹其芳容 ——

#include 

该 find 内部是从 beginend 进行一次遍历,其复杂度是  

值得一提的是,在C++中,凡是使用迭代器区间,都是左闭右开的 ——   

(map 和 set 接下来的章节会讲,以下部分可先作了解)

再去思考 map、set 为什么有 find() 通过迭代器从头到尾遍历 map 与 set 时,

得到的结果是按 key 排序的结果,而不是插入时的顺序,所以这两个容器没有 push_back 操作。

其实,插入到 map 与 set 中的元素会被组织到一颗红黑树上,红黑树是一颗平衡二叉树,

平衡二叉树是一颗二叉排序树,对一颗二叉排序树的查找有点像二分查找,复杂度是 ,

由于 map 与 set 的数据结构能有更快的查找方法,所以在容器内提供了 find 方法。

0x03 通用查找 find()

 如果非要在 vector 里用 find() ,我们可以用通用 find。

找到了:返回一个迭代器区间那个值的位置;

没找到:返回的是  ,即右边开区间的位置。

 使用方法演示:

void test_vector7() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector::iterator ret = find(v.begin(), v.end(), 3);
    // auto ret = find(v.begin(), v.end(), 3);

	if (ret != v.end()) {
		cout << "找到了" << endl;
	}
}

0x04 insert() 插入

 比如我们刚才用通用 find 找到了 3 的位置,

我们想在这个位置前面插入一个数据,就可以使用 insert() 插入。

 在 3 前面用 insert 插入一个 30:

void test_vector7() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector::iterator ret = find(v.begin(), v.end(), 3);
	if (ret != v.end()) {
		cout << "找到了" << endl;
		v.insert(ret, 30);         // 在ret位置前面插入一个30
	}

	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:

我们刚才说过,虽然没有 vector 没有提供 push_front,但是我们也可以用 insert 去头插。

 用 insert 去头插:

void test_vector8() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
    for (auto e : v) cout << e << " "; cout << endl;

	v.insert(v.begin(), -1);   // 在起始位置插入一个-1

	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:

0x05 erase() 删除

 我们我们想删除数据,我们就可以用 erase 去删除。

 使用 erase 的时要判断一下有没有找到要删除的目标:

void test_vector9() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v) cout << e << " "; cout << endl;

	vector::iterator pos = find(v.begin(), v.end(), 5);
	if (pos != v.end()) {   // 判断pos是否存在
		v.erase(pos);       // 删除pos
	}

	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:

❓ 如果没有判断会怎么样?

void test_vector9() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v) cout << e << " "; cout << endl;

	vector::iterator pos = find(v.begin(), v.end(), 3);
	v.erase(pos);

	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:

如果要删除的目标存在,不会怎么样。

怕的就是要删的目标不存在!比如我要删个 5,但是 vector 里只有 1,2,3,4 。

vector::iterator pos = find(v.begin(), v.end(), 5);
v.erase(pos);

  (待删目标不存在导致)

  如果有了判断,就不会翻车了,如果待删目标不存在,就不会去走 erase()  。 

因为 pos 如果找不到就会等于 end() 上的值,我们利用这一点进行 if 判断,

vector::iterator pos = find(v.begin(), v.end(), 5);
if (pos != v.end()) {   // 检查!
	v.erase(pos);   
}

0x04 clear() 清空数据

 clear:

void test_vector10() {
	vector v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	
	printf("清空前:");
	for (auto e : v) cout << e << " "; cout << endl;
	
	v.clear();
	printf("清空后:");
	for (auto e : v) cout << e << " "; cout << endl;
}

 运行结果如下:


 参考资料

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

. C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

 [ 笔者 ]   王亦优
 [ 更新 ]   2022.5.8
❌ [ 勘误 ]   
 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

本章完。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/872963.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号