目录
一.vector的成员变量
在string的模拟实现是将capacity和size直接作为成员变量,而vector不是这样的,可以先来看看vector源代码
从上图可以看出来vector的成员变量就start,finsh和end_of_storage三个迭代器,start其实就是begin(),而finsh迭代器是end()也就是最后一个有效字符的下一位,end_of_storage是整个vector数组的最末尾的下一个,给个大致他们关系的图示
从上图可以看出来size()就是finish减start,而capacity就是end_of_storage减start
二.迭代器
vector迭代器的实现与string类似,功能都是指针一样的功能,所以直接用指针来封装迭代器。
typedef T* iterator;//迭代器统一封装成iterator
typedef const T* const_iterator;//常量迭代器
typedef const T* const_iterator;//反向迭代器
begin()和end()就是start和finish所以直接返回就可以了(下图所示)
因为还有const_iterator常量迭代器,不过也就是加个const而已,不让修改指向的内容
cbegin和cend与begin之类的类似,唯一的区别在于const系列的迭代器限制不能修改指向的空间的内容。所以这个成员函数末尾要加上cosnt。不能写成iterator string::const cbegin(),const在类型之后指的是指针本身不能改变,也就是不能加减偏移量之类的,但是指向的内容还是可以改变的。
rbegin与begin的区别在于rbegin指向的是最后一个有效字符(end减1),rend指向的是第一个字符的上一个位置
三.容量相关函数的实现
size()函数
size上面讲过其实就是finish减start,其实也可以end()减begin()
capacity函数
capacity是end_of_storage减start
reserve函数
reserve预留空间,因为还没学到内存池,所以直接用new来开辟空间。
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* newptr = new T[n];
if (start)
{
for (int i = 0; i < size(); i++)
{
newptr[i] = start[i];
}
delete[]start;
}
start = newptr;
finish= start + oldsize;
end_of_storage = start + n;
}
}
if (n > capacity())首先要考虑的是原版vector预留空间小于原本的capacity是无法完成预留空间缩容的,只能完成扩容。所以加限制条件if (n > capacity()),原版reserve缩容情况如下图
size_t oldsize = size();
T* newptr = new T[n];为什么要有个oldsize这是为了后面确定新finish和end_of_storage的位置做准备,因为扩容后旧的空间就销毁了,而finish和end_of_storage还指向原来的旧空间,所以把他们到新空间上。
T* newptr = new T[n]; 具体开辟新空间,T* newptr = new T[n]; 和T* newptr = new T(n);区别之处就在于T[n]就开辟一个大小为n的数组,数组里的类型都相同为T,会调用T类型的默认构造函数数n次,T(n)是开一一个T类型大小为n的单一对象,用于创建单个对象,只会调一次构造函数,而是n是作为构造函数的参数传递的
if (start)
{
for (int i = 0; i < size(); i++)
{
newptr[i] = start[i];
}
delete[]start;
}这段代码是将旧空间的数据复制到新空间去,然后释放旧空间,因为start实际上指向的是空间的开头,所以可以通过它来访问到整个旧空间,如果它为空说明之前压根就没分配空间,自然也就没有旧空间数据复制了
start = newptr;
finish= start + oldsize;
end_of_storage = start + n;这三句是更新成员变量,start指向旧空间开头,数据已经复制到新空间,而且旧空间已经释放了,所以直接让start指向新空间就行了。finish此时还指向旧空间,start已经确定了,finish是不能直接一步到位指向有效字符的下一个的,所以用上了之前保存的oldsize,oldsize是原来的有效字符个数,start加上start就是finish的位置,因为我的有效字符size压根就没改变,在新空间里finish和start也会保持这种相对位置关系。
至于end_of_storage,它和start的相对位置关系就是start+capacity()容量而已,而此时我的capacity已经改变成n了,所以直接让新的start+n就找到了capacity的位置了。为什么这个扩容不用改变size和capacity呢,因为size和capacity实现的时候是通过start和finish以及end_of_storage进行获取的,所以改变了他们三也会间接改变size和capacity
四.插入删除
push_back()函数
插入首先要考虑的是原有空间有没有满,如果满了则需要扩容,因为已经提前做好了reserve,所以可以利用reserve进行扩容。其次就是将新数据插入到finish位置上,因为finish是最后一个有效元素的下一位,所以进行尾插一般就是直接插入到finish位置上。插入完了finish++;
void push_back(const T& key)
{
if (finish == end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
}
*finish = key;
finish++;
}
if (finish == end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
} 扩容操作, if (finish == end_of_storage) finish控制的是size有效字符个数,end_of_storage控制的是实际空间容量capacity大小,如果它们相等就说明vector存满了需要扩容size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity(); vs编译器是1.5倍扩容,g++是2倍扩容,为了测试方便所以直接使用了2倍扩容。如果原空间没一个数据,空间为空就先给10个大小空间。扩容操作直接给reserve函数进行处理了,简化操作
pop_back()函数
尾删就很简单了,直接finish往前挪就行了,不过这种方式并不是真正删除了数据,只是加了访问限制而已,因为访问打印只会打印到finish,后面的压根不会访问到
void pop_back()
{
--finish;
}
迭代器失效
在说insert之前先来说一说迭代器失效,迭代器失效是和扩容机制紧密相连的,扩容的话一般是开劈一个新空间然后把旧空间的数据复制过去,这当中就有一个问题,insert插入时的迭代器位置pos还指向原空间,但是我旧空间已经释放了,pos就成了野指针或者无效化,这时候再通过pos访问空间就是访问野指针,造成崩溃报错
举个例子
我们往vec当中插入数据,然后保存第二个元素的位置,插入数据后重新分配内存,此时it没被处理,依旧还指向旧空间,再访问它就会报错
解决办法就是记住与开头迭代器begin()的相对位置(偏移量),然后再让begin加上这个偏移量就找到了新it的位置 。不过vector源代码已经进行处理,insert函数返回值就是it的新位置,所以直接接收就可以了
来看看linux环境下的情况
先用vim写好代码
编译运行
此时得到的不是一个有效值,在linux环境下insert也存在迭代器失效问题,为什么是0而不是像vs一样访问野指针直接崩溃呢,这就是不同编译器的处理规则不一样。可以写一个new空间出来然后释放,但是呢又访问这个野指针的代码,来看一下究竟g++对野指针访问是怎么处理的
下面这个代码可是百分百野指针的,输出也是0
如果说insert是因为扩容开辟了新空间造成野指针所以才会导致迭代器失效,那么erase这个不用扩容开辟新空间会不会迭代器失效呢。可以举个例子来查看一下
下面的例子是在it这个迭代器的位置上删除数据,然后访问it上的数据,已经assert报错,说明erase依旧有迭代器失效
那么究竟为什么会这样呢,按理说虽然it上面的数据删除了,但是没有换空间,按理说删除是后面数据覆盖前面数据,所以此时解引用it依旧是有效数据啊。先来看看linux上面是怎么处理的
先用vim写代码
编译运行
从上图可以看出来,linux环境下erase不会有迭代器失效正常输出了,vs编译器上更严格一点直接一刀切断言终止运行,这是因为erase还是有迭代器失效的 风险的。迭代器失效可不只是野指针也有无效化啊,比如删除最后一个数据,也不过是让end()迭代器往前挪到而已,这个数据依旧在原先删除的那个位置上,此时再访问它不是野指针所以可以读到数据,但是这个数据是脏数据啊,所以vs干脆全设置成迭代器失效了
insert
insert有两种插入方式,一种是通过迭代器控制插入位置,然后直接插入单个数据,另一种虽然也是通过迭代器控制插入位置,但是是插入另一个容器两个迭代器构成区间内的多个数据
第一种通过迭代器插入单个数据,也就是在那个位置上把它的后面的数据(包括它直接往后面挪动就行了,因为insert是在指定位置之前插入,所以这个位置上的数据也需要往后面挪动)
iterator insert(iterator pos, const T& key)
{
assert(pos <= finish && pos >= start);
if (finish == end_of_storage)
{
size_t n = pos - begin();
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}
iterator end = finish-1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = key;
finish++;
return pos;
}
assert(pos<=finish&&pos>=start); 限制范围,肯定要在删除的vector对象的范围内,start是头,finish是有效数据的下一个,可以在finish位置上直接插入(这样就相当于尾插了),所以pos位置可以等于finish
if (finish== end_of_storage)
{
size_t n = pos - begin();
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}这段代码完成扩容工作,同时 size_t n = pos - begin(); pos = start + n;这个就是返回pos在新空间上的位置,避免迭代器失效,n是pos距离开头位置的偏移量有多少,即使改变了空间也可以通过这个偏移量定位到pos在新空间上的位置
iterator end = finish-1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = key;
finish++;
return pos;这段代码完成的就是具体的插入工作,因为insert是在指定的位pos之前插入,所以要把pos和pos之后的数据都往后面挪到,然后才在pos位置上插入实际数据。最后一次情况是*(pos+1)=*pos;实际上就是pos上的数据挪到下一位上了。return pos是为了后续还要使用pos这个迭代器,但是pos如果不返回新位置的话就会产生迭代器失效
第二种insert插入,是区间插入
区间和单个值插入差不了多少,首先的不同点在于扩容,单个insert插入只需要考虑扩容一次就够了,因为无论咋样扩容两倍容量足够存的下一个数据。但是区间扩容不一样,它可以拿别的容器的数据来插进去,这个数量有可能比两倍的capacity还要大,所以要循环重复扩容。另一个要考虑的是数据的挪到,单个数据挪到时只是每个前面的数据往后挪动一位而已,而区间插入要考虑的是往后挪到gap位(要插入数据的数量)
template<class InputIterator>
iterator insert(iterator pos, InputIterator first, InputIterator last)
{
assert(pos >= start && pos <= finish);
assert(first<last);
size_t gap = last - first; // 计算要插入的元素数量
size_t n = pos - start;
// 如果需要扩展存储空间
while(size() + gap > capacity()) {
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}
// 移动现有元素以为新元素腾出空间
iterator end = finish-1;
while (end >=pos)
{
*(end + gap) = *(end); // 将现有元素向后移动
end--;
}
//将新元素复制到插入位置
for (auto& it = first; it != last; ++it)
{
*pos++ = *it; // 将元素插入到新空间
}
finish += gap; // 更新有效大小
return pos; // 返回新的结束迭代器位置
}
while(size() + gap > capacity()) {
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}这个是具体的扩容操作,把if改成while了, pos = start + n; 依旧是防止迭代器失效。
其实也可以考虑直接写成 size_t newcapacity = max(capacity() * 2, size() + len);这样只用if也可以扩容成功,只不过有可能会造成一点浪费,不过不用循环了
iterator end = finish-1;
while (end >=pos)
{
*(end + gap) = *(end); // 将现有元素向后移动
end--;
}具体的挪动数据操作,end必须在finish-1上,不能在finish上,finish位置上是无效数据(有可能是随机值), 即使空间容量足够也不能挪动这个位置上的数据。调试虽然不会出错,插入和输出也不会报错,但是测试函数一结束就会报断言错误。这是因为挪动finish位置上无效数据会被看做使用了失效的迭代器,vs编译器会强制报错(这个错误异常是捕获不到的,只有运行结束才会报出来),在 C++ 中,使用失效的迭代器(即已经指向无效或被删除元素的迭代器)通常不会抛异常,而是会导致未定义行为。这意味着程序可能会崩溃、产生错误结果或表现出预期外的行为,但并没有机制捕获这种情况。
template<class InputIterator>
iterator insert(iterator pos, InputIterator first, InputIterator last)为什么要重写迭代器的模版,这是因为如果一直用iterator,那么只能使用vector实例化对象同类型的迭代器,使用string类的迭代器控制区间范围插入就做不到了,所以整个模板InputIterator,只要这个迭代器可以使用引用,可以++偏移量那么无论什么类型的迭代器都可以使用
erase删除
erase删除也有两种,一种是迭代器控制要删除的位置,然后直接删除这个位置上的单个数据,另一种是两个迭代器控制删除区间,这个区间内全部删除
第一种情况单个删除,其实也就是把这个位置后面的数据全都往前挪到然后覆盖而已
因为vs编译器中erase也有跌代器失效,所以依旧要更新pos的位置,其实也就是返回原来的pos的位置
iterator erase(iterator pos)
{
assert(pos >= start && pos < finish);
size_t n = pos - start;
while (pos < finish - 1)
{
*pos = *(pos + 1);
pos++;
}
finish--;
pos = start + n;
return pos;
}
第二种区间删除 iterator erase(iterator first, iterator last)
与单个数据删除相比只是让last和last之后的数据全部往前挪动(last-fist)个位置而已,因为C++一般是左闭右开,所以last也要挪动
iterator erase(iterator first, iterator last)
{
assert(first >= start && first < last && last <= finish);
size_t ret = first - start;
size_t n = last - first;
iterator nextit = last;
while (nextit != finish)
{
*(nextit - n) = *nextit;
nextit++;
}
finish -= n;
return start + ret;
}
五.遍历输出
常见的遍历输出就两种重载operstor[ ]像数组一样输出,另一种是迭代器输出
operator[ ]的实现
operator[ ]一般会接收一个位置值i,这个i其实就是开头迭代器start具体当前这个数的偏移量,所以*(start+i)就可以访问到了当前在i这个位置上的数,因为我实现的start底层本来就是指针,所以可以直接用解引用和加偏移量。同时还有一种写。start[i],因为start是这个数组的开头,而数组名其实也看做是数组的开头,所以可以直接数组一样使用[ ]访问
T& operator[](size_t i)const
{
assert(i < size());
return *(start + i);
}
测试
此时可以对上面的删除插入操作进行测试了
push_back插入和pop_back()删除测试
pop_back
insert单个数据插入
中间位置插入
begin()位置插入
在end()位置插入6
insert区间插入
中间位置插入
begin()位置插入
end()位置插入
erase删除测试
单个数据删除
区间删除bcd
迭代器访问输出
迭代器之前已经已经实现过了,所以不加赘述了,其实就是一个控制头一个控制尾,然后解引用输出就行。因为底层封装的迭代器是指针,所以也不用特意去实现++功能
测试
C++11标准的范围for底层还是迭代器,所以只要实现了begin()和end()就可以使用范围for了
六.swap函数的实现
因为我的所有实现都是用三个成员变量start,finish,end_of_storage来实现的,所以直接使用库里现成的swap把他们直接交换就行。
void swap(vector<T>& tamp)
{
std::swap(start, tamp.start);
std::swap(finish, tamp.finish);
std::swap(end_of_storage, tamp.end_of_storage);
}
七.构造函数与析构函数
默认构造
默认构造直接给个空就行了,因为我之前在成员变量声明那里给了缺省值nullptr,所以默认成员函数可以不处理。但是即使是空也还是要写出来,否则写了别的构造就不会生成默认构造,编译器会报错
fill填充构造
fill填充构造是用n个val填充容器,实际上就走个循环把val尾插n次而已
但是有个问题,fill填充如果不给数据默认是填充0的,如果是别的stl类型比如string可能会去调默认构造,构造一个空的string数据。如果此时我又要处理内置内型的话,是不是又要重新写一个构造函数来对待不同的情况呢。针对这种情况C++给内置类型做了处理,给内置类型加上了构造函数。
比如int类型,你定义变量一般是这样写的int a=5; 加上构造函数可以写成这样int a=int(5);实际上就是就是把5作为参数传递给int的构造函数
这样的话fill填充构造就可以写成vector(int n,const T &val=T());如果你不给具体数据那么自动就去调对应类型的构造函数,传参为空如果是string类型构造的是空数据( "" 但是还是会分配空间的),char类型里面填充的是\0(这样打印不出来的) ,int类型自动填充的是0
但是测试时报错了
为什么会报个 无法取消引用类型为“InputIterator”的操作数,这是因为范围构造也用了模版, template <class InputIterator>
vector(InputIterator first, InputIterator second) //范围构造 ,而此时fill填充构造是 vector(size_t n, const T& val = T()),函数模版选择时一般是哪个更匹配就选哪个,对于vector<int> tamp(5,5)全是int类型,而const T& val = T()这个是可以直接推演成int,可是size_t可是不能推演成int。 范围构造vector(InputIterator first, InputIterator second)是两个需要推演的模板可以全推演成int,所以他自动匹配了这个,但是范围构造里是需要解引用之类的操作,int类型不能解引用所以直接报错了。
那么怎么解决呢,先来看看vector源代码是怎么解决的
所以库里解决办法也很简单,直接另写一个更加匹配的版本,这样它就不会匹配到范围构造里了
所以再另写一个int类型的范围构造,实现方法基本一样只是类型不一样而已
范围构造
范围构造函数实际上就是用另一个容器的两个迭代器之间的数据来进行构造,是不是挺像insert的区间插入,所以可以用区间插入直接来构造出来,也可以用push_back挨个从first开始循环往后尾插
initializer_list构造
initializer_list实际上也算一种容器,他里面也有迭代器begin和end以及size函数,花括号给它传参的时候传的是一个列表,begin指向开头,end指向结束的下一个位置
既然有迭代器,那就说明可以使用范围for进行遍历,所以也就可以通过push_back挨个把数据插入vector中
构造测试
默认构造
fill填充构造
范围构造
initializer_list
析构函数
因为所以new出来的空间都是由start进行指向管理的,所以只需要针对start释放资源就可以了,其余的finish之类的全置为空,避免释放空间后还访问他们(访问野指针)
八 拷贝构造及赋值重载
拷贝构造
实际就是把要拷贝的数据挨个push_back进新对象而已,因为我已经提前知道了要开多大空间,所以可以直接reserve(tamp.capacity()),免得每一次push_back都要扩容
测试
赋值重载
因为已经实现了拷贝构造了,所以tamp赋值就很简单了。首先形参传给实参并没有加引用,所以形参是实参的一份临时拷贝。而我正好要这份临时拷贝的数据,所以直接swap交换就行了。而这个临时拷贝的对象出了循环就直接自动析构销毁了
vector<T>& operator=(vector<T> tamp)
{
swap(tamp);
return *this;
}
完整代码如下
#pragma once
#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;
namespace myvector
{
template <typename T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
typedef T* reverse_iterator;
iterator end() { return finish; }
iterator begin() { return start; }
const_iterator cbegin() const { return start; }
const_iterator cend() const { return finish; }
reverse_iterator rbegin() { return end() - 1; }
reverse_iterator rend() { return begin() - 1; }
size_t size() const { return finish - start; }
size_t capacity() const { return end_of_storage - start; }
vector() //默认构造
{
}
vector(size_t n, const T& val = T())
{
while (n--)
{
push_back(val);
}
}
vector(int n, const T& val = T()) //填充构造
{
while (n--)
{
push_back(val);
}
}
template <class InputIterator>
vector(InputIterator first, InputIterator second) //范围构造
{
while (first < second)
{
push_back(*first);
first++;
}
}
vector(const vector<T>& tamp)
{
reserve(tamp.capacity());
for (size_t i = 0; i < tamp.size(); i++)
{
push_back(tamp[i]);
}
}
vector(initializer_list<T> li1)
{
size_t n = li1.size();
reserve(n);
for (const auto& e : li1)
{
push_back(e);
}
}
void swap(vector<T>& tamp)
{
std::swap(start, tamp.start);
std::swap(finish, tamp.finish);
std::swap(end_of_storage, tamp.end_of_storage);
}
vector<T>& operator=(vector<T> tamp)
{
swap(tamp);
return *this;
}
T& operator[](size_t i)const
{
assert(i < size());
return *(start + i);
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* newptr = new T[n];
if (start)
{
for (size_t i = 0; i < size(); i++)
{
newptr[i] = start[i];
}
delete[] start;
}
start = newptr;
finish = start + oldsize;
end_of_storage = start + n;
}
}
void push_back(const T& key)
{
if (finish == end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
}
*finish = key;
finish++;
}
void pop_back() { --finish; }
iterator insert(iterator pos, const T& key)
{
assert(pos <= finish && pos >= start);
if (finish == end_of_storage)
{
size_t n = pos - begin();
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}
iterator end = finish-1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = key;
finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= start && pos < finish);
size_t n = pos - start;
while (pos < finish - 1)
{
*pos = *(pos + 1);
pos++;
}
finish--;
pos = start + n;
return pos;
}
iterator erase(iterator first, iterator last)
{
assert(first >= start && first < last && last <= finish);
size_t ret = first - start;
size_t n = last - first;
iterator nextit = last;
while (nextit != finish)
{
*(nextit - n) = *nextit;
nextit++;
}
finish -= n;
return start + ret;
}
template<class InputIterator>
iterator insert(iterator pos, InputIterator first, InputIterator last)
{
assert(pos >= start && pos <= finish);
assert(first<last);
size_t gap = last - first; // 计算要插入的元素数量
size_t n = pos - start;
// 如果需要扩展存储空间
while(size() + gap > capacity()) {
size_t newcapacity = capacity() == 0 ? 10 : 2 * capacity();
reserve(newcapacity);
pos = start + n;
}
// 移动现有元素以为新元素腾出空间
iterator end = finish-1;
while (end >=pos)
{
*(end + gap) = *(end); // 将现有元素向后移动
end--;
}
//将新元素复制到插入位置
for (auto& it = first; it != last; ++it)
{
*pos++ = *it; // 将元素插入到新空间
}
finish += gap; // 更新有效大小
return pos; // 返回新的结束迭代器位置
}
void resize(size_t n, const T& value = T())
{
if (n > size())
{
reserve(n);
size_t ret = n - size();
while (ret--)
{
push_back(value);
}
}
else
{
finish = start + n;
}
}
~vector()
{
if (start)
{
delete[] start;
start = finish = end_of_storage = nullptr;
}
}
private:
iterator start=nullptr;
iterator finish=nullptr;
iterator end_of_storage=nullptr;
};
}