【C++】:深入理解vector(2):vector深度剖析及模拟实现
目录
一 怎么看源代码
二 vector的部分源代码
1 迭代器类型的成员变量
2 迭代器
3 指定位置插入
4 其他
三 自己实现vector
1 头文件
2 类
3 构造函数和析构函数
4 尾插
5 capacity()
6 reverse
7 insert
8 erase
9 iterator补充
10 resize
11 深拷贝和浅拷贝
1 传统写法
2 现代写法
四 memcpy拷贝时出现的问题
编辑
五 理解动态二维数组
深入理解vector(1)链接:【【C++】深入理解vector(1):vector的使用和OJ题
一 怎么看源代码
在我们尝试去看源代码的时候,因为源代码的复用性较高,比较冗杂,对于我们这种没有足够实战经验的uu来说不是特别友好,所以我们可以借助工具去阅读源代码。
链接:Source insight
在看源代码的时候,要学会抓核心:了解内容,抓框架,注释,画图。
画图是一项很有用的功能,在面对很多比较复杂,容易搞混的代码和情况的时候,能很直观的反映问题。
二 vector的部分源代码
1 迭代器类型的成员变量
start
:指向 vector
内部数组的起始位置,即第一个元素的存储地址
start
:指向 vector
内部数组的起始位置,即第一个元素的存储地址
end_of_storage
:指向 vector
当前已分配内存空间的末尾位置
2 迭代器
3 指定位置插入
4 其他
在学习STL源代码的时候,可以让AI帮忙写相关的注释帮助理解,也可以看书:《STL源码剖析》
三 自己实现vector
1 头文件
#pragma once#include<assert.h>
2 类
namespace zz
{template<class T>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++中对内置类型初始化的一种方式
例如:
//C98int j = int();int k = int(1);
// C++11int 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<T>& v){reserve(v.capacity());for (const auto& e : v){push_back(e);}}
这里的v是v1,this是v2
2 现代写法
vector(const vector<T>& v){vector<T> 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<int>bit::vector<bit::vector<int>> vv(n);// 将二维数组每一行中的vecotr<int>中的元素全部设置为1for (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构建动态二维数组时与上图实际是一致的。