【C++】:深入理解vector(2):vector深度剖析及模拟实现 - 教程
目录
深入理解vector(1)链接:【【C++】深入理解vector(1):vector的使用和OJ题
一 怎么看源代码
在我们尝试去看源代码的时候,因为源代码的复用性较高,比较冗杂,对于我们这种没有足够实战经验的uu来说不是特别友好,所以我们可以借助工具去阅读源代码。

在看源代码的时候,要学会抓核心:了解内容,抓框架,注释,画图。
画图是一项很有用的功能,在面对很多比较复杂,容易搞混的代码和情况的时候,能很直观的反映问题。
二 vector的部分源代码
1 迭代器类型的成员变量

start:指向 vector 内部数组的起始位置,即第一个元素的存储地址
start:指向 vector 内部数组的起始位置,即第一个元素的存储地址
end_of_storage:指向 vector 当前已分配内存空间的末尾位置
2 迭代器

3 指定位置插入

4 其他

在学习STL源代码的时候,可以让AI帮忙写相关的注释帮助理解,也可以看书:《STL源码剖析》
三 自己实现vector
1 头文件
#pragma once
#include
2 类
namespace zz
{
template
class vector
{
public:
//typedef T* iterator;
using iterator = T*;
using const_iterator = const T*;
//.........
private:
iterator _start;
iterator _finish;
iterator _end_of_storage'
};
其中,使用using定义了T*重命名为iterator
在此处,using的作用和typedef是一样的,但是using比typedef的作用更广泛,这个我们后面再讲
注意:后面写的函数实现等内容都是再类中的public中实现的,后面不再赘述。
模板不能声明和定义分离
3 构造函数和析构函数
(1)构造函数
vector():
: _start(nullptr);
, _finish(nullptr);
, _end_of_storage(nullage);
{}
在 C++ 中,这个构造函数定义末尾的
{}是函数体的标志,用于表示构造函数的实现部分。具体来说:
- 前面的
vector()是构造函数的声明(因为是无参构造,所以括号内为空)。- 中间的
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)是成员初始化列表,用于在进入函数体之前初始化类的成员变量(这里将三个指针都初始化为nullptr)。- 最后的
{}是构造函数的函数体,由于成员变量已经通过初始化列表完成了初始化,且这个构造函数不需要额外执行其他逻辑,所以函数体为空。简单理解:
{}在这里表示 “构造函数的具体执行代码”,只是当前这个构造函数没有需要执行的额外代码,所以用空的函数体即可。如果后续需要在构造时添加其他操作(比如打印日志),就可以在{}内部编写代码。
(2)析构函数
~vector():
{
if(_start)
{
delete[] _start;
_start = _finish = _endd_of_storage;
}
}
4 尾插
void push_back(const T& x)
{
if(_finsh == _end_ of_storage)
{
reverse(capacity()==0 ? 4 : capacity()*2);
}
*_finish = x;
++_finish;
}
因为finish指向最后一个有效数据的下一个位置,所以先赋值,再finish++
注意:因为我们的成员函数不含capacity,所以我们需要自己实现一个capacity ,还有扩容reverse函数
5 capacity()
size_t capacity() const
{
return end_of_storage - _start;
}
6 reverse
void reserve(size_t n)
{
if(n > capacity() )
{
size_t sz = size();
T* tmp = new T[n];
if(_start)
{
memcpy(tmp, _start, sizeof(T)*sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
7 insert

但是这样写其实是有问题的,造成了迭代器失效。

在开辟了新空间里之后,_start和_finish都指向了新的空间,但是此时pos还指向的是原来的旧 空间,但是此时旧空间已经释放,所以会造成pos为野指针
修改:
iterator 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;
}
// Ų
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
修改点:用len记录pos到_start的距离,扩容完后,重新确定pos的位置
那为啥要返回pos呢:

当it传递给pos后,形参的改变不会影响实参
8 erase
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finih);
iterator it = pos + 1;
while(it != _finissh)
{
*(it-1) = *it;
++it;
}
--finish;
return pos;
}
erase和insert都会造成迭代器失效
vector的erase函数删除元素时,会执行 “元素挪动” 操作iterator it = pos + 1; while (it != _finish) { *(it - 1) = *it; // 后续元素向前挪动,覆盖被删除元素的位置 ++it; } --_finish; // 有效元素数量减少这个过程会导致两类迭代器失效:
被删除的
pos迭代器本身失效pos原本指向待删除的元素,但删除后,该位置被后续元素覆盖(或变成无效元素)。此时pos指向的内存虽然可能存在(未释放),但已不再是原来的元素,继续使用pos会访问错误的数据。
pos之后的所有迭代器失效由于pos之后的元素都向前挪动了一个位置(例如,原pos+1位置的元素移动到pos位置,原pos+2移动到pos+1位置……),原本指向这些元素的迭代器(如pos+1、pos+2等)现在指向的是 “原位置的下一个元素”,与预期不符,因此失效。
9 iterator补充
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
10 resize
调整容器有效数据个数
viod resize(size_t n, T val = T() )
{
if(n < size() )//n小于当前元素个数
{
_finish = _start + n;//finish截断多于数据
}
else
{
reserve(n);
while (_finish < _strt + n)
{
*_finish = val;
++_finish;
}
}
这个时候就有uu有疑问了,这个参数中的 T val = T()是什么
这也是C++中对内置类型初始化的一种方式
例如:
//C98
int j = int();
int k = int(1);
// C++11
int y = {};
int t = {1};
int z{ 2 };
int m{};
不带括号的类型是将变量初始化为接近0的值(可以看为是0),具体内容我们到后面再讲解,现在先让大家认识一下
11 深拷贝和浅拷贝
拷贝构造只能用引用传参,不能用传值传参
深拷贝不止要拷贝对象,也要拷贝对象所指向的内容
1 传统写法
初始化列表,显示写了,用显示的值,没有显示写,看缺省值(在private中的定义直接给值);如果都没有,也要走初始化列表(自定义类型调默认构造,内置类型可能是随机值),初始化列表是一定要写的
没有显示的写,给缺省值的情况是这样写的;
public:
vector()
{}
//.......
private:
_start = nullptr;
_finish = nullptr;
_end_of_storage = nullptr;
我们来看拷贝构造函数
// 传统写法
v2(v1)
vector(const vector& v)
{
reserve(v.capacity());
for (const auto& e : v)
{
push_back(e);
}
}
这里的v是v1,this是v2
2 现代写法
vector(const vector& v)
{
vector tmp(v.begin(), v.end());
swap(tmp);
}
不自己去开辟空间,通过他人开辟空间
四 memcpy拷贝时出现的问题
在我们前面写的reserve接口中,如果函数模板T表示的时内置类型:Int,double之类的,不会出现问题了,但如果是自定义类型呢?例如string
这个时候就会出错,因为memcpy是浅拷贝,如果有像string中含有指向其他地方的成员变量时,就会出错。
我们来看一下这个部分的详细解释:


那么我们如何修改呢?
(1)使用for循环
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i]; // 如果是string,调用string的赋值深拷贝
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
如果此时时自定义对象,那么在for循环中,就会自动调用对应的赋值运算符重载

(2)swap
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
std::swap(tmp[i], _start[i]); // 如果是string,调用string的交换,交换资源指向
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
// 如果是string,调用string的交换,交换资源指向

五 理解动态二维数组
以杨辉三角为例:

// 以杨慧三角的前n行为例:假设n为5
void test2vector(size_t n)
{
// 使用vector定义二维数组vv,vv中的每个元素都是vector
bit::vector> vv(n);
// 将二维数组每一行中的vecotr中的元素全部设置为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];
}
}
}
bit::vector> vv(n); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素 都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:

vv中元素填充完成之后,如下图所示:

使用标准库中vector构建动态二维数组时与上图实际是一致的。


浙公网安备 33010602011771号