主页:HABUO主页:HABUO
C++入门到精通专栏
如果再也不能见到你,祝你早安,午安,晚安
目录
一、vector的介绍
二、vector的使用
✨2.1 vector的定义
✨2.2 vector iterator (迭代器)的使用
✨2.3 vector 空间增长问题
✨2.4 vector 修改
✨2.5 迭代器失效问题
三、vector的简单模拟实现
四、总结
前言:
上篇博客我们了解了STL中的string类,本篇博客我们继续对STL中的容器进行学习,这篇博客我们来了解vector,学习思路与string是一样的,第一步就是是什么怎么用,第二步就是我们能不能自己实现一个简单的vector(即为了了解它的底层原理),经过这两步的学习之后,应对绝大多数的场景已经足够使用。经过上篇博客string的学习,vector是相对比较轻松的,因为各种接口的使用和实现逻辑是相差不大的。希望大家有所收获!
核心概念:动态数组
vector
是 STL 中最常用、最基础的序列容器之一。
它本质上是一个动态数组。这意味着:
内存连续: 它的元素在内存中是连续存储的(就像普通数组一样)。这是它最核心的特性,带来了高效的随机访问能力。
动态大小: 它的大小(size()
)可以在运行时根据需要自动增长或缩减。你不需要像使用原生数组那样预先指定一个固定的大小,或者手动管理内存的重新分配。这是它相对于原生数组的最大优势。
它被定义在
头文件中。
它是一个类模板,你需要指定它要存储的元素类型:
#include
std::vector myIntVector; // 存储 int 的 vector
std::vector names; // 存储 string 的 vector
std::vector objects; // 存储自定义类 MyClass 对象的 vector
总结:
事实上vector就类似于我们数据结构中的顺序表,里面可以存放各种类型的数据,当然同一个vector中只能存放同种类型的数据,string就有点vector中存放字符的韵味,但是它们还是有差别的,例如 '\0'
类似于string,我们学习使用主要针对的就是,定义:构造(无参的有参的)、拷贝构造。修改:insert、erase、push_back、pop_back、resize、reserve、operator=。输出:operator[]。结束:析构。下面我们;来一一介绍。
构造函数声明 | 接口说明 |
---|---|
vector() (重点) |
无参构造 |
vector(size_type n, const value_type& val = value_type()) |
构造并初始化 n 个 val |
vector(const vector& x) (重点) |
拷贝构造 |
vector(InputIterator first, InputIterator last) |
使用迭代器进行初始化构造 |
vector v1;
v1.push_back(1);
v1.push_back(2);
vector v2(v1);
接口名称 | 接口说明 |
---|---|
begin + end (重点) |
begin :获取第一个数据位置的 iterator/const_iterator |
rbegin + rend |
rbegin :获取最后一个数据位置的 reverse_iterator |
//迭代器遍历方式
vector::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it;
++it;
}
cout << endl;
接口名称 | 接口说明 |
---|---|
size |
获取数据个数 |
capacity |
获取容量大小 |
empty |
判断是否为空 |
resize (重点) |
改变 vector 的 size |
reserve (重点) |
改变 vector 的 capacity |
vector v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.resize(4);
v1.resize(6);
v1.resize(8, 2);
这里resize与reserve使用与string没有什么区别,唯一需要注意的就是这里提供了size与capacity的接口,这是因为vector底层私有变量仅有三个变量都是指针,没有像string那样直接提供size与capacity。
补充知识:
对于容量部分:
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。 具体增长多少是根据具体的需求定义 的。vs是PJ版本STL,g++是SGI版本STL。
它们是各有利弊,像1.5倍是空间利用率高了但是效率相对于2倍是更低了。
接口名称 | 接口说明 |
---|---|
push_back (重点) |
尾插 |
pop_back (重点) |
尾删 |
find |
查找(注意:这是算法模块实现的,不是 vector 的成员接口) |
insert |
在 position 之前插入 val |
erase |
删除 position 位置的数据 |
swap |
交换两个 vector 的数据空间 |
operator[] (重点) |
像数组一样访问 |
vector v1;
v1.push_back(1);
v1.push_back(2);
v1.pop_back();
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i];
}
cout << endl;
注意vector没有提供find接口我们需要用到算法当中的find:
vector
::iterator pos = find(v1.begin(), v1.end(), 2);//注意这个函数的返回值如果没找到会返回v1.end()即所给范围的最后一个 if (pos != v1.end()) { v1.erase(pos); }
迭代器失效: 重要! 以下操作会使指向 vector 元素的迭代器、指针和引用失效:
任何导致 reallocation 的操作(如 push_back
导致容量不足时、resize
增大超过容量、reserve
小于当前容量等)。
在迭代器指向位置之前进行 insert
或 emplace
。
对迭代器指向位置或其之后的元素进行 erase
。
使用失效的迭代器等会导致未定义行为。务必小心操作后迭代器的有效性。
如:
情况1
vector::iterator it = v1.begin();
v1.push_back(5);
v1.push_back(6);//push_back、insert、resize、reserve等只要涉及扩容都有可能导致迭代器失效的问题
while (it != v1.end())//这里就会遇到迭代器失效的问题,本质就是你迭代器已经给了it,但是你再插入数据,是不是就有可能
{ //导致扩容,一经扩容,原来的it所指向的空间还在吗?是不是不在了,你再访问当然就错了
cout << *it; //所以当使用迭代器的时候一定要在这之前将数据弄好
++it;
}
cout << endl;
情况2
//假如我们想删除上述数据中的偶数
vector::iterator it = v1.begin();
while (it != v1.end())
{
//if (*it % 2 == 0)//可以发现也是报错的,原因就在于,例如删除2时,后面的数据紧接着就往前移,但是我们下面就进行了++
// v1.erase(it);//那3还进行了检查吗?当然没有,如果这个也是个偶数,那就直接跳过去了,以此编译器直接检查了出来
//++it;
//else
// ++it;//这样写也是报错的,但是这样写在linux下是不会报错的,而且是正确的,但是一段代码总不能在windows是错误的
//在linux下是正确的吧,所以这种写法本质就是错误的
//所以错误的本质原因是迭代器失效:当调用 v1.erase(it) 删除元素时:
//被删除元素之后的所有元素会向前移动(内存位置改变)
//it 迭代器立即失效(指向无效内存)
//后续操作 ++it 或解引用* it 会导致未定义行为(程序崩溃或数据错误)
//逻辑错误:
//删除元素后,容器大小减小,但循环仍尝试递增已失效的迭代器
//若删除最后一个元素,it 会指向 end() 之后的位置,++it 将越界
if (*it % 2 == 0)
it = v1.erase(it);//使用 erase() 的返回值更新迭代器。erase(it) 返回指向被删除元素下一位置的有效迭代器。
else
++it;
}
简单学习之后大家可以通过以下几道题检验一下自己:
只出现一次的数字i
只出现一次的数字 II
只出现一次的数字 III
杨辉三角
reserve和resize
void reserve(int n)
{
if (n > capacity())
{
size_t sz = size();//这里要注意为什么在这里给了个sz,因为如果下面再调用size()来获取大小,
T* temp = new T[n];//一旦_start变了,size()函数调用就会有问题,因为外面size本质就是通过两个指针相减来获取的
if (_start)
{
for (size_t i = 0; i < sz; ++i)
{
*(temp + i) = *(_start + i);
}
delete[] _start;
}
_start = temp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
void resize(size_t n, const T& val = T())//模板参数给缺省值就是这样给的
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
}
insert与erase
void insert(iterator pos, const T& n)
{
assert(pos <= _finish);
if (_finish == _endofstorage)
{
size_t npos = pos - _start;
size_t newcapacity = capacity() == 0 ? 2 : 2 * capacity();
reserve(newcapacity);
pos = _start + npos;
}//这里会出现pos失效的问题,原因还是因为开辟了新的空间但是pos却没有进行更新
iterator end = _finish - 1;
while (pos <= end)
{
*(end + 1) = *end;
--end;
}
*pos = n;
++_finish;
}
iterator erase(iterator pos)
{
assert(pos < _finish);
T* it = pos;
while (it < _finish)
{
*it = *(it + 1);
++it;
}
--_finish;
return pos;
}
需要格外注意我们之前string用了memcpy等一系列接口,但是要注意这些接口用是有一定代价的,非常容易导致错误。本质原因就在于,mem一系列的接口是按照字节进行操作的。
如:
int a[10]; memset(a, 0,sizeof(int)* 10); memset(a,1,sizeof(int)* 10); memset(a,2,sizeof(int)* 10);
对于第二个,我们希望它给我们设置成10个1,但是是吗?肯定不是因为按字节进行处理
00000001 00000001 00000001 00000001,这个数是不是一个很大的数,怎么可能是1?
注意小知识:
在C++中,类的成员函数可以访问该类的所有成员,包括私有成员(private),无论这些成员是属于哪个对象。也就是说,在`vector
::operator=`这个成员函数内部,不仅可以访问当前对象(即`this`所指的对象)的私有成员,也可以访问同类型其他对象(比如参数`v`)的私有成员。
更详细的请见本人代码库:
https://gitee.com/hanaobo/c-learningcode/tree/master/vector_simulate
本篇博客我们了解学习了veector。 std::vector
是 C++ STL 中的动态顺序容器,内部以连续内存存储元素,支持随机访问、自动扩容。常用构造:无参、n 个 val、拷贝、迭代器区间。遍历可用迭代器(begin/end、rbegin/rend)或 operator[]。容量接口:size、capacity、empty、reserve(预分配)、resize(调 size)。修改接口:push_back / pop_back、insert / erase(配合迭代器使用)、swap。注意迭代器失效:扩容或 erase 后原迭代器可能悬空,erase 需接收返回值更新迭代器。模拟实现要点:三指针(_start、_finish、_endofstorage);reserve 重新分配并拷贝旧数据;resize 按需填充或截断;insert 检查扩容并重置 pos;erase 移动元素并返回下一位置。