目录
本篇文章模仿的vector与STL源码并不完全一致,例如本文直接通过new来开辟空间,但是源码中通过内存池分配,但是这并不影响彼此之间的关系,所以本篇文章还是有一定的学习意义的。
模板:
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
}
我相信如果大家用过vector的话,一定是知道每一次使用vector都需要标注清楚这个类是用来存储什么样的数据的,例如:
vector<int> vv1; 存整型数据
vector<double> vv2; 存浮点型数据
vector<vector<int>> vv3; 存存整型数据的vector数据
所以,我们模拟的vector类就不能单独面向某一种数据,而是应该考虑到所有的类型,就算是一直自定义类型嵌套也能实例化出来,如下:
vector<vector<vector<vector<vector<int>>>>> vv4;
虽然上述的类型对于我们来说估计这辈子都不会遇到,但是我们总不能防止某些好奇心重的小伙伴搞事,所以是必须要能够支持这种写法的,就像是为了满足到酒吧点炒饭的帅小伙。
所以首先得出结论:我们的类需要被构建成模板类。
成员变量:
上面代码中可以看到我们的成员变量的类型是自定义类型重定义iterator,也就是平时我们熟知的迭代器,我们的迭代器实现又是指针的方式,所以可以将这三个变量理解为我们存储数据空间的三个位置。_start对应开头,_finish对应数据结尾,_end_of_storage对应空间容量的最后位置。也即是对应我们顺序表的size和capacity。
每当我们实现一个类,默认成员函数是必不可少的,特别是像是我们vector这样需要向堆申请空间的类。
无参构造可以说是非常简单了,我们甚至连空间都不需要开辟,只需要通过初始化列表为我们的三个迭代器变量初始化即可,如下:
//默认构造方式,全指针都置空值,后续无论怎么插入都有扩容为指针赋值
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
可能有朋友要问,那用这种方式,不就表示了自己之后不能插入数据之类的吗?事实不是,因为我们还有其它的函数接口为我们服务,大家看下去就明白了。
有参构造咱们就与库保持一次,既然它不支持通过一个值直接去初始化,而支持通过n个T类型数据去构造,那么我们也这样学习它这样:

看不懂上面的库实现方式没事,看我的也是一样:
//有参构造n个T类型的数据进入vector<int>
vector(size_t n, const T& va = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
T* temp = new T[n];
for (size_t i = 0; i < n; ++i)
{
//这里赋值操作是<T>和<T>之间的操作
temp[i] = va;
}
_start = temp;
_finish = _start + n;
_end_of_storage = _finish;
}
这里的构造也是一样,需要通过初始化列表先将三个迭代器变量初始化空指针,因为我们不能保证有没有小聪明蛋给n赋值为0去初始化,其次就是我这里使用了T()这样的匿名对象作为缺省参数。
有同学在这里就要问了:T()这样的匿名对象生命周期不是只有一行吗?你这代码都多少行了,还在用不是错的吗?
其实不是,在C++当中,当我们用const加引用类型的变量去接收匿名对象,会改变匿名对象的生命周期为这个变量的生命周期长度,如下:
const int& ret = T(); 改变T()的生命周期为ret
T(); 生命周期只有一行
值得注意的是上面的const是必须加的,因为我们的T()属于本身是属于临时对象的,而临时对象又有常量属性,也就是不可更改属性。如果不用const接收,那么就会导致错误。
有点意思的是,不加const在vs2013及其之前的版本都是不报错的,后续版本我不清楚,但是到了vs2019这个BUG就被修复了。
函数设计:
这里的代码我用了最全面的写法,后面可以通过函数复用的方式直接究极简化。
首先通过new向堆申请n个T类型的空间,这里不能使用malloc这种C语言的申请方式,原因我相信大家也明白,就是因为我们要实现模板类,那必然有可能使用自定义类型,有自定义类型要就要去调用它的构造函数,只有new才能实现,malloc不行。
T* temp = new T[n];
然后通过循环不断的赋值,最后为三个迭代器变量定位即可,这里循环体中的赋值操作涉及了很复杂的嵌套操作,绝不是简单的一行代码,等到下一小节重载赋值运算符我再来讲。
vector(int n, const T& va = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (n--)
{
push_back(va);
}
}
该重载的函数体就是我所说的究极优化的方式,通过尾插函数,不断的复用,就实现了函数功能,但是重点不在这里,我重载这个函数的意义在于为我们的迭代器区间构造函数服务。
为什么这么说?请先看迭代器区间构造函数。
template<class InputIterator>
vector(InputIterator start,InputIterator end)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (start != end)
{
push_back(*start);
++start;
}
}
首先我们得知道一个点,那就是C++为我们提供了函数重载,那么编辑器就会通过传入的参数类型为我们匹配最适合的函数。
举例:当我们通过下列方式传参:
vector<int> vv1(5,5);
编辑器会将我们的传参类型认为是int,int,而不是size_t,int,也就是说,编辑器就会找到迭代器区间构造函数中去了,这并不是我们想要的结果,而编辑器却认为这是最合适的函数,为了避免这种情况的出现,所以需要重载int型的构造函数。
当然,有的小伙伴可能不太理解为什么我们需要再写一个模板出来,这理解起来真难受。这确实让人很难受,但是他带来的好处也是很大的,因为这样实现的功能,让我们的构造方式做种多样的起来,无论是何种形式的迭代器去构造,我们都有对应的方式去构造出来,这就是牛逼之处。
代码:
vector<T>& operator=(const vector<T>& va)
{
if (_start != va._start)
{
//只能通过new的方式实现,因为要调用自定义类型的构造函数
T* temp = new T[va.capacity()];
//保留数据
//memcpy(temp, va._start, sizeof(T)* va.size());
int len = va.size();
for (int i = 0; i < len;++i)
{
//嵌套
temp[i] = va[i];
}
//释放掉原来空间,如果有的话
delete[] _start;
_start = temp;
_finish = _start + va.size();
_end_of_storage = _start + va.capacity();
}
return *this;
}
赋值需要相同类型才能成功,这里用const vector<T>&相信大家也能理解,我就直接切入重点了。
为什么我代码中要用循环一个一个的赋值,为什么不直接调用库函数memcpy()呢?简单又方便,而是通过循环一个一个的赋值,感觉写得好搓哦。其实呢,也不是博主不想用memcpy(),而是现实给了博主一巴掌,让我认清,写模板类的深拷贝有多令人头疼。
举一个例子:如果我们构建的vector<vector<int>>类型的类,那么我们开辟了一片空间用于存n个vector<int>,然后这些vector<int>被我们直接用memcpy直接拷贝过来了,但是我们呢,又不能直接为单独写一个函数,因为这是模板,要是单独写了,又不满足vector<int>类型了。下图:

原本我们是希望赋值出来的对象能有一个新的空间去承载这些数据,如上图,但是如果我们通过memcpy来实现能够成功吗?请看下图:

很明显,如果通过memcpy实现了一个什么功能?我们确实实现了一层深拷贝,将vector<int>用新的空间承载起来了,但是还有更里面的空间呢?这些空间也是需要new出来的哇。用memcpy还能行吗?能行,但不完全行,除非你保证你以后不会用自定义类型模板。
所以决定采用循环的方式,一次一次的赋值,其中有一句话非常重要,那就是:
temp[i] = va[i];
这一句话没有你想象的那么简单,你想,如果是内置类型我们关不关心他申请空间?不关心,那么对于自定义类型它会怎么做?嵌套!!!,直到遇到内置类型嵌套就结束了,如下图:

上图中,我们可以看到,每次我们赋值拷贝,如果遇到了一个赋值运算符,编辑器就回去检查是否是自定义类型,需要去调用赋值运算符重载函数,如此反复,直到没有赋值运算符重载函数,也就是没有需要申请空间的必要了。

下图的编译,运行,使用也不会报任何的错误了。
拷贝构造:
//拷贝构造
vector(const vector<T>& va)
{
//赋值重载
*this = va;
}
拷贝构造直接复用赋值运算符重载函数就行,写法和逻辑思维差不多,博主也想偷懒。
//析构函数
~vector()
{
//释放空指针不会出错,无论是什么方式
delete[] _start;
_start = nullptr;
_finish = nullptr;
_end_of_storage = nullptr;
}
析构函数也没有什么好讲的,看代码就行。
代码:
void reserve(size_t n)
{
if (n > capacity())
{
size_t len = size();
T* temp = new T[n];
if (_start != nullptr)
{
//memcpy(temp, _start, sizeof(T) * size());
for (size_t i = 0; i < len; ++i)
{
temp[i] = _start[i];
}
delete[] _start;
}
_start = temp;
_finish = _start + len;
_end_of_storage = _start + n;
}
}
vector和其它string、顺序表或者说任何数据结构一样,能不缩容就不缩容,所以,当我们的n小于容量大小的时候,不做任何处理直接返回即可。
同样,这里也涉及到了多次深拷贝的问题,也就不能用memcpy来实现,需要用循环依次赋值。还有,咱们需要考虑到当空间异地扩容之后是需要将原空间释放的,以防内存泄漏。
代码:
void resize(size_t n,const T val& = T())
{
//小于操作不需要缩容,只需要将_finish重定位即可
if (n < size())
{
_finish = _start + n;
}
else if (n == size()){} //无动作
else
{
int len = n - size();
//_finish和_end_of_storage的绝对位置之差与n的大小之比
if (_finish + n > _end_of_storage)
{
reserve(capacity() == 0 ? n : capacity()+n);
}
//补齐T类型数据
while (len-- > 0)
{
*_finish = val;
_finish++;
}
}
}
resize功能比reserve多一个,那就是能够实现重定size大小,大于size的部分通过数据val占位。然后复用reserve,如果是第一次扩容,那就需要用三目运算来判断给定大小。
内容比较简单,相信大家配合注解能够看懂,直接上代码:
//尾插只需要考虑如果空间不够就应该扩容
//并且,还有空容量的情况也需要考虑
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//如果不需要reserve表示不用扩容,那么就是原地扩,不需要动_start和_end_of_storage
//如果扩了容,reserve会为我们将这几个变量重定向
*_finish = val;
++_finish;
}
//插入
iterator insert(iterator pos, const T& val)
{
//检查插入位置的有效性
assert(pos >= _start);
assert(pos <= _finish);
//提前保留绝对距离
size_t len = pos - _start;
//判断扩容
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//通过绝对值偏移重定向
pos = _start + len;
//指向最后一个位置的下一个地址
iterator end = _finish;
//移动完pos位置的数据,结束
while (end > pos)
{
*(end) = *(end - 1);
--end;
}
//因为有赋值运算符重载,那么不管是否是内置类型,都能满足
*pos = val;
//向后移动一位
++_finish;
return pos;
}
//删除
void erase(iterator pos)
{
//判断pos可行性
assert(pos >= _start);
assert(pos < _finish);
//从pos下一个位置地址开始依次向前移动
iterator start = pos + 1;
//中途是没有任何扩容缩容的地方,所以迭代器不会更换
while (start != _finish)
{
*(start - 1) = *start;
++start;
}
//删除会有机会产生结果不确定的意外情况
//所以,任何一次删除,该迭代器都应该被销毁
//该函数才不会设置返回值
--_finish;
}
//尾删,检查是否还有数据能删除
void pop_back()
{
assert(_start != _finish);
--_finish;
}
//迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
//求个数、容量
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
//重载[],需要区分const有无的区别和pos位置的准确性
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
迭代器是有失效的情况的,这部分根据不同的编辑器会有不同的实现方式,博主这里不太想多讲,不过我建议大家通过迭代器插入删除数据之后就别再使用这个迭代器了,或者是重新给他定位,防止非法越界。
以上就是博主对vector模拟实现的全部理解了,希望能够帮到大家。
是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复
假设我在Store的模型中有这个非常简单的方法:defgeocode_addressloc=Store.geocode(address)self.lat=loc.latself.lng=loc.lngend如果我想编写一些不受地理编码服务影响的测试脚本,这些脚本可能已关闭、有限制或取决于我的互联网连接,我该如何模拟地理编码服务?如果我可以将地理编码对象传递到该方法中,那将很容易,但我不知道在这种情况下该怎么做。谢谢!特里斯坦 最佳答案 使用内置模拟和stub的rspecs,你可以做这样的事情:setupdo@subject=MyCl
在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定
我有一个gem,它有一个根据Rails.env的不同行为的方法:defself.envifdefined?(Rails)Rails.envelsif...现在我想编写一个规范来测试这个代码路径。目前我是这样做的:Kernel.const_set(:Rails,nil)Rails.should_receive(:env).and_return('production')...没关系,只是感觉很丑。另一种方法是在spec_helper中声明:moduleRails;end而且效果也很好。但也许有更好的方法?理想情况下,这应该有效:rails=double('Rails')rails.sho