𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇:Solitary_walk
⸝⋆ ━━━┓
- 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑
目录
问题的本质:memcpy()这个函数在进行拷贝的时候是值拷贝(不管是内置类型还是自定义类型
其实对于vector 模拟实现的学习和前面的string 是一样滴,学起来简直就是 so easy!
首先需要我们对vector 这个类有一定的了解
其实vector 就是数据结构里面的顺序表
vector 里面有三个重要的指针,以下所有的模拟实现都是基于当前这三个指针进行滴,所以对着三个指针必须要深入了解
结合草图进行理解:
为了避免和库里面的vector 发生冲突,我把自己实现的vector 这个类放在一个命名空间里面
定义一个vector类:
1: 构造函数
1)无参构造函数
vector()//无参构造函数
:_start(nullptr)
,_finish(nullptr)
,_enofstorage(nullptr)
{}
之所以选择对_start _finish _enofstorage 这三个指针进行初始化为空,就是避免在后面调用析构函数的时候导致程序崩溃
2)有参构造函数
1) 用 n 个 val 进行实例化(标准库里面,默认是使用0进行初始化的)
vector(size_t n, const T& val)
{
reserve(n);
for (size_t i = 0; i < n; i++)
push_back(val);
}
2)迭代器区间进行初始化:注意是可以在一个类里面再写一个模板的
template <class InputIterator>// 在一个类模板里面是支持在写一个模板函数的
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
各位老铁看一下,下面问题是咋回事???
问题:
当对v2这个对象进行调用构造函数的时候,是可以编译通过的;但是当对v1这个对象进行构造函数调用的时候编译失败。
分析:
v2这个对象对应调用构造函数的参数的类型是 int ,string 类型,此时编译器会自动匹配最佳的函数,所以直接调用第2个构造函数;但是对于v1这个对象的调用构造函数的参数类型是 int ,int 类型此时编译器只会直接匹配第一个构造函数,之所以不匹配第2个构造函数是因为:要进行整型的提升,那编译器还不如直接走第一个构造函数的调用,但是此时的参数是int 类型不能进行解引用的所以编译器报错:非法的间接寻址
解决:
咱直接搞一个构造函数参数的类型是int int 类型的不就OK了
3)赋值运算符重载
void swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstoreage, tmp._endofstoreage);
}
vector<T>& operator=(vector<T>& tmp)
{
swap(tmp);
return *this;
}
注意:
对于多个函数之间的调用,参数必须达到一致,不能一个使用模板,一个不使用模板
4)[ ] 运算符重载
T& operator[] (const size_t pos)
{
assert(pos < size());//防止越界
return _start[pos];
}
const T& operator[] (const size_t pos) const
{
assert(pos < size());//防止越界
return _start[pos];
}
2: 析构函数
~vector()
{
delete[] _start;//注意一定要匹配使用,在delete 的时候
_start = _finish = _enofstorage = nullptr;
}
3:size( )
size_t size()
{
return _finish - _start;//返回当前有效的数据个数
}
4: capacity( )
size_t capacity()
{
return _enofstorage - _start;//返回当前的空间容量
}
5:reserve ()
reserve()坑点一:野指针
注意:这个是错误的代码
分析:
对当下问题的纠正:
reserve()坑点二:浅拷贝
借助对象v1 对v2进行实例化,以下程序会崩溃
分析:
此时2个不同对象指向同一块空间,在调用析构函数的时候,v2这个对象先销毁(符合先创建的对
象后析构),当v1调用析构函数的时候,此时所指向的空间已经归还系统,因此出现野指针,造成
程序的崩溃
其实不仅仅是在当下这个场景面临浅拷贝问题,当我们实例化对象是自定义类型的时候,也会出现
浅拷贝问题
问题的本质:memcpy()这个函数在进行拷贝的时候是值拷贝(不管是内置类型还是自定义类型
数据)
解决:对数据进行深拷贝
//memcpy(tmp, _start, sizeof(T) * size());//err 浅拷贝
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
正确代码:
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();//提前记录扩容之前的size()
T* tmp = new T[n];
// 数据拷贝
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * size());//err 浅拷贝
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;//注意一定要匹配使用
}
//更新
_start = tmp;
_finish = _start + sz;
_enofstorage = _start + n;
}
}
6:resize ()
正确代码:
思考以下几个问题:
1) 为什么要调用构造函数进行指定的初始化 直接默认用0进行初始化不OK???
2)可以不用引用吗
1)之所以使用构造函数进行初始化是因为,并不确定当下T的类型是否为int,还是其他类型:char vector<vector<int>>……
2)可以不使用引用第二个参数这样写也是OK的
const T val = T()
3)resize()reserve()函数区别:
相同点:都是进行扩容
不同点:
resize()扩容的同时会进行空间的初始化,默认初始化为0,会影响当前实际元素的个数多少;
reserve()不会进行初始化,不会影响当前的实际元素个数,只是提前进行空间的申请,是否真正的
使用取决于实际的需要。
7: push_back()
void push_back(const T& v)
{
// 是否扩容
// _finish 进行++
if (_finish == _enofstorage)
{
size_t sz = size();//记录扩容之前的size() 避免后续野指针的出现
size_t cp = capacity() == 0 ? 4 : 2 * capacity();
T* tmp = new T[cp];
delete[]_start;//旧空间释放
if(_start)// 数据拷贝
{
memcpy(tmp, _start, sizeof(T) * size());
}
// 扩容之后的更新
_start = tmp;
_finish = _start + sz;
_enofstorage = _start + cp;
}
//数据插入
*(_finish) = v;
++_finish;
}
8:insert()
相信有很多老铁们会这样写代码吧(俺一开始就是这样搞滴)
错误代码:
遇到问题不慌不忙,咱调试走起,瞧瞧咋回事
分析:迭代器失效
扩容前后 pos 所指向的位置是不一样的;一旦进行空间的扩容,会新开一段空间,拷贝原来的数
据,之后原来的空间进行释放,所以此时 pos 就会指向一个非法位置。
通过调试我们发现,在没有扩容之前程序是可以正常跑起来的,但是当涉及到扩容时,就出现了
随机值。
其实这就是我们所说的迭代器的内部失效的问题,要想再对迭代器更改后进行使用,我们可以记录
一下pos 迭代器相对于起始位置的偏移量,下一次再使用的时候对pos 迭代器进行更新即可
运行结果:
正确代码:
void insert(iterator pos, const T& val)
{
//默认pos 位置之前进行插入
// pos 位置是否合法
// 是否扩容
assert(pos >= _start);
assert(pos < _finish);
if (_finish == _enofstorage)
{
size_t sz = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = sz + _start;//更新迭代器
}
iterator end = _finish;
while (end > pos)
{
*end = *(end - 1);
--end;
}
*pos = val;
++_finish;
}
9: erase()
咱话不多说,代码先跑起来,康康!
可能有的老铁看到当前的运行结果觉得很满意:不错不错是我预期的(试过了各个位置的删除)
但是:也有些敏锐的老铁,会觉得有了上面insert()函数给我们的经验,erase() 函数也会涉
及到迭代器实失效的问题
通过以下程序的分析结果变可以看出:
正确代码:
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
//数据覆盖
vector<int>::iterator end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
}
while (pos != v1.end())
{
if (*pos % 2 == 0)
v1.erase(pos);// 删除之后自然返回挪动数据之后的对应位置的迭代器,从而避免连续偶数的时候有没有删掉的
else
{
++pos;
}
}
运行结果:
通过对insert() erase() 调用我们发现:都会出现迭代器失效的问题
10:迭代器相关函数模拟
typedef T* iterator; // 默认受到类域的限制,所以默认属性是private
typedef const T* const_iterator;
iterator _start; // 指向数据的起始位置
iterator _finish; // 指向最后一个数据的下一个位置
iterator _enofstorage;// 指向剩余容量的末尾位置
//迭代器相关实现
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;;
}
结语:
以上就是要share 的内容,对于初次模拟实现vector的小白而言,还是多多少少有些坑注定
少不了的,所以说咱也不要害怕踩坑。其实对这个vector模拟实现重点就是迭代器相关的使用以及
理解,所以说咱还是多多敲敲代码,看看stl库里面相关的资料介绍,希望各位大佬们都有所收
获!