目录标题
- 准备工作
- 构造函数
- 迭代器的完善
- 性质相关的函数实现
- reserve
- push_back
- [ ]
- empty
- resize
- insert
- erase
- erase后迭代器失效问题
- swap
- clear
- ~vector
- 老式拷贝构造
- 迭代器构造
- 新式拷贝构造
- 老式赋值重载
- 新式赋值重载
- N个数据的构造
- vector的浅拷贝问题
准备工作
首先我们知道vector是一个顺序表,并且可以存放各种各样类型的数据,所以我们这里的类得是一个模板类,因为vector中的数据存放是一段连续的空间我们可以通过对指针的加减从而来获得一系列的数据,所以vector的迭代器就可以通过对指针重命名来实现,其次迭代器有两个类型一个是可读可写的迭代器,一个是只读迭代器,那么这里的代码就如下:
namespace ycf
{
template<class T>
class vector
{
typedef T* iterator;
typedef const T * const_iterator;
};
}
在string的模拟实现当中,我们是通过三个变量(_str , _size,_capacity)来控制string对象的增删查改,这三个变量的类型分别为 char* , size_t , size_t,由于vector中存放着各种各样类型的数据,所以在vector里面我们就得用迭代器类型的数据来控制vector中的内容,但是不管类型怎么变,这里无疑还是需要三种功能的数据:记录对象数据开始的位置,记录插入数据的位置,记录该对象的容量为多少,那么这里的代码就如下:
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T * const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
这里的_start
等于string中的_str
,这里的_finsih
等于_str+_size
,这里的_end_of_storage
等于_str+_capacity
,那么到这里我们的准备工作就完成了。
构造函数
实现类的第一步就是把对应的构造函数写出来,这样才可以实现接下来的增删查改,vector中有三个成员变量其类型都是iterator,iterator是指针类型重命名而来的,而我们刚开始执行构造函数的时候是没有内容的,所以在构造函数里面我们就得将这三个成员变量赋值为空指针,那么这里的代码就如下:
vetor()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
迭代器的完善
要想使用迭代器,我们还得实现begin和end函数,那这里就非常的简单,在begin函数里直接将成员变量start作为返回值进行返回,在end函数里面直接将成员变量finish的值作为返回值进行返回,那么这里的代码就如下:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
但是迭代器有两种不同的类型,一种是可读可写类型一种是只读类型,而我们上面写的是可读可写类型,那么下面我们要补充的就是只读类型,这种类型的begin和const得在this指针和返回类型上面加上const,比如说下面的代码:
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;
}
reserve
插入数据避免不了对象的扩容,所以在实现插入数据的时候我们得先实现扩容函数reserve,这个函数就一个参数用来表示你要扩容的空间能够容乃多少个数据,所以该函数的参数就是一个无符号的整型,并且没有返回值:
void reserve(size_t n)
{
}
为了提高代码执行的效率,在reserve里面我们只对对象进行扩容不会对对象进行缩容,所以我们这里在函数的开始就得判断一下:如果传过来的n比对象的容量小的话我们就不进行任何的操作:
void reserve(size_t n)
{
if (n > capacity())
{
}
}
在if里面我们就要实现扩容操作,首先使用new申请n个T类型大小的空间,如果原来的对象里面有数据的话我们就将原来空间中的内容拷贝到新的空间里面再将该空间进行释放,最后对三个成员函数进行修改使其能够描述新的空间,那么这里的拷贝得用到memcpy函数,这里的该函数的介绍如下:
该函数的代码实现如下:
void reserve(size_t n)
{
if (n > capacity())
{
iterator tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, size() * sizeof(T));
delete[] _start;
}
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n;
}
}
但是这样实现会存在一个问题:我们是先修改_start的值再修改_finish的值,而修改finish的时候会调用size函数该函数的返回的值是_finish - _start,那么这里就会出现一个问题:返回值里面的_finsh指向的是原来的空间,但是这时候start已经指向了新的空间,这两个值相减并不会得到原来对象的长度,而是一个随机值,所以上面的代码就会存在问题,那这里解决方法也很简单我们可以将两个值
修改的顺序交换一下,先改_finish再改_start:
void reserve(size_t n)
{
if (n > capacity())
{
iterator tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, size() * sizeof(T);
delete[] _start;
}
_finish = _start + size();
_start = tmp;
_end_of_storage = _start + n;
}
}
或者再创建一个变量oldsize用来记录原来对象的长度,这样在修改_finish的时候就不用调用size函数而是直接使用oldsize的值,代码就如下:
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
iterator tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, size() * sizeof(T);
delete[] _start;
}
_start = tmp;
_finish = _start + oldsize;
_end_of_storage = _start + n;
}
}
push_back
有了扩容函数这里的push_back就可以顺利的实现,实现的步骤如下:首先判断一下该对象是否需要进行扩容,如果_finish的值等于_end_of_storage的值的话我们就进行扩容,如果原来对象的容量为空的话我们就扩容四个空间出来,如果原来的容量不为空的话我们就将容量扩大为原来的两倍,那么这一步的代码如下:
void push_back(const T& x)
{
if (capacity() == size())
{
int new_capacity = _size == 0 ? 4 : 2 * capacity();
reserve(new_capacity);
}
}
检查完是否需要扩容之后我们就可以插入数据,由于成员变量_size刚好指向数据要插入的地方,所以我们这里就可以直接对_size进行解引用然后使用参数对解引用后的结果进行赋值,再将_size的值加1,这样我们就完成了数据的插入,那么完整的代码就如下:
void push_back(const T& x)
{
if (capacity() == size())
{
size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(new_capacity);
}
*_finish = x;
++_finish;
}
写到这里我们就可以写一段测试代码来看看上面写的函数是否正确:
void test1()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
cout << "修改前对象的数据个数为:";
cout << v1.size() << endl;
cout << "修改前对象的容量为:";
cout << v1.capacity() << endl;
v1.reserve(10);
cout << "修改后对象的数据个数为:";
cout << v1.size() << endl;
cout << "修改后对象的容量为:";
cout << v1.capacity() << endl;
ycf::vector<int>::iterator it1 = v1.begin();
while (it1 != v1.end())
{
cout << *it1<<" ";
it1++;
}
}
我们将代码运行一下就可以看到这里的函数实现是真确的:
[ ]
为了方便修改vector对象中的数据,我们得在该类中实现该操作符的运算符重载,这个重载只需要一个参数i,在函数体里面先判断给的下标是否合法,然后再将其对应的数据引用返回即可,那么这里的代码就如下:
T& operator [](size_t i)
{
assert(i < size());
return *(_start + i);
}
因为该操作符的重载可读可写,所以我们这里就还得实现一个只读的形式,该形式就得在this指针和返回值上各加一个const,以防止数据的修改
const T& operator [](size_t i) const
{
assert(i < size());
return *(_start + i);
}
我们来看看下面的测试代码:
void test2()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
size_t i = 0;
while(i<v1.size())
{
++v1[i];
++i;
}
i = 0;
while (i < v1.size())
{
cout << v1[i] << " ";
++i;
}
}
这段代码的运行结果如下:
我们可以看到这里的执行结果没有错误,所以我们这里的代码的实现是没有问题的。
empty
这个函数的作用就是用来判断该对象的内容是否为空,如果为空的话这个函数就会返回true,如果不为空的话这个函数就会返回flase,那这里的实现就非常的简答如果内容为空的话_start的值就会等于_finsh,所以我们直接将这个表达式的结果作为empty函数的返回值,那么这里的代码就如下:
bool empty() const
{
return _finish == _start;
}
resize
首先这个函数得需要一个参数来表示这里修改之后的对象数据个数,然后还需要一个参数用来作为填充的内容,那么该函数的声明就如下:
void resize(size_t n, T val = T())
{
}
因为你想要修的内容个数有大有小,所以我们这里就得分情况进行讨论,如果你给的个数小于原本个数的话,我们这里就可以通过修改_finsih的值来实现内容的删减:
void resize(size_t n, T val = T())
{
if (n < size())
{
_finish = _start + n;
}
}
因为reserve不会进行缩容当你给的n小于capacity的时候不会执行任何操作,所以我们这里不管参数n是否大于_capacity我们都调用reserve函数,这样的话看起来就简洁一点,扩容完之后我们这里就可以根据n的值来插入适当的数据,这里可以使用while循环在循环体里面对_finish进行解引用和赋值来实现对应的目的,那么这里的代码就如下:
void resize(size_t n, T val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n )
{
*_finish = val;
++_finish;
}
}
}
下面是该函数的测试代码:
void test3()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (auto v : v1)
{
cout << v << " ";
}
cout << endl;
v1.resize(2);
for (auto v : v1)
{
cout << v << " ";
}
cout << endl;
v1.resize(10, 10);
for (auto v : v1)
{
cout << v << " ";
}
}
该代码的运行结果如下:
insert
这个函数就是实现任意位置的插入,这个函数有两个参数一个表示具体的位置,一个是你要插入的数据,所以该函数的声明就如下:
void insert(iterator pos, const T& val)
{
}
然后我们得判断一下你给的位置是否合法:
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos < _finish);
}
在插入数据之前我们还得判断一下这里是否需要进行扩容:
void insert(iterator pos, const T& val)
{
assert(pos >=_start);
assert(pos < _finish);
if (_finish == _end_of_storage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
}
因为这里是随机位置插入数据,所以我们这里还得干一件事就是将该位置往后的数据全部都往后面挪动一个位置,那么我们这里就可以创建一个迭代器end将其值赋值为对象中的最后一个数据,然后再创建一个while循环,每次循环都将end位置的数据复制到end+1的位置上,然后再让end的值减一,那么这个循环结束的条件就是end<pos的时候结束循环,循环结束之后我们就可以在对应位置上插入数据然后将_finsh的值++,那么这里的代码就如下:
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos < _finish);
if (_finish == _end_of_storage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
// 挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
}
我们可以用下面的代码测试一下这个函数的正确性:
void test4()
{
ycf::vector<int> v1;
v1.reserve(10);
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.insert(v1.begin() + 1, 100);
for (auto v : v1)
{
cout << v << " ";
}
}
该代码的运行结果如下:
我们可以看到这里的代码运行结果是正 确的,但是这个函数的实现难道没有问题吗?我们再写一段测试代码:
void test4()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.insert(v1.begin() + 1, 100);
for (auto v : v1)
{
cout << v << " ";
}
}
将这段代码运行一下就会发现这里的insert函数有问题:
这里没有将100插入到对象里面,而且打印数据的时候出现了随机值那这是为什么呢?原因很简单,这里的迭代器pos失效了,我们是根据迭代器pos来帮助我们插入数据,pos是以参数的形式被这个函数得到的,它指向你要插入的空间,但是insert在插入数据的时候是第五个数据此时的容量为4,那么这时在insert函数里面就会进行扩容,c++的扩容一定是异地扩容,原来的数据都会被搬往另外一个空间,但是迭代器pos这时并没有更新啊,它还是指向着原来的老空间,所以后面在插入数据的话就会出现插不进去的情况,但是我们将_finish的值加了1,后面的内容没有新的数据也没有初始化所以这里就会在末尾打印出一个随机值出来,那么这里就是出现问题的过程,解决问题的方法也非常的简单,我们创建一个变量len用来记录pos到开始位置的长度,等我们扩容完之后再用变量len将pos的值进行一下初始化就可以解决上述的问题,那么这里的代码就如下:
void insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos < _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
///扩容会导致pos迭代器失效,需要更新处理一下
pos = _start + len;
}
// 挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
}
那这里就有个问题insert之后的迭代器还能够使用吗?答案很明显是不能的,比如说下面的代码:
vector<int>::iterator it = find(v.begin(), v.end());
v.insert(it, 30);
因为形参的改变不会影响到实参,如果insert发生了扩容外面的迭代器it就变成了野指针,所以这里大家得注意下。
erase
这个函数的功能就是删除指定位置的数据,那该函数的实现就跟上面的insert函数非常的类似,这里不会插入新的数据所以该函数只有一个参数,也就是迭代器用来指向删除的位置,在函数里面还得判断一下删除的位置是否合理,所以还得用到assert函数来进行判断,这一步的代码如下:
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
}
然后我们创建一个迭代器begin将其值赋值为pos,创建一个while循环在循环体的内部将begin位置的值赋值为begin+1位置的值,再让begin的值加一,
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos;
while (begin < _finish)
{
*begin = *(begin + 1);
++begin;
}
}
最后将_finish的值减一,并返回迭代器pos那么完整的代码就如下:
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos;
while (begin < _finish)
{
*begin = *(begin + 1);
++begin;
}
--_finish;
return pos;
}
我们来看看下面的测试代码:
void test5()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.erase(v1.begin() + 1);
for (auto v : v1)
{
cout << v << " ";
}
}
其运行的结果如下:
符合我们的预期所以这里的函数实现是正确的。
erase后迭代器失效问题
这里的erase函数会导致迭代器失效吗?根据我们的推理应该是会的,比如说我们一个vector对象里面含有4个数据,我们创建了一个迭代器使其指向了最后一个数据,然后再用这个迭代器来调用erase函数,这时我们就将最后一个数据删除了,可是该迭代器的值却不会发生改变,他还是指向了原来的位置,但是这个位置已经没有了数据,所以该迭代器变成了一个野指针也就跟着失效了,这是一种特殊的情况,那对于其他情况下erase后的迭代器会失效吗?我们来看看下面这段代码:
void test17()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector<int>::iterator it1 = v.begin();
v.erase(it1);
cout << *it1 << endl;
++(*it1);
cout << *it1 << endl;
}
我们将其运行一下就可以发现这里是打印出来任何的数据的:
原因是迭代器失效了,编译器无法对失效的迭代器进行其他的操作,这时vs编译器的运行结果,erase之后的迭代器会失效,那在g++编译器下erase迭代器会失效吗,我们看看下面这段代码:
将这段代码运行一下,看看这里的运行结果:
我们可以看到这里的在g++编译器里面erase之后vector的迭代器不会失效,那这是怎么回事呢?我们再来看看下面这段代码
这段代码的意思就是遍历vector中的数据,如果数据中出现了偶数那么就将这个数据删除,将这段代码运行一下我们发现这里好像有点不对劲,运行结果报错
这里告诉我们这段代码存在越界访问的错误,我们将这里的数据进行一下修改在尾部添加一个5再看看运行的结果如何:
我们可以看到这里运行的结果又没有了问题,我们再对数据进行修改将其改成1 2 2 3 4 5 再运行一下看看结果如何:
我们看到这里的运行结果又变得不正常了,这里没有爆出越界访问的错误,但是整数2却没有被删除,那这是为什么呢?原因是erase函数在删除数据的时候会挪动数据,使后面每个数据都会向前挪动一格,但是不管if语句是否判断位置,都会++it,这就会使得在一次循环当中可能会出现删除两个数据的情况,比如说下面的图片
当迭代器来到2时if语句会判断为真,然后将2删除并将后面的数据全部往前挪动一格,
但是在if语句后面又有一个++it语句,执行完这个语句会将迭代器往后挪动一格,所以此时迭代器会跳过3直接来到了4
那么这时if语句就会判断为真将数据4删除并将后面的数据往前挪动一位,然后再让迭代器往后走一位,
因为while循环判断结束的条件是it是否不等于end,而这时的迭代器的位置在end的后面,所以依然会进入while循环执行里面的语句,那么这里就会爆出越界访问的错误,另外两种情况也是相同的道理,看到这里想必大家直到上面的代码哪里出现了错误,既然erase会让数据往前挪动一个位置,而++it又会让迭代器往后挪动一个位置,那这里我们就可以添加一个if else语句,每次循环要么执行++it,要么执行erase,这样的话不就解决上述的问题了吗,那修改之后的代码如下:
将这段代码运行一下就可以看到第三组数据并没有报错:
第一组数据也没有发生越界访问的错误·:
看到这里肯定有些小伙伴就要说:那这段代码改成这样是不是就没有问题了呢?使用erase删除数据之后迭代器是不是就不会失效了呢?很明显在g++这个平台下迭代器是不会失效的,但是在vs这个平台下迭代器依然会失效,那我们平时在使用的时候到底认不认为他失效呢?很明显嘛肯定得认为他失效嘛,对吧我们得保证自己写的代码能够在所用的平台下全部都能顺利通过,上面修改之后虽然在linux下能跑的过去,但是我们换个平台就不行了比如说下面的图片:
直接报了断言错误,所以我们认为erase删除数据之后迭代器是失效的,那这里解决问题的方法也很简单,使用erase的返回值将迭代器更新一下就可以正常的运行了,比如说下面的代码:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it=v.erase(it);
}
else
{
++it;
}
}
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
这样这段代码就能在所有平台下都能正常的运行:
那么这就是erase的迭代器失效问题,希望大家能够理解。
swap
这个函数的作用就是将两个vector对象中的内容进行交换,因为vector中是通过指针的形式来控制的数据,所以在这个函数里面我们可以调用三次库中的swap函数来将对象中的三个成员变量进行交换,以此来实现该函数的功能,那么该函数的代码就如下:
void swap(vector<T>& v1)
{
std::swap(_start, v1._start);
std::swap(_finish, v1._finish);
std::swap(_end_of_storage, v1._end_of_storage);
}
该函数的测试代码如下:
void test6()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
ycf::vector<int> v2;
v2.push_back(5);
v2.push_back(6);
v2.push_back(7);
v2.push_back(8);
v1.swap(v2);
cout << "v1中的内容为:";
for (auto v : v1)
{
cout << v << " ";
}
cout << endl;
cout << "v2中的内容为:";
for (auto v : v2)
{
cout << v << " ";
}
}
我们将上面的代码运行一下就可以看到这里的函数实现就是真确的:
v1和v2的内容确实发生了交换。
clear
该函数的功能是将对象中的数据全部都清空,因为_finish的值表示的数据的末尾,_start的值表示的是数据的开始,所以我们这里可以通过将_finish的值赋值为_start的值以此来达到删除数据的目的,那么代码就如下:
void clear()
{
_finish = _start;
}
该函数的测试代码如下:
那么该函数的实现就是真确的。
~vector
有了构造函数就得有析构函数,我们这里是通过new在堆区上申请了空间,那么在析构函数里面我们就得通过delete函数来释放申请的空间,并且将对象中的三个成员变量全部都置为空指针,那么该函数的代码就如下:
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
老式拷贝构造
我们首先来看看这个构造函数的声明和对应的初始化列表:
vector( const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
}
这里大家要注意的一下,因为这里的是拷贝构造,会传过来任意类型的大量数据,所以为了提高效率我们这里就得在参数里面创建添加引用以防止拷贝而导致的效率降低,其次拷贝构造不会改变传过来的对象的内容,所以我们还得在参数的前面加上const,那么在函数体的第一步我们就得先开辟一个空间,这个空间的大小就是v的容量大小,然后再用push_back函数将v中的内容一个一个的插入尾插进去,这里的代码如下:
vector( const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(v.size());
for (auto& v1 : v)
{
push_back(v1);
}
}
测试代码如下:
void test8()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
ycf::vector<int> v2(v1);
cout << "v2中的内容为:";
for (auto v : v2)
{
cout << v<< " ";
}
}
代码的运行结果如下:
迭代器构造
我们实现了push_back函数,那么这里的迭代器构造函数就很好的实现了,首先这个函数需要两个迭代器参数,因为该函数会接收各种类型的迭代器,所以我们这里就得加个函数模板,并且还得通过初始化列表将这个对象中的三个成员变量全部都初始化为空指针:
templete<class InputIterator>
vector(InputIterator begin, InputIterator end)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
}
然后再通过while循环加push_back函数将迭代器的内容尾插到对象里面,循环结束的条件就是begin等于end的时候,因为push_back函数会进行成员变量的修改和空间的扩容,所以循环结束之后我们就不用再执行其他的操作,那么这完整的代码就如下:
templete<class InputIterator>
vector(InputIterator begin, InputIterator end)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
while (begin != end)
{
push_back(*begin);
++first;
}
}
我们来看看下面的测试代码:
void test9()
{
ycf::vector<int>v1;
v1.push_back(1);
v1.push_back(10);
v1.push_back(100);
v1.push_back(1000);
v1.push_back(10000);
ycf::vector<int>v2(v1.begin()+1, v1.end()-1);
cout << "v2中的内容为:";
for (auto v : v2)
{
cout << v << " ";
}
}
新式拷贝构造
现代的拷贝构造函数,与老式的最大区别是不用我们自己写一个数据一个数据的push_back而是通过创建一个临时对象将临时对象将零时对象的数据构造成我们想要的,然后再用swap函数本对象的数据与临时对象的数据进行交换,以达到简便的效果,那么这里的第一步还是将拷贝构造的声明和初始化列表写出来:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
}
然后再创建一个临时对象出来并使用迭代器构造函数将其内容初始化为对象v中的数据,最后使用swap函数将其数据进行交换,就可以达到这样的目的,那么这里的代码就如下:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
}
那么这里的测试代码就如下:
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
ycf::vector<int> v2(v1);
cout << "v2中的内容为:";
for (auto v : v2)
{
cout << v<< " ";
}
代码的运行结果如下:
老式赋值重载
这里老式的写法就是先将对象里面的数据清空,然后再创建一个迭代器来控制while循环,在循环里面讲迭代器里面的内容以此出入到对象里面即可,那么这里的代码就如下:
vector<T>& operator=(const vector<T>& v)
{
clear();
iterator it = v.begin();
while (it != v.end())
{
push_back(*it);
++it;
}
return *this;
}
测试代码如下;
void test10()
{
ycf::vector<int>v1;
v1.push_back(1);
v1.push_back(10);
v1.push_back(100);
v1.push_back(1000);
v1.push_back(10000);
ycf::vector<int>v2(v1.begin() + 1, v1.end() - 1);
cout << "v2赋值之前的值为:";
for (auto v : v2)
{
cout << v << " ";
}
cout << endl;
v2 = v1;
cout << "v2赋值之后的值为:";
for (auto v : v2)
{
cout << v << " ";
}
}
运行结果如下:
新式赋值重载
有了上面的铺垫这里的赋值重载就非常的简单,我们在参数那里就无需加上&,因为我们本来就需要一个临时变量,所以直接利用形参是实参的临时拷贝的特性将数据进行交换即可,那么代码就如下:
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
测试代码如下:
void test10()
{
ycf::vector<int>v1;
v1.push_back(1);
v1.push_back(10);
v1.push_back(100);
v1.push_back(1000);
v1.push_back(10000);
ycf::vector<int>v2(v1.begin() + 1, v1.end() - 1);
cout << "v2赋值之前的值为:";
for (auto v : v2)
{
cout << v << " ";
}
cout << endl;
v2 = v1;
cout << "v2赋值之后的值为:";
for (auto v : v2)
{
cout << v << " ";
}
}
运行结果如下:
当然我们这里的赋值重载还存在一个问题就是如果是自己给自己赋值重载的话我们这里可能就会出现问题,但是这种情况属于是极少数情况,即使我们这里不做处理也能够保证很高的真确性,所以我们这里就干脆不做处理了。
N个数据的构造
采用N个数据的构造函数跟我们之前的实现方式差不多,大致的思路就是先开辟n个大小的空间,然后再通过循环和push_back函数来一个一个的插入数据,那么这里的代码就如下:
vector(size_t N, const T& val = T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(N);
int i = 0;
while (i < N)
{
push_back(val);
}
}
我们可以用下面的代码来测试一下这个函数的正确性:
void test11()
{
ycf::vector<int> v(10, 1);
for (auto v1 : v)
{
cout << v1;
}
}
我们将这段代码运行一下就会发现这里出现了这样的问题:
编译器说这里报错了,报错的原因是非法寻址,那为什么会这样呢?原因很简单,我们这里构造函数穿过去的参数是10和1,是两个整型但是我们刚刚写的那个构造函数的参数是一个无符号的整型和一个整型,但是我们这里还写了一个构造函数:
template<class InputIterator>
vector( InputIterator begin, InputIterator end)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
while (begin != end)
{
push_back(*begin);
++ begin;
++ i;
}
}
这个构造函数的是一个模板,需要推导的类型只有一个,所以当我们传的参数是10和1的时候,编译器会将这里的InputIterator推导成int类型,而两个int类型很明显会比这里的size_t和int类型更加合适,所以当构造函数的参数为两个int类型的时候,编译器会调用迭代区间的构造函数,这样的话10和1就会被认为是两个地址,在这个构造函数里面会对其进行解引用,所以就会报出非法寻址的问题,那么这里解决问题的方法就是利用函数重载我们再写一个size_t类型的函数重载:
vector(int N, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(N);
int i = 0;
while (i < N)
{
push_back(val);
++i;
}
}
再运行一下上面的测试代码就会发现结果是对的:
vector的浅拷贝问题
vector的大致实现我们这里已经完成了,但是这里存在着一个问题,我们来看看上面写的reserve函数:
void reserve(size_t n)
{
if (n > capacity())
{
iterator tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, size() * sizeof(T));
delete _start;
}
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n;
}
}
这里采用的是memcpy函数来进行的数据赋值,而我们知道memcpy函数他是按字节来进行拷贝,对于一些整型,浮点型和一些自定义类型来说的话,按字节拷贝内容或许没有什么问题,那如果是一些特殊的类型呢?比如说:vector类型,内部还有指针的自定义类型,这些类型按字节拷贝会不会出现问题呢?我们来看看下面的代码:
void test12()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
ycf::vector<ycf::vector<int>> v(4,v1);
for (auto v1 : v)
{
for (auto v2 : v1)
{
cout << v2 << " ";
}
cout << endl;
}
}
这段代码的运行结果如下:
但是我们在这里再尾插一个数据上去就会发现这里会出现问题:
void test12()
{
ycf::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
ycf::vector<ycf::vector<int>> v(4,v1);
v.push_back(v1);
for (auto v1 : v)
{
for (auto v2 : v1)
{
cout << v2 << " ";
}
cout << endl;
}
}
这段代码的运行结果如下:
那这是为什么呢?我们可以通过画图来理解一下这里字节拷贝所带来的问题:
首先在没有扩容的时候对象v申请的空间里面存放着4个vector<int>
对象,然后每个对象都会指向着一个自己开辟的空间,该空间里面装内容是1 2 3 4,当我们再往对象v中插入一个数据的时候,该对象就会就会进行扩容,会再创建一个空间出来,
然后用memcpy函数进行字节上的拷贝,所以在扩容完之后新空间中就会有四个对象,并且对象之间的内容是一样的:
而我们知道vector中的成员变量是三个指针,如果按照字节拷贝的话,新空间的指针内容和老空间的指针内容会是一样的,所以他们也就都会指向同一块数据:
在扩容函数的最后会执行delete函数,该函数会销毁原来的空间,所以原来的四个对象都会被销毁,当这些对象被销毁的时候,就会顺便执行他们的析构函数,析构函数会将这些对象的内容全部释放掉:
所以这个时候新空间中的指针就会变成野指针,当我们再使用他的时候就会发生报错,那么这就是上面问题发生的原因,该问题主要发生在扩容函数里面,所以得对扩容函数进行修改,memcpy函数会导致浅拷贝,所以这里就不能再使用该函数来拷贝数据,而是得通过for循环将创建出来的空间和原来的老空间进行一个数据一个数据赋值重载,因为我们在实现赋值重载的时候是实现的深拷贝,而不是简单的浅拷贝,所以这里我们就可以采用赋值重载的形式来解决这里的问题,那么这里的代码就如下:
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
iterator tmp = new T[n];
if (_start)
{
/*memcpy(tmp, _start, size() * sizeof(T));*/
for (size_t i = 0; i < oldsize; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + oldsize;
_end_of_storage = _start + n;
}
}
上面的代码再运行一下就可以发现运行是正常的: