C++ -- STL【vector的使用】

目录

[一、 C/C++中的数组](#一、 C/C++中的数组)

1、C语言中的数组

2、C++中的数组

二、vector的接口

1、vector的初始化与销毁

2、vector的迭代器

3、vector的容量操作

[3.1 有效长度与容量大小](#3.1 有效长度与容量大小)

[3.2 容量操作](#3.2 容量操作)

4、vector的访问操作

5、vector的修改操作

三、迭代器失效

1、定义

2、分类

2.1、扩容后失效和插入后失效

[2.2 删除后失效](#2.2 删除后失效)

四、default关键字

五、类名与类型


一、 C/C++中的数组

1、C语言中的数组

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

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

2、C++中的数组

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

  1. vector 是可变大小的序列容器,采用连续存储空间存储元素,可通过下标高效访问。
  2. 与数组不同,vector 大小可动态改变,由容器自动处理。
  3. vector 本质上用动态分配数组存储元素,插入新元素时可能重新分配空间,即分配新数组并移动全部元素,此操作时间代价高,但不是每次插入都重新分配。
  4. vector 会分配额外空间适应增长,不同库策略不同,但重新分配通常是对数增长间隔,使末尾插入元素能在常数时间完成。
  5. 与 deque、list 和 forward_list 相比,vector 访问元素及末尾添加和删除元素更高效,非末尾的删除和插入操作效率低,且统一的迭代器和引用更好。
cpp 复制代码
//整型数组
vector<int> v1;
//浮点型数组
vector<double> v2;

并且注意每次使用 vector 都需要包含头文件 #include<vector>。并且 vector 是一个模版类,所以在使用时需要显示实例化


二、vector的接口

接下来我们将介绍一些 vector 的常见接口,因为很多接口的作用都与 string 的接口非常类似,所以很多就不在详细说明,大家具体也可以参考vector的使用

1、vector的初始化与销毁

同样的 vector 也支持多种构造函数,拷贝构造以及赋值运算符重载。

cpp 复制代码
void Test2()
{
	//1.默认构造函数初始化
	vector<int> v1;
	//2.n个val初始化
	vector<int> v2(3, 2);
	string s("abcd");
	//3.利用迭代器区间初始化
	vector<int> v3(s.begin(), s.end());
	//4.拷贝构造
	vector<int> v4(v3);
	//5.赋值重载
	v2 = v3;
	//6.可变参数列表初始化
	vector<int> v5 = { 1,2,3,4,5 };
	vector<char> v6 = v4;//error 不同类型不能赋值
}

其中需要注意的是可变参数列表初始化,这是在 C++11 之后支持的新语法,具体讲解我们之后再谈。

2、vector的迭代器

同样的 vector 中也存在迭代器 iterator,因为定义在 vector 类中,所以其需要通过域作用限定符访问 ------ vector<类型>::iterator

下面将介绍的 begin(),end(),rbeign(),rend() 的使用访问方法与 string 中的几乎一摸一样,我们直接上实例演示:

cpp 复制代码
void Test1()
{
	vector<int> v = {1,2,3,4,5,6,7,8};
	vector<int>::iterator it = v.begin();
	cout << "顺序遍历:";
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	cout << "逆序遍历:";
	vector<int>::reverse_iterator rit = v.rbegin();
	while (rit != v.rend())
	{
		cout << *rit << " ";
		++rit;
	}
}

当然 vector 也支持 const_iterator,用法也类似,这里就不在赘述。

3、vector的容量操作

函数名称 功能
size 返回数组的有效长度
capacity 返回数组的容量大小
clear 清空数组
empty 检查是否为空数组,是则返回 ture,否则返回 false
reserve 请求改变数组的容量
resize 重新设置有效元素的数量,超过原来有效长度则用 c 字符填充

3.1 有效长度与容量大小

在 vector 类中,我们同样可以通过 size() 容器的有效长度;capacity() 返回容器的容量大小。

cpp 复制代码
void Test3()
{
	vector<int> v = { 1,2,3,4,5 };
	cout << v.size() << endl;
	cout << v.capacity() << endl;
}

在初始化时,vector 中的 size 与 capacity() 一般相同。这时我们也可以通过以下程序探究一下其扩容机制:

cpp 复制代码
void TestExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:"<<endl;
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << endl;
		}
	}
}

在 VS 环境下,vector 一般是以 1.5倍 扩容。但是在 Linux 环境下一般就以 2倍 扩容。

至于 clear() 与 empty() 两个函数用法就十分简单,这里就不在赘述了。

3.2 容量操作

接下来我们来使用一下 vector 中的 resize() 与 reserve()。其实他们的用法与特点也是与 string 类中的相同,我们直接上手即可。

  • 当 n < sz 时,reserve 并不会发生任何改变,resize 会删除有效字符到指定大小。
  • 当 sz < n < capcity 时,reserve 并不会发生任何改变,resize 会补充有效字符(默认为0)到指定大小。
  • 当 n > capacity 时,reserve 会发生扩容,resize 会补充有效字符(默认为 0 )到指定大小。
cpp 复制代码
void Test4()
{
	vector<int> v1 = { 1,2,3,4,5 };
	cout << "v1的有效长度为:" << v1.size() << endl;
	cout << "v1的容量大小为:" << v1.capacity() << endl;
	v1.reserve(10);
	cout << "v1的有效长度为:" << v1.size() << endl;
	cout << "v1的容量大小为:" << v1.capacity() << endl;
	v1.resize(8, 10);
	for (auto& e : v1)
	{
		cout << e << " ";
	}
}

在这里我们需要注意一个经典错误,如下列代码:

cpp 复制代码
void Test5()
{
	vector<int> v;
	v.reserve(10);
	for (int i = 0; i < 10; i++)
	{
		v[i] = i;
	}
	for (auto& e : v)
	{
		cout << e << " ";
	}
}

一旦运行就会发生如上错误,这是为什么呢?因为 reserve 只是改变了容量 capacity 并没有改变size,而 operator[]访问时元素时是禁止访问下标 size 以后的元素的,一旦访问就会直接报错。

4、vector的访问操作

函数名称 功能
operator[] 返回指定位置的元素,越界则报错
at 返回指定位置的元素,越界则抛异常
back 返回字符串最后一个元素
front 返回字符串第一个元素

这四个函数的用法也与 string 中的函数用法相同,我们就直接上手示例。

cpp 复制代码
void Test6()
{
	vector<int> v = { 1,2,3,4,5 };
	for (int i=0;i<v.size();i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "front:" << v.front() << endl;
	cout << "back:" << v.back() << endl;
}

5、vector的修改操作

函数名称 功能
push_back 在数组后追加元素
insert 在指定位置追加元素
assign 使用指定数组替换原数组
pop_back 删除数组最后一个元素
erase 删除数组指定部分区间
swap 交换两个数组

我们首先先介绍最简单的四个函数 push_back(),pop_back(),assign(),swap()。

cpp 复制代码
void Test7()
{
	vector<int> v = { 1,2,3,4,5,6 };
	cout << "back:" << v.back() << endl;
    //尾插
	v.push_back(7);
    //尾删
	cout << "back:" << v.back() << endl;
	v.pop_back();
	cout << "back:" << v.back() << endl;
	vector<int> vv = { 6,5,4,3,2,1 };
    //n个val赋值给原数组
	vv.assign(3, 2);
	for (int i = 0; i < vv.size(); i++)
	{
		cout << vv[i] << " ";
	}
	cout << endl;
	vv.swap(v);
	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < vv.size(); i++)
	{
		cout << vv[i] << " ";
	}
}

然后我们来介绍 insert() 与 earse() 的用法,这两个函数的用法就与 string 中的有所不同。首先是 insert() 函数:

cpp 复制代码
void Test8()
{
	vector<int> myvector(3, 100);
	vector<int>::iterator it = myvector.begin();
	//1.向指定位置插入一个元素
	it = myvector.insert(it, 200);
	cout << "myvector contains:";
	for (it = myvector.begin(); it < myvector.end(); it++)
		cout << ' ' << *it;
	cout << endl;
	//2.向指定位置插入n个元素
	myvector.insert(it, 2, 300);
	cout << "myvector contains:";
	for (it = myvector.begin(); it < myvector.end(); it++)
		cout << ' ' << *it;
	cout << endl;
	//3.向指定位置插入一段迭代器区间
	it = myvector.begin();
	vector<int> anothervector(2, 400);
	cout << "myvector contains:";
	for (it = myvector.begin(); it < myvector.end(); it++)
		cout << ' ' << *it;
	cout << endl;
	it = myvector.begin();
	myvector.insert(it + 2, anothervector.begin(), anothervector.end());
	//4.向指定位置插入一段迭代器区间
	int myarray[] = { 501,502,503 };
	myvector.insert(myvector.begin(), myarray, myarray + 3);
	cout << "myvector contains:";
	for (it = myvector.begin(); it < myvector.end(); it++)
		cout << ' ' << *it;
	cout << endl;
}

接下来我们继续来使用 erase() 函数。

cpp 复制代码
void Test9()
{
	//1.删除迭代器所指元素
	vector<int> myvector;
	for (int i = 1; i <= 10; i++) 
		myvector.push_back(i);
	vector<int>::iterator it = myvector.erase(myvector.begin() + 5);
	it = myvector.erase(it);
	//2.删除一段迭代器区间
	it = myvector.erase(myvector.begin(), myvector.begin() + 3);
	cout << "myvector contains:";
	for (int i = 0; i < myvector.size(); ++i)
		cout << ' ' << myvector[i];
	cout << endl;
}

上面这段代码的核心是利用 erase 的返回值安全删除 vector 元素,避免迭代器失效,最终实现连续删除指定位置的元素。下面逐步拆解执行过程和结果:


三、迭代器失效

1、定义

虽然看起来 vector 的 insert() 和 erase() 与 string 的没有什么区别,但是仔细观察就可以发现我们每次使用完迭代器之后都会更新,这是为什么呢?

主要还是因为我们每次插入数组都可能发生扩容,而扩容分为就地扩容与异地扩容。如果发生的异地扩容,这时的迭代器就不在指向原来的空间,而就指向一块释放的内存,我们一旦继续访问就会报错,这种现象我们称为迭代器失效。为了避免出现这种情况,所以我们在使用完迭代器之后需要更新。

2、分类

2.1、扩容后失效和插入后失效

以 insert 的 pos 为例,扩容后 pos 还指向旧空间,但是旧空间销毁了,这时 pos 就是野指针,pos 已经失效了。

cpp 复制代码
void insert(iterator pos,const T& x)
{
	assert(pos >= _start);
	assert(pos <=_finish);
	if (_finish == _end_of_storage)
	{
		//size_t len = pos - _start;//计算相对位置防止迭代器失效
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		//pos = _start + len;
	}
	for (iterator i = _finish; i >=pos ; i--)
	{
		 *i =*(i - 1);
	}
	*(pos) = x;
	_finish++;
}

所以我们扩容后要计算相对位置更新 pos。但是形参的改变不影响实参,函数内部修复,不能改变外面的实参。

cpp 复制代码
void insert(iterator pos,const T& x)
{
	assert(pos >= _start);
	assert(pos <=_finish);
	if (_finish == _end_of_storage)
	{
	    size_t len = pos - _start;//计算相对位置防止迭代器失效
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	for (iterator i = _finish; i >=pos ; i--)
	{
		 *i =*(i - 1);
	}
	*(pos) = x;
	_finish++;
}

所以这里扩容后再 insert 访问到的位置就是旧空间的位置,就会出现随机值,并且也没有在 2 的位置插入。

那是不是不扩容就没事了?

cpp 复制代码
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	print_Container(v);
	cout << endl;
	vector<int>::iterator it = find(v.begin(), v.end(), 2);
	v.insert(it, 100);
	*it*= 10;

我们原本是想让 2 *= 10,但是实际上程序却在 100 的位置 *= 10。这是因为insert后数据挪动,2 的位置后移,导致迭代器指向的位置是插入的100,这也是一种变相的迭代器失效,因为我们无法得知什么时候扩容。

所以 vs 下进行强制检查,不能继续访问,访问就报错。一句话,insert 后就认为迭代器已经失效,不要再访问了。

2.2 删除后失效

我们这里写一个删除所有偶数的程序:

cpp 复制代码
vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
	{
		v.erase(it);
	}
	it++;
}

如果我们继续访问 erase 后的迭代器就会出现各种问题。


四、default关键字

C++11 标准引入了一个新特性:default 函数。程序员只需在函数声明后加 =default,就可将该函数声明为 default 函数,编译器将为显式声明的 default 函数自动生成函数体。例如:

cpp 复制代码
vector() = default;//强制生成

default 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。

五、类名与类型

cpp 复制代码
template<class T>
void print_Container(const  vector<T>& v)
{
	//规定不能去没有实例化的类里面取东西
	//因为编译器无法区分这里的const_iterator是类型还是成员变量
	//typename就是说是类型,让程序员自己确认。用auto也可以自动识别
	vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
}

类里面类名可以替代类型。

cpp 复制代码
vector<T>& operator=(vector<T>& v)//类里面可以类名替代类型 类外面不行
{
	swap(v);
	return *this;
}

所以这个代码还可以这样写。

cpp 复制代码
vector& operator=(vector& v)//类里面可以类名替代类型 类外面不行
{
	swap(v);
	return *this;
}
相关推荐
郝学胜-神的一滴1 小时前
Linux C++系统编程:使用mmap创建匿名映射区
linux·服务器·开发语言·c++·程序人生
李余博睿(新疆)1 小时前
双向指针算法(练习)
c++
新手村领路人1 小时前
c++ opencv缺少openh264-1.8.0-win64.dll
开发语言·c++
kyle~1 小时前
C++ --- noexcept关键字 明确函数不抛出任何异常
java·开发语言·c++
lijiatu100861 小时前
[C++ ]qt槽函数及其线程机制
c++·qt
帅_shuai_1 小时前
UE GAS 属性集
c++·游戏·ue5·虚幻引擎
Juan_20121 小时前
P2865 [USACO06NOV] Roadblocks G 题解
c++·算法·图论·题解
Chrikk1 小时前
【上篇】AI 基础设施中的现代C++:显存安全 零拷贝
c++·c++40周年
止观止9 小时前
C++20 Concepts:让模板错误信息不再“天书”
c++·c++20·编程技巧·模板编程·concepts