C++ - vector 的使用

发布于:2025-06-22 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

前言

一、认识vector

二、vector 的接口函数

1、成员函数

1.1 构造

1.2 赋值运算符重载

2、迭代器

3、容量

4、扩容机制

5、修改

6、元素访问

6.1 operator[]

6.2 at

6.3 front

6.4 back

7、非成员函数

三、vector 与 string 的区别

四、注意事项

1、隐式类型转换

2、范围for 

3、迭代器

eg.find

eg.reverse

eg.sort

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、认识vector

vector ,中文含义:矢量,本质上就是顺序表

vector 是一个可以改变大小的数组实现的顺序结构;

二、vector 的接口函数

1、成员函数

1.1 构造

Q: 已经再调用vector 的时候传了空间配置器的模板参数,为什么在调用vector 构造函数的时候还要传?

  • 因为在一般情况下我们无需传空间配置器,但是有可能让你自己显式传;显式传如果该空间配置器带参数的话,其外面就只有模板类型Alloc ,而用Alloc 只能调用默认构造(因为不知道传什么参数);故而此处这么写:的意义在于:一般情况下,我们不用自己创建空间配置器,使用其默认的就够用了;但是不排除在有些情况下,你需要自己实现一个空间配置器,那么此时就可以以传参的形式传进去使用该空间配置器

另外在C++11 中还给了其他的初始化方式,例如:使用初始化列表进行初始化:

使用如下:

用花括号的形式将要初始化的值传给 iniltializer_list ,而inilitializer_list 会依次取到花括号中的值然后依次 push_back 到v 中;

1.2 赋值运算符重载

因为vector 底层涉及资源,所以 operator= 肯定也是深拷贝实现的;

2、迭代器

跟string 中迭代器的涉及一摸一样,此处就不做过多赘述;

3、容量

在vector 之中多了一个:resize;resize ,就是对 size 进行改变

如果resize 中所传参数 n 比当前的size大,它会进行插入(插入 '\0');如果比当前的size 小,它会删除;

reserve ,就是手动扩容如果reserve 中所传参数 n ,小于当前容量,一般是不会缩容,但是也不排除会缩容的可能性(因为在不同平台之下其具体实现不同);reserve 主要是对 capacity 进行改变;

4、扩容机制

VS下的 vector 的扩容机制是怎样的?

  • vs 中vector 的扩容机制与 string 中的扩容机制是一样的,基本上也是按照1.5 倍进行扩容;
void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; i++)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed:" << sz << '\n';
		}
	}
}

在Linux 中,测试:

在Linux 中走的就是2倍扩容;

5、修改

与string 不同的是,在vector 中并没有重载 operator+= ;因为对于string 而言,string 可以尾插(拼接)一个字符、一个字符串;而对于整型来说,没有串的概念,即只有一个一个的数据而没有多个数据冗在一起的概念;对于vector , 想要实现string 中 operator+= 的功能,有 push_back 就够了;

测试使用一下尾插、尾删:

void test2()
{
	vector<int> v1;

	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);

	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;

	v1.pop_back();
	v1.pop_back();

	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;

}

并且在vector 中并没有实现头插、头删;一部分原因是因为头插、尾删需要挪动数据,效率非常低,并且可以使用 insert 或者erase 间接实现头插、头删,故而vector 中就没有提供push_front 和 pop_front;

vector 的erase 和 insert 均是支持迭代器的;

测试使用一下:

void test3()
{
	vector<int> v1 = { 10,20,30,40,50 };
	v1.insert(v1.begin(), 1);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	v1.erase(v1.begin() + 2);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

经过上述代码我们可以发现,我们可以在迭代器的基础上进行加、减,间接地实现除了类似于下标访问地方式,所以erase 和 insert 全部用迭代器实现就可以解决问题,也就没有支持下标版本的插入、删除;

6、元素访问

6.1 operator[]

6.2 at

返回向量中位置为n的元素的引用。

6.3 front

返回vector 中的第一个元素的引用。

6.4 back

返回vector 中最后一个元素的引用。

7、非成员函数

和string 不同的是,vector 的非成员函数中并没有支持 流插入和流提取的重载;因为string 是“串”,“串”可以输入一个输出一个,而vector 想要输出应该借助迭代器或者范围 for ,需要使用者自己控制;

三、vector 与 string 的区别

需要注意的是,不能使用vector<char> 去代替 string! 本质上就是字符数组与字符串的区别;

	vector<char> v = { 'a' , 'b','c' };
	string s("abc");

虽然看起来,vector<char> 和 string 均是一个“数组”,均存放了三个字符;但是 s 的底层之中还隐藏着一个 '\0', 正因其最后有 '\0' 才可以兼容C语言;而vector<char> 中没有隐藏的 '\0’,于是就不兼容C语言;并且在string 中有许多接口函数是vector 没有的(eg. vector 之中没有实现 find ,只能使用算法库中的find );vector<char> 与 string 在功能上存在本质的区别;

四、注意事项

1、隐式类型转换

vector 之中可以存int 、char等内置类型,当然也可以存 string, 是不是vector 中还可以存 vector<int> ? 是的!

	vector<int> v1;
	vector<string> v2;
	vector<vector<int>> v3;

单参数的构造函数支持隐式类型转换,也就意味着 string 支持转换,为什么要支持这种很怪的内置类型到自定义类型的转换呢?

  • eg. vector<string> v2;
  • 若想要向 v2 中push_back , 按照常规思路,就需要先创建 string 对象(有名对象或者匿名对象),然后再插入;但是实际上这样做是有一点麻烦的,直接在push_back 中放所要插入的数据(单参数的构造函数支持隐式类型转换),操作起来是会更方便的
     
void test4()
{
	vector<int> v1;
	vector<string> v2;
	vector<vector<int>> v3;

	//有名对象
	string name1 = "张三";
	v2.push_back(name1);
	//匿名对象
	v2.push_back(string("李四"));
	//隐式类型转换
	v2.push_back("王五");
}

更加细节的是, push_back 的形参类型为 const T& , 也就是说用引用来接收参数,无论实参是内置类型数据还是自定义类型数据,均不会在传参的过程中产生巨大的消耗,提高了效率;而加上 const ,是为了支持匿名对象、临时对象传参(权限不能放大,但是权限可以平移、缩小);

实际上push_back 中形参引用的是 "王五"所创建的临时对象;

与之前所学的类型转换很像:

	int i = 0;
	const double& d = i;
  • 变量 d 不可以直接引用 i ,因为此处的 i 并不只直接赋值给 d ; d 实际上引用的是  i 产生的临时对象,而临时对象具有常性,所以要给 d 加上 const 进行修饰(引用的时候权限不可以放大);

2、范围for 

在使用范围 for 的时候需要注意传值与引用;

void test4()
{
	vector<int> v1;
	vector<string> v2;
	vector<vector<int>> v3;

	//有名对象
	string name1 = "张三";
	v2.push_back(name1);
	//匿名对象
	v2.push_back(string("李四"));
	//隐式类型转换
	v2.push_back("王五");

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

这样写范围for 非常不好;首先v2 中存放的是string对象;而范围for 最终是会转换成迭代器让其去取 v2 中的每一个数据,而现在每个数据是string 对象,那么将每个string 对象给给(传值) e , 会进行深拷贝(自定义类型对象的深拷贝的代价很大);所以写范围for 的时候建议用上引用,如果不改变该数据可以考虑再加一个const 修饰;

	for (const auto&e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

结合编码的知识我们可以思考汉字是如何存储起来的;在实际的内存当中并非存储的是汉字本身,而是先去对应的编码表中查询“张” “三”等汉字在编码表中所对应的字符(一般常见的汉字是由两个字符构成),而当我们将汉字取出准备打印的时候,又会去调查对应的编码表显示成对应的汉字;

3、迭代器

STL的六大组件中比较重要的两个:容器和算法,而容器和算法又是通过迭代器进行访问的;

迭代器本质上就是封装的体现;(封装是面向对象的三大特性)

Q:迭代器为什么需要封装?

  • 首先,容器有很多种,eg.vector、list、set(树)……如果没有迭代器,先要了解清楚容器结构才可以去遍历,即需要暴露底层的结构;而暴露一个容器的底层结构就意味着需要知道这个容器的底层命名,并且在不同的平台之下其命名需要统一;这样处理,不仅加大了程序员们的工作量,而且使用者也会感到非常麻烦(需要对应不同的容器使用不同的遍历方式);

但倘若有迭代器就不一样了;迭代器封装的实现,屏蔽了容器底层实现的细节,提供了统一的访问方式;我们无需去管这个容器、算法等其底层的具体是如何实现的,并且迭代器的使用方式均是完全类似的;而对于算法(eg.查找函数,若没有迭代器,就需要针对不同的容器写对应的查找函数),而有了迭代器之后就可以提供统一的算法,算法的本质是一个模板,针对迭代器设计的模板;

eg.find

之所以vector 、list等容器之中并未提供find ,是因为算法库中有一个函数模板 find,是针对所有容器设计的;

Q:但是不同的容器结构不同;eg. 顺序表是一个数组的结构,链表是一个链式结点的结构、set 是一个二叉树树型的结构……那么 find 是如何针对每一种不同的结构实现的?

  • find 基于模板的迭代器来实现的;也就意味着,使用 find 的时候并不会关心你是哪一个容器;

Q:想要查找该容器中的数据又该如何查找呢?

  • 提供一段迭代器区间(该区间是左闭右开的), 以及所要查找的值 val;

需要注意的是,所有迭代器其访问的区间一定都是左闭右开的!

所有迭代器封装屏蔽了容器底层的实现细节,提供了统一的访问方式:首先可以让我们访问并修改容器;其次也可以让算法作用到容器上去;

再看看算法库中的其他算法:

eg.reverse

Q: 为什么要比较  first != --last?

  • 此处还存在奇数和偶数的问题;只要两指针从两边向中间走就需要考虑数据奇偶数的问题;

 

Q: 能否将 ((first != last) && (first != -- last)) 的判断改为 first < last?

不能因为所有迭代器都支持 == 以及 != 的比较,但是有的却不支持 < 、> 等的比较;因为数据的结构有可能是数组也有可能是链表……像vector 这种底层是连续的空间所以支持迭代器比较大小,但是如果底层空间不连续则就不支持;

eg.sort

sort , 底层是快排,严格来说是自省排序;

注:自省排序是快排与堆排的结合;因为快排可能在极端情况下效率低下;在 sgi 版本的算法库中的sort 使用的就是自省排序;

倘若想利用sort 排降序,此时就会涉及到“仿函数”的概念,sort 默认排升序,实质其第三个参数默认传 less<T>() ;在库之中定义了有两个类模板: greater<T>() 、less<T>() ;

此处我们大致了解一下即可;

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

void test1()
{
	vector<int> v1 = { 10,6,8,32,3,1,90,11 };
	sort(v1.begin(), v1.end());//默认排升序
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	greater<int> gt;
	sort(v1.begin(), v1.end(), gt);//传排降序的仿函数,传有名对象
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

    sort(v1.begin(), v1.end(), less<int>());//排升序,传匿名对象
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

}
int main()
{
	test1();
	return 0;
}

还需要注意的是。list 不支持使用算法库中的sort 进行排序,这是源于list 中迭代器种类;从迭代器功能角度分为:单向迭代器(只支持 ++ ,eg.单链表)、双向迭代器(支持 ++、--) 、随机迭代器(支持 ++、--、+、-);将算法库中的sort转到定义 :

而显然,对于list 来说,结点的地址之间没有任何关系,所以对于list 来说,迭代器相减是没有任何意义的;而算法库中sort 的实现用到了迭代器相减,故而list 不能使用算法库中的sort 来进行排序;


总结

1、vector ,中文含义:矢量,本质上就是顺序表 是一个可以改变大小的数组实现的顺序结构;

2、与string 相比,vector不支持find等string特有功能

3、所以写范围for 的时候建议用上引用,如果不改变该数据可以考虑再加一个const 修饰;

4、sort 默认排升序,实质其第三个参数默认传 less<T>() ;在库之中定义了有两个类模板: greater<T>() 、less<T>() ;


网站公告

今日签到

点亮在社区的每一天
去签到