深入计算机语言之C++:STL之vector的认识和使用

🔑🔑博客主页:阿客不是客

🍓🍓系列专栏:从C语言到C++语言的渐深学习

欢迎来到泊舟小课堂

😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注

一、vector 的了解和介绍

1.1 vector 的介绍

在 C 语言中,数组是一组相同类型元素的有序集合。与字符串类似,它的大小在编译时就已经确定,不可更改。

//大小为5的整型数组
int arr1[5] = { 1,2,3,4,5 };
//大小为5的浮点型数组
double arr2[5] = {0.0};

同样与 string 类似,C++为了更加方便就引入了一个支持可动态大小数组的序列容器 vector。

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

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

  • 说它像数组体现在:vector 就像是数组一样,它也是采用连续存储空间来存储元素的。这也就意味着我们可以用下标访问 vector 的元素,和数组一样的高效。
  • 说它不像数组体现在:vector 的大小是可以动态改变的,而且它的大小会被容器自动处理。

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

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

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

 与其它动态序列容器相比(deques, lists and forward_lists):vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。非末尾的删除和插入操作效率低,且统一的迭代器和引用更好。

1.2 vector 的初始化

📜 头文件:<vector>

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

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

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

vector<int> v2(10, 1);   // 10个1

 迭代器区间初始化:begin 👉 end:

vector<int> v3(v2.begin(), v2.end());

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

 拷贝构造:

vector<int> v3(v2);

但是不乏有这种情况,我不要拷贝对象的头尾数据呢?即,不要它的 begin 和 end,这个时候就得用迭代器了。

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

对于迭代器区间初始化,它这里的 InputInerator 不一定是 vector Inerator。它是一个模板,所以你传的是谁的迭代器,它就可以实例化出谁的迭代器,去遍历 first 和 last 区间,进行初始化。

1.3 string 和 vector 的区别

string 和 vector 有什么区别呢?通过我们之前的学习,string 的底层就是以 char* 来实现的,感觉 vector char 已经很像 string 了。

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

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

vector 是顺序表,存的是任意类型,是针对可动态增长的数组的。而 string 就只是字符串,我随便举两个例子:

  1. vector 支持正常的增删查改,但是不支持 += (本身也没必要+=),也不支持比较大小。
  2. vector 也没有 c_str 这些东西,因为 string 作为字符串专用的类,能提供专有的接口(比如 +=,find),所以这就是 string 存在的意义。

二、vector 的基本 使用

2.1 vector 的遍历  

2.1.1 下标访问

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

💬 下标 + 方括号:遍历

void test_vector1()
{
	vector<int> v1;
	vector<int> v2(10, 1);

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

🚩 运行结果如下:

2.1.2 迭代器

iterator 的使用接口说明
begin + end (重点)

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

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

rbegin + rend

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

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

💬 迭代器的读和写:

void test_vector1()
{
	vector<int> v1;
	vector<int> v2(10, 1);

	vector<int>::iterator it = v2.begin();
	while (it != v2.end())
	{
		*it -= 1;             // 令每个元素-1
		cout << *it << " ";
		it++;
	}
	cout << endl;

}

🚩 运行结果如下:

2.1.3 范围 for

vector 支持迭代器,也就支持范围 for,这个我们在模拟实现 string 的时候已经验证过了。范围 for 的本质就是编译器在编译时自动替换成迭代器,这里也一样。

💬 范围 for 的读和写:

void test_vector1()
{
	vector<int> v1;
	vector<int> v2(10, 1);

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

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

 🚩 运行结果如下:

 记得范围 for 的写要加引用。

2.2 vector 的空间 

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

改变 vector 放入 capacity

2.2.1 获取数据个数的 size()

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

void test_vector2()
{
	vector<int> v(6, 6);
	cout << v.size() << endl;
}

🚩 运行结果如下:

2.2.2 改变 vector 容量的 reserve()

 💬 reserve:

void test_vector2()
{
	vector<int> v(6, 6);
	v.reserve(100);
}

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

2.2.3 改变 vector 大小的 resize()

💬 resize:

void test_vector2()
{
	vector<int> v(6, 6);
	v.resize(100);
}

string 的 resize 如果不指定 "填充值" ,默认给的是 \0而 vector 的 resize 如果不指定,默认给的是其对应类型的缺省值作为 "填充值",这里是 int 就是 0,如果是指针,对应的缺省值就是空指针。

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

void test_vector2()
{
	vector<int> v(6, 6);
	v.resize(100, 6);
}

📌 注意事项:如果开的数据比之前更小,还会删除数据!当然,正如我们 string 章节所说,它的容量并不会因此改变。

2.2.4 vector 空间增长问题

  1. capacity 的代码在 VS 和 g++下分别运行会发现:VS下 capacity 是按 1.5 倍增长的,而 g++ 下 capacity 是按 2 倍增长的。 这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。VS 是 PJ 版本 STL,g++ 是 SGI 版本 STL。
  2. reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。
  3. resize 在开空间的同时还会进行初始化,影响 size。

💬 测试:

int main()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	cout << "capacity changed: " << sz << endl;
	for (int i = 0; i < 100; i++)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			std::cout << "capacity changed: " << sz << endl;
		}
	}
	return 0;
}

🚩 VS运行结果如下:

🚩 g++ 运行结果如下:

making foo grow :

capacity changed : 0
capacity changed : 1
capacity changed : 2
capacity changed : 4
capacity changed : 8
capacity changed : 16
capacity changed : 32
capacity changed : 64
capacity changed : 128

2.3 vector 的增删查改

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

2.3.1 push_back()尾插

vector 不能用 operator+= ,string 能用 += 主要是 string 不仅可以尾插一个字符还可以追加一个字符串。但是 vector 就只支持一个一个数据的插入和删除,push_back 和 pop_back。

💬 举个例子:

void test_vector1()
{
	vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
}

2.3.2 assign() 赋值

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

💬 用 n 个 value 覆盖:

void test_vector6()
{
	vector<int> 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
}

2.3.3 为什么 vector 类中不提供 find 接口

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

其实,我们应该考虑的是 —— 为什么 string、map、set 能有 find 操作。而 vector 之所以不提供 find ,是因为如果去查找元素效率就会是 O(n) …… 

当然,如果我们非要用的话 "algorithm库" 里有通用的 find 操作

#include <algorithm>

该 find 内部是从 begin 到 end 进行一次遍历,其复杂度是 O(n) 值得一提的是,在C++中,凡是使用迭代器区间,都是左闭右开的 ——  (\, first, \, last\, ] 

2.3.4 insert() 插入

比如我们刚才用通用 find 找到了 3 的位置,我们想在这个位置前面插入一个数据,就可以使用 insert() 插入。

注意与 string 类不同的是,string 类中 insert 是按照内置的 find 函数返回的下标进行访问的,而vector 没有内置的find 函数insert 只能使用通用的 find 函数返回的迭代器进行访问,这也导致了经典的迭代器失效的问题,这部分内容我们后续再进行讲解。

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

	vector<int>::iterator ret = find(v.begin(), v.end(), 3);
	if (ret != v.end())
	{
		v.insert(ret, 666);
	}

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

🚩 运行结果如下:

2.3.5 erase() 删除

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

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

void test_vector3()
{
	vector<int> 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<int>::iterator pos = find(v.begin(), v.end(), 3);
	if (pos != v.end())
	{
		v.erase(pos);
	}

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

🚩 运行结果如下:

❓ 如果没有判断且数据不存在会怎么样?

void test_vector3()
{
	vector<int> 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<int>::iterator pos = find(v.begin(), v.end(), 5);
	//if (pos != v.end())
	//{
	//	v.erase(pos);
	//}
	v.erase(pos);

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

 如果有了判断,就不会翻车了,如果待删目标不存在,就不会去走 erase()  。 因为 pos 如果找不到就会等于 end() 上的值,我们利用这一点进行 if 判断。

2.3.6 claer() 清空数据

💬 clear:

void test_vector4()
{
	vector<int> 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;
}

🚩 运行结果如下:

三、vector 不支持流插入和流提取

int main()
{
	vector<int> v1(10, 0);
	//模拟流提取:从键盘提取值
	for (size_t i = 0; i < v1.size(); i++)
	{
		cin >> v1[i];
	}

	//模拟流插入:往屏幕输出值
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

四、vector 存储自定义类型

4.1 vector 存储 string 类型

vector 不只是可以存储内置类型,还可以存储自定义类型,比如存储 string 类。

💬这个时候使用范围 for 就需要使用引用来减少拷贝构造了:

int main()
{
	//vector存储string
	vector<string> v1;
	string s("hello world");
	v1.push_back(s);
	v1.push_back("hello bit"); //隐式类型转换

	for (const auto& e : v1) //减少拷贝构造,提高效率
	{
		cout << e << endl;
	}
	cout << endl;

	return 0;
}

🚩 运行结果如下:

 4.2 vector 实现二维数组

vector 既然都能存储 string 类了,那自然也可以存储 vector<int> 类型,这样实现的结果就是二维数组。

4.2.1 传统开辟二维数组

在这里插入图片描述

int main()
{
	int** p = (int**)malloc(sizeof(int*) * 3);
	for (int i = 0; i < 3; i++)
	{
		p[i] = (int*)malloc(sizeof(int) * 3);
	}

	for (int i = 0; i < 3; i++)
	{
		free(p[i]);
		p[i] = NULL;
	}
	free(p);
	p = NULL;

	return 0;
}

这种方式需要开辟空间,使用有的时候甚至需要二级指针

4.2.2 vector模拟实现二维数组

在这里插入图片描述
在这里插入图片描述

template<class T>
class vector
{
public:
	T& operator[](int i)
	{
		return _a[i];
	}
private:
	T* _a;
	size_t size;
	size_t capacity;
};

int main()
{
	//利用vector模拟实现:10 * 5 的二维数组
	vector<int> v(5, 1);
	vector<vector<int>> vv(10, v);
	vv[2][1] = 100;
	//vv.operator[](2).operator[](1) = 100;

	for (int i = 0; i < 10; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			cout << vv[i][j] << " ";
		}
		cout << endl;
	}

	return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值