今天我们不直接上代码了,我们先来分析一下vector这个容器。
我们来看看这个东西
vector
这我们可以大概猜到是一个二维数组。
但并不完全是,二维数组是一个n*n 的矩形结构,而这里的vector套vector可不是这样,
我们来剖析一下它
当我们执行
vv [ i] [ j ] 这行代码时
首先执行 vv. operator [ i ],也就是vv [ i ] 这时候我们在一个大vector中找到了,很多很多的vector。每一个vector 是用i 控制的,一般用来控制矩形中行的信息。
这时候我们需要访问具体的int,它们存在每个小vector中,那就执行这样的代码
vv [ i ] .operator [ j ] ,这也就等价于 vv [ i ][ j ]
至此我们理解了双v的底层逻辑。
有人会问,为什么不直接用二维数组呢?这个问题很简单,我们的双V什么数据类型都可以存放,而且空间非常的灵活。数组能存什么呢?你不得使用数组指针之类的复杂概念。
那你又说了,这个东西调用来调用去,效率坑定没有数组直接开的快啊,
真是不好意思,我们函数也是有inline 的,展开后并没有什么差别的呦。
我们在上代码之前,再来看看stl源码中是如何组织整个vector的。
有了这个图,再看看他们的成员函数和变量就大概懂了,我的能力暂时无法完整的实现stl源码。
所以这里的vector模拟实现,只是简易版的。
接下来就直接上代码了,具体需要解释的地方,我会在代码中解释。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
#include
using namespace std;
namespace xxx
{
template <class T>
class Vector
{
public: c++中默认是private,所以要加上pblic
一:先来搞迭代器 迭代器怎么搞呢 1、先写指针
2、 写begin 和end 当然要有const和非const版本了
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator cbegin() const
{
return _start;
}
const_iterator cend() const
{
return _finish;
}
二 、我们开始实现默认成员函数
构造函数:构造函数要怎么实现呢?函数名和类名一样,Vector,主要完成初始化列表,让他自动调用自动初始化。
我们需要初始化什么呢?那就是那几个成员变量而已啦,我们在private中写的只是声明,这里才会真正的定义。顶多加上缺省功能。
我们注意一下,这里初始化可不是用等号初始化的,要用小括号。而且不需要带类型名。这里就是不用带iterator
而且只有第一个变量前是 : 其他都是 ,
Vector() = default; //这里是让系统生成默认的构造函数
Vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
printf("构造函数很好,over\n");
}
析构函数:函数和类同名,前面加上~,函数里面要实现 1、if 判空 2、delete 注意delete不加= 3、指针赋空
~Vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
printf(" ~析构函数很好,over\n");
}
我们给这两个函数加上const,使调用者缩小权限。
size_t Capacity() const
{
return _end_of_storage - _start;
}
size_t Size() const
{
return _finish - _start;
}
reserve开辟capacity空间
返回void ,传入开辟的size_t 一般实参就为capacity
1,判断是否需要扩容,如果需要扩容,我们开辟一个临时变量存新开辟的数组。2,需要判空 3,若不为空就循环赋值给临时数组4,更新三个变量的值。
这里我们解释一下这个函数的功能:v2(v1) 这里的rerserve是v2的,当然如果之前有数据的话,size()就不是0,这里开辟新的够大空间,把原来的数据拷贝的新数组里。
void reserve(size_t n)
{
if (n > Capacity())
{
size_t size = Size();
T* tmp = new T[n]; //这里可以理解为,T类型的数组开n个空间。
if (_start)
{
memcpy(tmp, _start, sizeof(T)*size); //这里我们换一种写法。
for (size_t i = 0; i < size; i++)
{
tmp[i] = _start[i];
}
delete[] _start; //把原来的_start删了,不然还是用的原来的。
}
_start = tmp;
_finish = _start + size;
_end_of_storage = _start + n;
}
}
resize 传参为大小和一个缺省构造。1、判断并缩容 2、否则如果n过大就增容,循环给默认值并更新变量
void resize(size_t n, const T& val = T()) //这里T()为构造函数且是val 的缺省值,默认是0;
{
if (n <= Size())
{
_finish =_start + n;
}
else
{
if (n>Capacity())
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
这里为什么不更新_endof
}
*****拷贝构造****** 难点
拷贝构造是构造函数的重载,所以函数写法一样,只是参数不一样,参数这里使用类模板并且取引用
功能如何实现呢?1、首要new一个新空间,T类型的数组,capacity那么大,指向新start。2、然后把传进来的类一个一个赋值给新空间3、修改另外两个变量的值
Vector(Vector <T> & v)
{
printf("我是构造函数的弟弟,我很好,over\n");
_start = new T[v.Capacity()];
for (int i = 0; i < v.Size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.Size();
_end_of_storage = _start + v.Capacity();
}
拷贝构造还有第二种实现方法 v2(v1)
我们传入const 变量,因为我们不会去修改小v的内容,一般传进来权限缩小,变为只读。同时我们进行初始化列表初始化,这里有个问题,这里不是和构造函数的初始化列表一样吗?所以我们可得,调用拷贝构造出来的函数,不会再去调用它的构造函数了。
1 ,用reserve开空间 2, 我们用两组迭代器,一组是this的迭代器,一组是const的迭代器,循环赋值 3,更新另外两个变量
Vector(const Vector<T> & v)
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(v.Capacity());
iterator vi = begin();
const_iterator cvi = v.cbegin();
while (cvi != v.cend())
{
*vi++ = *cvi++;
}
_finish = _start + v.Size();
_end_of_storage = _start + v.Capacity();
}
****赋值运算符重载*****
函数的返回值是类的引用, 参数是const引用,1、判断是否给自己赋值2、删除之前的空间,并开辟新的空间3、然后把数据拷贝过来4、更新变量5、返回值
Vector<T>& operator=(const Vector<T>& v)
{
if (this != &v) //注意这里是取地址V
{
delete[] _start;
_start = new T[sizeof(T)*v.Size()]; //这里为什么要sizeof(T),不是应该T类型有几个就是几个吗,为什么要算字节呢?
//reserve(sizeof(T)*v.Size()); //这里为什么不去调用开辟函数呢?
memcpy(_start, v._start, v.Size()*sizeof(T));
_finish = _start + v.Size();
_end_of_storage = _start + _capacity;
}
return *this;
}
当然还有第二种实现方法
我们返回值依然是引用返回,而参数变成非const 非引用。1、我们进行swap,2、返回
这里解释一下这个函数,当我们拷贝构造一个v传进来。我们在里面进行交换,有用的已经被this存下来了,没用的出函数栈帧自动销毁了。
Vector<T>& operator=(Vector<T> v)
{
Swap(v);
return *this;
}
void Swap(Vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
pushback 1、判断扩容 2、更新变量
void pushback(const T& val)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = Capacity() == 0 ? 2 : Capacity() * 2;
reserve(newcapacity);
}
*_finish = val;
_finish++;
insert(_finish, val); 用insert的时候一定要结合find找新的迭代器,不然开新空间后,直接找不到
}
popback 1、断言 2、变量更新
void popback()
{
assert(Size() > 0);
--_finish;
//erase(--end());
}
insert 插入 :插入不需要返回值、参数是pos位置的迭代器,还有T类型引用,当然还要const。
1、我们先断言一下pos位置的正确性。2、存一下pos位置(避免迭代器失效) 3、判断空间大小,不够需要扩容并更新 4、迭代器循环移动空位。5,存入数据。6、更新变量
void insert(iterator pos, const T& val)
{
assert(pos <= _finish);
size_t posindex = pos - _start;
if (_finish == _end_of_storage)
{
size_t newcapacity = Capacity() == 0 ? 2 : Capacity() * 2;
reserve(newcapacity);
pos = _start + posindex;
}
iterator end = _finish;
while (pos < end) //从后往前挪
{
*end = *(end - 1);
end--;
}
*pos = val;
_finish++;
}
Erase 返回下一个位置(这里默认用pos迭代器返回值自动更新) ,参数pos迭代器。
1、pos给一个迭代器。2、循环给空3、更新变量
iterator erase(iterator pos)
{
iterator begin = pos;
while (pos != _finish)
{
*pos = *(pos + 1);
++pos;
}
--_finish;
return begin;
}
operator[] 这里应该有两个版本 返回引用,使之可修改,参数传一个位置就行
1、断言 2、返回
T& operator [] (size_t pos) //可读可写
{
assert(pos < Size());
return _start[pos];
}
当我们用const对象掉上面的函数时候,是无法调用的,当调用下面的函数时,返回应该是const,不然被动的扩大了权限。本来const调用的你,在你函数里面转一圈我被修改了。
const T& operator[] (size_t pos) const //只可读
{
assert(pos < Size());
return _start[pos];
}
void print()
{
for (size_t i = 0; i < Size(); i++)
{
cout << _start[i] << " ";
}
cout << endl;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
void test1() 测试构造、析构、pushback popback 迭代器 ,拷贝构造 .访问
{
Vector<int> v1;
v1.pushback(1);
v1.pushback(2);
v1.pushback(3);
v1.pushback(4);
v1.pushback(5);
v1.pushback(6);
v1.print();
v1.popback();
v1.print();
cout << endl;
Vector<int>v2(v1);
v2.print();
Vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << " " ;
it++;
}
cout << "我是循环迭代器";
cout << endl;
for (auto e : v1)
{
cout << e << " ";
}
cout << "我是for迭代器";
cout << endl;
Vector<int> v3;
v3 = v2;
v3.print();
}
void test2() //测试insert和erase
{
Vector<int> v1;
v1.pushback(1);
v1.pushback(2);
v1.pushback(3);
v1.pushback(4);
v1.pushback(5);
v1.print();
Vector<int>::iterator pos = find(v1.begin(), v1.end(), 3);
v1.insert(pos, 250);
v1.print();
v1.insert(pos, 0);
Vector<int>::iterator pos1 = find(v1.begin(), v1.end(), 250);
Vector<int> ::iterator e_ret = v1.erase(pos1);
e_ret = v1.erase(e_ret);
e_ret = v1.erase(e_ret);
v1.print();
}
}
在代码的最后我们来总结一下比较难理解的地方:
我们先谈一下构造函数
可以给一个 n ,再给一个模板参数类型的值。这种情况很简单,就是给n个T。
当然还有下图这样的写法:
这样写法的意思是,类模板的成员函数还可以再是函数模板。
我们解析一下这句话,类模板的成员函数指的是 InputIterator ,就是指我们的迭代器类型。我们把这些类型当作模板参数,再写一个函数模板。这样就可以根据迭代器类型自动实例化对应的迭代器了。
对于resize函数,我们先来看看stl库中是怎么实现的。
这里的第一个参数为开辟容量的大小,第二个参数是我们需要给的默认值。
我们从官方库中可以看到,这里的value_type 是指第一个模板参数,也就是T。
所以我们的函数可以这样写:
void resize (size_type n , T val = T() );
这里第二个参数是一个缺省参数,我们可以叫做T() 的匿名构造函数,我们不给值默认初始化为0。
int () , int都有默认构造函数 ,默认值也是0.
说完resize ,我们再看看insert 我们需要注意的地方。
这里是我们insert的实现,大家看看有什么问题。
我们在reserve中进行开辟空间,并进行原数组中数据的迁移,在reserve中,开辟新空间,我们会手动更新_start变量,
会自动更新pos位置的变量。但是我们可以看到,我们这里函数的参数是非引用的,也就是说这里reserve进行pos的改变并不会影响到我们insert函数中pos的位置,这时候pos可能已经变成一个野指针了。
总而言之,reeserve中对变量的更新为了确保开辟空间和返回的成功,但是无法确保调用它的函数insert中pos也跟着更新,从而reserve释放空间导致pos野指针的产生。这里有一种方法是进行取引用,这样当然可以解决这样的问题。但是存在很多其他问题(埋一个坑,我暂时也不知道)。
当然insert和erase还很容易引起我们的迭代器失效问题,我们简单提一下:
当我们写出这样的代码,很明显就有问题了,这个代码再处理偶数会跳过有效数据。
当末尾为偶数会错开循环终止条件。
这里我们处理的方法是接收erase函数的返回值,因为erase的返回值是它下一个数据的迭代器。
这里具体想知道的同学可以选择移步这里:
https://blog.csdn.net/Iiverson2000/article/details/119218457
拷贝构造函数
我们先来说一下拷贝构造的函数名
这两种写法都是可以的,当然这里建议加上类模板参数
第二点:当我们不写拷贝构造函数,用编译器默认的时候,它对内置类型变量只会完成浅拷贝。我们知道我们的内置类型变量是三个指针,对指针进行浅拷贝,把指针类型直接拷贝过来,两个vector用的同一套指针。这问题可很严重。所以我们要自己实现深拷贝。其实深拷贝就是调用reserve进行开空间。
我们进入拷贝构造内部看看需要有什么注意的地方
这是一种实现方法,我们一定要给this也就是被拷贝的对象的指针赋初值,不然我们进入reserve中会操作到随机值的指针。
这里用范围for循环进行赋值真的是很巧妙,还可以用memcpy进行拷贝。
但是这里一定要把指针变量_finish进行更新一下,endofstorage是不用更新的,他在reserve开空间的时候已经更新过了。
只有finish在进行赋值的时候会改变。所以更新一下它就可以了。
当然我们可以不用reserve,我们直接自己new,这里把三个变量都更新一下就好了。
接下来轮到我们的赋值运算符重载
同样的不写默认是浅拷贝,我们vector中都是指针,怎么能用浅拷贝呢?
这里有复杂写法和nb写法,我们再来总结一下怎么写。当然函数名是一样的,返回值是引用,参数最好写成const 引用。
复杂写法:v1=v2
0、判断是否给自己赋值 1、deletev1原空间 2、new一个v2大小的空间 3、memcpy或其他方式拷贝数据 4、更新变量 5、返回新的指针。
这里理解一下为什么要判断不能给自己赋值,因为第一步删除了空间,那第二步又new新值,这样直接造成野指针问题。
nb写法:
这里需要注意的是,我们返回值依旧是this指针,参数变成了非引用非const,
这里的大致意思为:一份有效的数据通过拷贝构造到参数中,我们用没有数据的this和拷贝来的有效数据进行交换,
最后函数结束,出栈帧,拿到无效数据的形参v 随着栈帧释放。
在形象一点吧:我叫外卖员给我送外卖,他来我们家,我拿了我需要的食物,并把家里的垃圾让他带走。我只是多花了点钱,相当于多进行了一次拷贝构造。
当然我们可以把swap进行包装一下:
如果不加域限定符,那swap会自动匹配局部我们写的单参数的函数进行调用,这里我们需要用std中的模板函数。
这样使用也是有前提的,我们需要把命名空间展开在头文件之前,不然全局也无法找到std的swap函数。
这里我们把基本的函数的实现和理解都了解的差不多了
当我们要构造一个这样的函数时,会发生什么情况呢?
我们构造一个vector ,每个数据都是一个字符串。
我们先来解释一下这里pushback是如何实现的。
v. push_back(“11111”)
v.push_back( string = “11111”)
其中,string = “11111” 就是先进行构造,用11111构造一个隐藏的string,在拷贝构造给我们这里的string。
这叫做隐式类型转换
当然,我们的编译器直接给我们优化成了构造。
这里了解后,我们构造内部是如何实现的呢?
当我们这样操作,把每一个v值拷贝给e 。 其中v中都是字符串string。
string我们也实现过的,内部变量还是指针,每一次的赋值给e都是一次深拷贝。
所以我们最好给它加引用,const也可以加上,我们只进行读取操作。
我们正式开始验证代码
我们发现前四个输出都是正常的,第五个输出是随机值。
当我们把第一个字符串给的很长很长 “11111111111111111”时,也会产生随机值,在析构的时候还会崩溃。
我们可以知道,我们在进行pushback的时候,如果空间不够会进行reserve扩容,其中我们reserve是用memcpy实现的自己原本数据的迁移。
每个string内部的指针是指向堆内开辟的空间的。那么1111 等字符串是在堆区域中,并不在我们的vector中,所以当我们大于四个串进来进行扩容的时候,我们会把vector中字节一个一个拷贝过来,当然我们知道,指针怎么能拷贝呢?那不是指向同一个地方,也就是说,我们在新开辟的二倍空间中也会有一个和原来指针一模一样的指针,指向同一个空间,那么这时候析构原空间,我的新空间没地方指了就会发生错误。这也就是为什么当pushback第五个数据的时候报错的原因。
那当我们第一个字符串给很长“111111111111”的时候为什么会报错呢?他没有开辟空间呀?其实不是这样的。
我们在vs中,它的string很有特色,
它不仅仅有一个指针ptr,他还有一个16个空间的数组buff [ 16 ].当数据小于16 我们会进行在buf中存,如果大于16就存在堆上的str上。
当我们给的串长度小于16,我们在进行拷贝的扩容拷贝reserve的时候,这里的浅拷贝会因为buf数组把串内的数据拷贝到新空间中,这样也就没问题了,如果当串大于16字节,那么在堆上的串通过浅拷贝是无法拷贝过来的,就和第一种情况一样了。
为什么说是vs特色呢?因为在linux下可是没有这样的buf,而全是_str 的。
我们总结一下上面的内容:
如果T 是内置类型,或者浅拷贝的自定义类型,在增容和拷贝构造时,使用memcoy是完全没有问题的,但是遇到深拷贝的自定义类型比如这里的string。我们就无法用memcpy拷贝。
那么该如何解决呢?
我们来看看这样的方法可行吗?
这里其实很容易理解, tmp [ i ] 是vector中的一个又一个字符串,同时_start [ i ]也是,那么字符串给字符串赋值就会调用对应的赋值重载进行深拷贝赋值。这样我们的问题就解决了。
当然我们c++stl中实现的方式是类型萃取的方式,简单的说就是能够区分我们的变量类型。
内置类型就用memcpy,自定义类型就是用我们这里的解决方案:for + 赋值重载的方式。
这里就深刻的体现了c++极致追求效率的品质。