C++之vector类(超详解)
这节我们来学习一下,C++中一个重要的工具——STL,这是C++中自带的一个标准库,我们可以直接调用这个库中的函数或者容器,可以使效率大大提升。这节我们介绍STL中的vector。
文章目录
前言
一、标准库类型vector
二、vector的使用
2.1 vector的初始化和定义
2.2 vector的元素访问
2.2.1 vector iterator的使用
2.3 vector的空间增长问题
2.4 vector增删改查
2.5 vector迭代器失效问题
三、vector的深度剖析与模拟实现
3.1 成员变量及迭代器实现
3.2 使用memcpy的拷贝问题
3.3 动态二维数组的理解
3.4 vector的模拟实现(代码)
前言
我们在上一节中学习了string类,我们通过这个类可以实现各种各样的操作。然而string类是专门设计来用来处理字符类型的,它是用来对字符的高级处理以及内存优化,适用于文本数据。但是在本节中我们将要的vector,是一个通用的容器类,它可以存储任意类型的数据,适用于处理任意类型的集合。
一、标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都是相同的。集合中每一个对象都有对应的索引,索引用于访问对象。因为vector中容纳着其他对象,因此我们也将它称为容器(container)。vector其实与我们C语言学习的数组很像,存放一组相同数据类型的集合,我们可以通过索引值来访问某个值,但是vector通过了类进行了封装,于是我们可以使用vector进行各种操作,可以任意改变存储空间的大小,这是我们前面的数组所没有,大大提高了操作的灵活性。
C++语言既有类模板(class template),也有函数模板,其中vector就是一个类模板。模板我们在之前就已经学习过了,模板并不是一个类或者函数,它们只是一个类或者函数的雏形,我们可以使用这些模板来实例出来一个具体的类或者函数。因此我们对于vector的数据类型要注意了:vector是模板并非类型,由vector生成的类型必须包含vector中的元素类型,例如vector中的元素类型是int,那么它的数据类型就是vector<int>。
vector可以容纳大多数类型的对象作为其元素,但是因为引用并不是一个对象,因此我们不存在包含引用的vector,其他大多数的内置类型(非引用)和类类型都可以构成vector对象。
二、vector的使用
在使用vector之前,我们要先包含一个头文件:#include<vector>和using namespace std这个标准库命名空间。vector的函数都放在这里面,如果我们没写这个头文件,编译器就不认识vector了。
2.1 vector的初始化和定义
同样地,vector有着属于自己的构造函数,vector要存储数据,因此它肯定是要进行资源的申请的。它的构造函数同样很多,主要的就是我们上面所展示的哪几种。我们通过VS2022上的监视调试可以看到,我们可以直接通过vector中的那几个构造函数来进行初始化,它也赋上了相应的值。
除了上面那几种构造方法,在C++11新标准中还提供了另一种为vector对象的元素赋初值的方法,即列表初始化。列表初始化和我们之前对数组初始化的方式很像,都是使用一对{ },然后我们将要初始化的内容放进去。我们可以直接使用{ }初始化,也可以将内容放到{ }中,然后赋值给vector对象。然后编译器就会自动创建相应的大小空间以及赋上我们给定的初始值。
这时可能会有人来问:这个列表初始化,会不会与我们上面那个初始化n个相同的值弄混呢?
这里我们要注意一下:在某些情况下,初始化的真实含义依赖于传递初始值使用的是{ }还是()。
例如,我们使用一个整数来初始vector<Int>时,整数的含义可能是vector的对象容量,也可能是vector的元素的值。类似的,我们如果使用两个整数来初始化vector<int>的话,那么这两个整数就可能是一个是vector的容量大小另一个是要初始化的值,也可能它们是容量为2的vector对象的两个元素的初始值。我们可以通过花括号{ }和圆括号()来区分上面的含义:
vector<int> v1(10) //v1有10个元素,每个元素的初始值都是0
vector<int> v2{10} //v2有1个元素,这个元素的初始值是10
vector<int> v3(10,1) //v3有10个元素,每个元素的初始值都是1
vector<int> v4{10,1} //v4有2个元素,分别是10和1
如果我们使用的是圆括号的话,那么可以说提供的值是用来构造(construct)vector对象。如果我们使用的是花括号的话,那么可以表述为我们想列表初始化该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成元素初始值的列表来进行处理,只有无法执行列表初始化时才会考虑其他初始化方法。另一方面,如果初始化时使用了花括号但是花括号中的内容不适合进行列表初始化,那么这时候就要考虑使用这样的值使用其他构造方法来构造vector对象了。例如,我们如果想要初始化一个含有string类型的元素的vector对象,我们应该赋给能够赋值给string对象的初始值。
vector<string> v5{"hi"} //列表初始化,v5中只有一个元素“hi"
vector<string> v6("hi") //错误,我们不能使用字符串字面值来构造一个vector对象
vector<string> v7{10} //v7中有10个默认值(string类型的)的元素
vector<string> v8{10,”hi“} //v8中有10个”hi“的元素
在之前,我们学习了,我们不能使用一个字面值来进行初始化了,因为字面值是一个常量,是一个不可以进行修改的值,如果我们使用这个来进行初始化,相当于将一个只读的元素放到了一个可读可修改的容器中,那样就造成了权限放大的情况,这样是不允许的。因此,我们就不可以使用()来初始化string类了,如果我们想要初始化多个相同的string对象,我们可以使用{ },同样地第一个参数放容量大小,第二个参数放初始化的值。
2.2 vector的元素访问
我们在string类中有三种访问元素的方法:1.使用普通for循环进行遍历访问;2.使用范围for进行遍历循环;3.使用迭代器进行访问。对于vector。同样适用于上面那三种方法。在此之前,我们先来学习一下vector中的迭代器。
2.2.1 vector iterator的使用
vector中的迭代器和string类中的迭代器大致一样,都是指向某个位置的元素。begin()函数是指向vector中首元素的迭代器,end()函数是指向vector中最后一个元素的下一个位置。至于rbegin(),rend()函数则是方向遍历vector。其具体使用方法,我们在string类中已经详细介绍过了。范围for是在迭代器的基础上实现的,这个一般适用于不知道容器中的元素的个数时。
元素访问的三种方式代码如下:
vector<int> v1{ 1,2,3,4,5 };
//1.使用普通for循环进行遍历访问v1
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
//2.使用范围for进行遍历访问
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
//3.使用迭代器进行访问
vector<int>::iterator it = v1.begin();
while (it!=v1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
2.3 vector的空间增长问题
string有的那些接口vector同样有,而且它们的功能基本都是一样的,不过是处理的数据类型不同而已,这里我们就不重复介绍这些接口的使用方法了。
我们主要讲讲上面那两个改变size和capacity的接口,这两个的实现原理有所不同:对于resize,它是与原来的那个vector的size值进行比较一下,如果是小于原来的size值,那么它就会删去一些原有的数据,如果大于原来的size值,它会补充我们给定的缺省值,如果大于我们原来的capacity,编译器将会给它重新分配一个内存空间了,这里往往也调用了reserve中的扩容操作。对于reserve,一般都是编译器自己执行这个函数的,我们在插入数据的时候,编译器会调用这个函数来进行扩容,有时候如果我们能够提前知道数据的个数,那么我们也可以使用reserve来提前预留空间大小,这样就能够缓解vector增容的代价缺陷问题,注意reserve一般都是异地创建一个新的内存空间,然后将我们的内容深拷贝过去,并非是在原来的空间中来扩容。
2.4 vector增删改查
上面的那些接口,我们可以实现vector的增删改查,大大提高了其灵活性,我们可以对vector对象中的数据进行修改。其中的find接口,我们需要注意一下,它并不是vector的成员接口,它是在算法库中的,但是我们可以使用vector来进行调用。
这是其在算法库中的函数原型,我们可以看到,它是一个函数模板,因此它并不是一个具体的函数,而是一个可以对于任意类型使用的函数出翔,我们可以传参实例化出来一个具体的函数来进行使用。上面的函数原型中,它的参数是两个迭代器和一个想要查找的值。对于那两个迭代器所表示的区间是:[first,last)。如果我们没有在那段区间中找到我们想要查找的值时,编译器就会返回last迭代器,用于表示未查找出来的情况。
2.5 vector迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T* (因为,对于vector的底层是一个数组)。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。在vector中迭代器失效有好几种,接下来我们一一介绍一下:
1.修改vector的大小(如使用resize)
我们在前面就已经说过了对于那些我们设置的新size值如果比原来的size小的话,那样就会导致超出范围的元素被删除,那么与那些超出范围的元素有关联的迭代器就可能会失效。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.resize(2); // 删除了第三个元素,导致 it 失效,那么我们就不能够使用迭代器,来访问到这第三个元素了
2.vector容量的改变(如使用insert,push_back)
vector在内存空间不够的情况下会自动进行扩容操作,当我们调用insert和push_back时,编译器就会进行扩容,我们在上面已经说过了编译器是异地扩容,在其他地方另外开辟一个新的空间,那么原来的那些迭代器就都会失效,因为它们所指向的位置都已经被改变了。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 如果触发扩容,it 会失效,因为我们开辟了一个新的内存块,原来的那些迭代器所指向的位置都已经改变了
3.erase操作
使用erase删除vector中的元素时,会导致被删除位置后的所有元素都会被移动,那样就会导致逻辑错误,原来的指向那些元素的那些迭代器也会失效。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.erase(it); // 删除了第一个元素,it 失效,第一位位置后面的元素都往前移动一位,后面的迭代器所指的元素可能都发生改变了,导致逻辑错误
4.clear操作
使用clear清除ector中的所有元素,会导致vector中有的迭代器全部都失效。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.clear(); // 所有迭代器都失效
5.insert操作
这个与上面的erase差不多,当我们插入某个位置之后,那个位置之后的所有元素都会发生改变,因此那些指向的迭代器就会改变,如果我们插入大量元素或触发内存扩展时,可能导致所有的迭代器都失效。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.insert(it, 0); // 插入元素导致 it 失效
6.swap操作
当我们调用swap函数改变两个vector的内容和容量时,可能会导致两个vetcor中原有的迭代器都失效了。
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {4, 5, 6};
auto it = vec1.begin();
vec1.swap(vec2); // it 失效,因为 vec1 和 vec2 的内存交换了
7.底层分配器的改变
如果你使用自定义的分配器(allocator)来管理vector的内存,某些内存分配或重新分配的策略也可能会导致迭代器失效。因为分配器所作用的对象就是内存,内存都改变了,原来的迭代器就不能指向原来的那些内容了,因此会失效。
上面这么多情况都会可能导致迭代器失效,因此我们在使用那些增删函数时要小心一点。如果需要避免迭代器失效,我们可以考虑使用其他容器,或者在修改vector时重新获取有效的迭代器(对迭代器进行一个更新),这样做之后我们就可以获取我们所需的迭代器了。
//string类中与vector类似,也会遇到迭代器失效的问题,我们可以使用如下方法来规避
#include <string>
void TestString()
{
string s("hello");
auto it = s.begin();
// 放开之后代码会崩溃,因为resize到20会string会进行扩容
// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
// 后序打印时,再访问it指向的空间程序就会崩溃
//s.resize(20, '!');
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
it = s.begin();
while (it != s.end())
{
it = s.erase(it);
// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
// it位置的迭代器就失效了
// s.erase(it);
++it;
}
}
三、vector的深度剖析与模拟实现
3.1 成员变量及迭代器实现
上面是由源文件中截取的源码,我们可以看出来vector的成员函数是三个迭代器,它们分别指向上面那些位置。我们在最开始学习的时候就说了,vector的底层实现是一个数组,为啥我们不像之前模拟实现顺序表那样设置一个数组,一个size值和一个capacity作为成员变量呢?其实这些在源文件中都已经有提及到了,在源文件中我们可以通过上面那三个指针来表示这几个变量。使用这些迭代器能够更加直观地看到vector中的具体位置。
在vetor中的底层中,它直接将那些数据类型的指针重名为迭代器了,因此,我们如果要实现那些begin(),end()函数直接使用我们开始定义的那几个迭代器变量表示就可以了。
3.2 使用memcpy的拷贝问题
我们在string类模拟实现时,我们对于两个字符串的拷贝是使用strcpy这个专门用来字符串拷贝的函数,对于其他类型的变量拷贝,我们就需要使用其他拷贝函数,而memcpy就是我们在C语言阶段学习的一个用来内存拷贝的函数,它可以将内存及内容拷贝过去。memcpy拷贝有如下两个特点:
1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存 空间中 。
2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型 元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
在C语言中,我们没有涉及到析构这一说,但是到了C++中,我们学习了析构函数,我们知道我们每次函数在销毁时都会自动调用析构函数来释放内存资源,这里我们使用了memcpy这个浅拷贝函数获取的那个对象,由于我们的一些函数会将拷贝前的那个对象释放掉,那么就会调用析构函数,那么就会将原来的那个空间释放掉,但是我们拷贝后的那个新对象仍然是指向那个旧空间的地址,这样我们新对象的地址就是一个被释放的空间地址,相当于一个野指针,野指针是十分危险的。因此我们不能使用这个浅拷贝的函数,我们需要自己来实现深拷贝(对于那些有资源申请的对象)。
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为 memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
3.3 动态二维数组的理解
二维数组这一概念,我们是在C语言阶段就已经接触到了。那么二维数组是如何实现的呢?二维数组其实就是一个数组中存放了一组数组的指针,这样就形成了二维数组。而vector的底层实现就是数组,于是我们也可以使用vector来表示一个二维数组,我们可以使用嵌套的方式来进行表示。例如vector<vector<int>>,这个就表示一个元素类型为int的二维数组。如下代码,我们使用vector来表示一个杨辉三角,杨辉三角的实现是一个二维数组。
// 以杨辉三角的前n行为例:假设n为5
void test2vector(size_t n)
{
// 使用vector定义二维数组vv,vv中的每个元素都是vector<int>
vector<vector<int>> vv(n);
// 将二维数组每一行中的vecotr<int>中的元素全部设置为1
for (size_t i = 0; i < n; ++i)
vv[i].resize(i + 1, 1);
// 给杨慧三角出第一列和对角线的所有元素赋值
for (int i = 2; i < n; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
}
vector<vector<int>> vv(n); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素 都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:
vv中元素填充完成之后,如下图所示:
3.4 vector的模拟实现(代码)
#pragma once
#include<assert.h>
namespace hjc
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
template <class InputIterator> //类模板中也可以定义函数模板
vector(InputIterator first, InputIterator last ) //这两个参数的类型是迭代器类型
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector()
{}
vector(initializer_list<T> il)
{
reserve(il.size());
for (auto& e : il)
{
push_back(e);
}
}
vector(size_t n, const T& val=T())
{
reserve(n);
for (size_t i = 0; i <n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
//拷贝构造
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
~vector()
{
if (_start)
delete[]_start;
_start = _finish = _end_of_storage = nullptr;
}
T& operator[](size_t i) //我们使用下标运算符,最终返回的是一个元素,因此它的类型是元素的类型
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
void resize(size_t n, T val = T()) //我们第二个参数直接写元素的类型即可, 然后我们初始化为默认初始值
{
if (n < size())//缩小size,那么size的值就到_start + n位置即可
{
_finish = _start + n;
}
else
{
reserve(n); //扩大size,我们要往里面添加值
while (_finish< _start+n) //size=_finish-_start且_start=0于是_finish=size。这里_finish=4
{
*_finish = val;
++_finish;
}
}
}
//扩容
void reserve(size_t n)
{
if (n >= capacity())
{
size_t oldSize = size();
T* tmp = new T[n];
for (size_t i = 0; i < oldSize; i++)
{
tmp[i] = _start[i];
}
delete[]_start;
_start = tmp;
_finish = _start + oldSize;
_end_of_storage = _start + n;
}
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = x;
_finish++;
}
void swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_end_of_storage, tmp._end_of_storage);
}
bool empty()
{
return _start == _finish;
}
void pop_back()
{
assert(!empty());
_finish--;
}
iterator insert(iterator pos, const T& x) //这里的位置起始位置是1
{
assert(_start <= pos && pos <= _finish);
if (_finish==_end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator i= _finish - 1;
while (i>=pos)
{
*(i + 1) = *i;
i--;
}
*pos = x;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(_start <= pos && pos < _finish);
iterator i = pos + 1;
while (i<_finish)
{
*(i - 1) = *i;
i++;
}
_finish--;
return pos;
}
private:
iterator _start=nullptr;
iterator _finish=nullptr;
iterator _end_of_storage=nullptr;
};
void test_vector1()
{
vector<int> v1;
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); i++)
{
cout <<v1[i]<< " ";
}
cout << endl;
for (int i : v1)
{
cout << i << " ";
}
cout << endl;
v1.pop_back();
v1.pop_back();
vector<int>::iterator it = v1.begin();
while (it!=v1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
void test_vector2()
{
vector<int> v1;
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
cout << v1.size() << endl;
cout << v1.capacity() << endl;
cout << endl;
v1.resize(2);
cout << v1.size() << endl;
cout << v1.capacity() << endl;
for (int i : v1)
{
cout << i << " ";
}
cout << endl;
cout << endl;
v1.resize(6,999);
cout << v1.size() << endl;
cout << v1.capacity() << endl;
for (int i : v1)
{
cout << i << " ";
}
cout << endl;
cout << endl;
vector<int> v2;
v2.push_back(1);
v2.push_back(1);
v1.swap(v2);
for (int i : v1)
{
cout << i << " ";
}
cout << endl;
cout << endl;
for (int i : v2)
{
cout << i << " ";
}
cout << endl;
}
void test_vector3()
{
vector<int> v1={ 1,2,3,4 }; //列表初始化
//vector<int> v1 { 1,2,3,4 }; //列表初始化
vector<int>v2(v1);
for (auto i : v1)
{
cout << i << " ";
}
cout << endl;
for (auto i : v2)
{
cout << i << " ";
}
cout << endl;
vector<int>v3(10, 1);
for (auto i : v3)
{
cout << i << " ";
}
cout << endl;
v2 = v3;
for (auto i : v2)
{
cout << i << " ";
}
cout << endl;
for (auto i : v3)
{
cout << i << " ";
}
cout << endl;
}
void test_vector4()
{
vector<int> v1 = { 1,2,3,4,5,6};
vector<int> v2(v1.begin(), v1.end()); //使用函数模板实例化出来的函数进行初始化
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
v1.insert(v1.begin()+2, 30);
v1.insert(v1.begin(), 30);
v1.insert(v1.begin()+8, 30);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
v1.erase(v1.begin());
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
int x;
cin >> x;
auto i = find(v1.begin(), v1.end(), x);
if (i != v1.end())
{
//对于这种改变了原来指定的顺序,可能会导致逻辑错误,因此我们要更新一下,才能够进行访问
i = v1.insert(i, 10 * x); //如果不是我们所输入的数,就将它扩大十倍放到指定位置(找到那个数的位置)
cout << *i << endl;
}
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
//删除v1中的所以偶数
auto e = v1.begin(); //确定起始位置
while (e!=v1.end()) //然后通过刚刚定义的位置来进行遍历
{
if (*e % 2 == 0)//如果为偶数,就进行删除,对于指定位置删除,我们要更新它的位置,即到删除元素的位置
{
e = v1.erase(e);
}
else //如果不是偶数,我们就直接跳过去
{
++e;
}
}
for (auto i : v1)
{
cout << i << " ";
}
cout << endl;
}
void test_vector5()
{
vector<string>v1;
v1.push_back("111111111111111");
v1.push_back("111111111111111");
v1.push_back("111111111111111");
v1.push_back("111111111111111");
v1.push_back("111111111111111");
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
}