您现在的位置是:首页 >技术交流 >【STL】vector的使用网站首页技术交流

【STL】vector的使用

LinAlpaca 2024-06-17 10:19:17
简介【STL】vector的使用

目录

前言

默认成员函数

构造函数

拷贝构造

赋值重载

迭代器

正向迭代器

反向迭代器

容量管理

查看容量和大小

扩容

判空

访问数据

下标访问

边界访问

数据修改

尾插尾删

指定位置插入删除

迭代器失效 

清空

​编辑

交换

查找数据

vector可以代替string吗


前言

讲完stringstring的模拟实现,今天讲讲vector的使用。虽然说它叫vector,使用时还是我们平常使用的数组,只不过会自动地调节分配的空间。由于在空间中使用的一块连续的空间,因此支持下标访问,使用起来相当地便利,与我们之前学习的string的区别就在于,string只能存储字符,而vector可以存储任意类型的数据。


默认成员函数

构造函数

在 C++98 中有三种构造函数可以对 vector 进行初始化,分别是:

  • 无参进行构造
  • 放入n个相同数据
  • 根据迭代器区间进行构造

其中的 allocator 是空间配置器,只是用于分配空间,目的为增加申请释放空间的效率。

因为 vector 是一个模板类,因此实例化的时候要声明内置类型。例如 vector 中要存 int 类型的数据便写作 vector<int> ,这样才是其完整的类型名。现在我们就可以试试以不同的构造函数来初始化 vector 了。

[注意]: 使用vector时要包含头文件<vector>!!

int main()
{
	string s("abcd");
	vector<int> v1;
	vector<int> v2(3, 2);
	vector<int> v3(s.begin(), s.end());

	return 0;
}

 

v3 中之所以是 97 98 99 100 是因为模板参数选择的是 int 因此 v3 中存的是字符的 ASCII 码。

也可以这样子定义,本质上是发生了一次拷贝构造,这样使用更加与数据接近更加形象和简便。

int main()
{
	vector<int> v4 = { 1,2,3,4,5 };
	return 0;
}

 

拷贝构造

vector 的拷贝构造便是支持对同类型 vector 的拷贝。

int main()
{
	vector<int> v1({ 1,2,3,4,5 });
	vector<int> v2 = v1;
	return 0;
}

 

 若是模板参数不同的 vector 便无法进行拷贝构造

 

赋值重载

赋值重载用于对已存在的两个 vector 之间进行赋值,就是值之间的拷贝。

         

迭代器

迭代器作为 STL 六大组件,必然是绕不开的话题。库中也准备了多种的迭代器供使用者选择。

正向迭代器

正向迭代器由 begin 开始 end 结束,直接使用起来与 string 的迭代器并无区别。

借助迭代器我们既可以直接使用迭代器,也可以使用范围 for 完成对 vector 的遍历了。

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
	vector<int>::iterator it = v1.begin();
    //auto it = v1.begin();     也可以直接使用auto
	while (it != v1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	for (auto it : v1)
	{
		cout << it << " ";
	}
	cout << endl;
	return 0;
}

  

反向迭代器

反向的迭代器名称为 reverse_iterator,其接口为 rbegin 和 rend ,使用起来与正向迭代器类似。

但值得注意的一点是:范围 for 只能进行正向的遍历,无法反向

 

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
	vector<int>::reverse_iterator it = v1.rbegin();
	//auto it = v1.rbegin();
	while (it != v1.rend())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	return 0;
}

 

容量管理

查看容量和大小

跟 string 一样,我们可以通过 size 和 capacity 两个接口分别访问 vector 的和容量,通过这两个接口也有效帮助我们确定访问时的边界值。

扩容

 通过 reserve 我们能够对 capacity 进行修改,根据传入值的大小会有两种处理方式。

  • n 大于 capacity 时会重新分配一块更大的空间给 vector ,将 capacity 增长到 n 或更大。
  • 其他情况下便不进行处理

同时,reserve 不会更改 vector 的大小和其中的元素。

resize 用于调整 vector 的大小,使其包含 n 个元素,能够对其中的值进行初始化,若未传参数则缺省为 0。

  • 如果n小于当前size,则内容将减少到其前n个元素,删除超出的元素,不更改capacity
  • 如果n大于当前size小于capacity,则通过在末尾插入所需数量的元素来扩展内容,以达到n的大小
  • 如果n大于当前capacity,将自动重新分配存储空间,再将元素填充至n个

使用这段代码便可以直接地观察到上述三种不同的情况。

int main()
{
	vector<int> v = { 1,2,3,4,5 };
	cout << v.size() << " " << v.capacity() << endl;
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.resize(20);
	cout << v.size() << " " << v.capacity() << endl;
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.resize(10);
	cout << v.size() << " " << v.capacity() << endl;
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	return 0;
}

 

判空

通过 empty 这个接口可以得知当前这个 vector 是否为空,换言之就是 size 是否等于 0。

 

访问数据

下标访问

库中对 [ ] 运算符进行了重载,因此可以使用下标对 vector 的元素直接进行访问。

 结合前面的 size 接口便可以简单实现下标遍历 vector。

边界访问

虽然说用下标直接访问就十分的方便了,但库中还有两个接口用于边界值的访问。

通过这两个接口可以直接访问 vector 中的第一个与最后一个元素。

 

数据修改

讲完数据访问,之后便是对 vector 之中的元素进行修改的操作了。

尾插尾删

库中有两个接口分别是 push_back 和 pop_back 分别对应尾插和尾删的功能。

 可以使用如下代码看看接口的效果。

int main()
{
	vector<int> v = { 1,2,3,4,5 };  //原数组
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.pop_back();         //尾删
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.push_back(8);       //尾插
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	return 0;
}

 

指定位置插入删除

 insert 支持让我们在指定的位置插入元素,同时有三种插入方式供使用者选择。

  • 指定位置插入一个val
  • 指定位置插入n个val
  • 指定位置插入迭代器区间

可以通过下面的实例代码看看实际使用的效果。

int main()
{
	vector<int> v = { 1,2,3,4,5 };  //原数组
	vector<int> v1 = { 8,9,10 };
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.insert(v.begin() + 3, 6);   //插入单个值
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.insert(v.begin() + 4, 3, 7);  //插入n个值
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.insert(v.begin() + 6, v1.begin(), v1.end());   //插入一段区间
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	return 0;
}

 ​​​​​​​

之后我们可以使用 erase 删除任意位置的元素。可以给定一个迭代器位置,或是一个迭代器区间,删除该区间内的元素。

值得注意的时,传入的迭代器区间其实是左闭右开的,因此迭代器 last 指向的那个元素不会被删除。

int main()
{
	vector<int> v = { 1,2,3,4,5 };  //原数组
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.erase(v.begin());   //删除第一个元素
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	v.erase(v.begin() + 1, v.begin() + 3);  //删除[3,5)
	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
	return 0;
}

 

迭代器失效 

若我们尝试连续插入多个数据的时候,可能会出现这样子的错误。

   

这是由于当我们不断插入数据的时候,如果出现容量不足的情况就会进行扩容,而扩容大概率不会直接在原地扩容。因此在异地开辟了一块新空间,并将原来的元素拷贝过去,从而达到扩容的效果。

此时,我们原先拿到的迭代器中存的仍是原来空间的地址,而原来那块空间已被释放,因此该迭代器中的地址便成了野指针

换言之,这个迭代器就失效了。那我们如何解决这个问题呢?其实 insert 是有一个返回值的,只不过我们之前忽略了它。

即,会返回指向插入的第一个元素的迭代器,因此我们只需要每次将 insert 的返回值再传回给我们的迭代器,就能够避免迭代器失效的问题。

int main()
{
	vector<int> v = { 1,2,3 };
	auto it = v.begin() + 2;
	for (int i = 0; i <= 20; i++)
	{
		it = v.insert(it, i);
	}
	return 0;
}

同样的现象也会出现在 erase 上,其返回的迭代器是指向删除区间的下一个位置。

清空

我们还可以使用 clear 清空 vector 中的所有元素,本质上就是将 vector 的 size 大小改成 0 即可。

交换

之前在 string 我们也讲过,容器之间的交换并不需要完全的拷贝,而是直接交换类中的指针即可。

因此 vector 中的这个交换函数,自然比原生的交换函数效率要来得高

 

 

查找数据

虽然 vector 的库中并没有查找的这个函数,但是在算法库中为我们准备了一个 find 函数可以进行查找。

传入一个迭代器区间,再输入要查找的值即可,找到了则返回指向该元素的迭代器,反之则返回该容器的 end。

int main()
{
	vector<int> v1 = { 1,2,3,4,5 };
	cout << *find(v1.begin(), v1.end(), 4);
	return 0;
}

这个函数通过使用迭代器因而同时支持了多种容器,使用起来相当方便。

vector<char>可以代替string吗

vector 是一种泛型编程,同时支持 int 和 double 等不同类型的存储,而这些存储形式一般用不到 string 之中的某些接口,比如 += 、逻辑运算等。

可以这么说,string 这个容器是专门针对存储字符类型的,因此其种的接口也是专门适用于处理字符串的,而 vector 需要考虑的是如何支持各种类型都能够存储其中而舍弃了 string 之中的部分操作。


好了,今天使用 vector 的讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。