文章目录
- 模拟实现
- 1. 迭代器
- 2. 容量操作
- (1)size和capacity
- (2)reserve
- (3) resize
- 3. 元素访问
- (1)下标 + [ ]
- 4. 修改操作
- (1)push_back
- (2)insert
- (3)erase
- (4)swap
- 5. 默认成员函数
- (1) 构造函数
- (2) 拷贝构造
- (3) 复制重载
- (4) 析构函数
- 全部代码参考
模拟实现
整体的架构:我们要实现的vector类是包在【hao】这个命名空间中,而对于这个类而言,我要将其定义为一个 模版类
,这一块如果还有同学不太熟悉的话可以去看看 C++模版
这里需要设置三个私有成员变量,都是指针类型分别是[_start]、[_finish]、[_end_of_storage]
。
_start表示数组的起始地址
_finish表示数组中最后一个元素的下一个 的 地址,功能上可以理解为类似于数据结构中实现顺序表结构体定义中的size。比如有个数组开了十个空间那里面有三个数据,那_start指向的就是0号下标处元素的地址。一共三个数据,下标占用0,1,2,那_finish就指向下面为3元素的地址(所谓最后一个元素**的下一个** 的 地址)
_end_of_storage表示数组最大容量的地址,功能上可以理解为类似于数据结构中实现顺序表结构体定义中的capacity。
这三个成员变量都是指针那我们如何计算该数组中到底有多少个元素,容量是多少呢?答案是我们可以通过指针相减的操作得到
数组中元素个数(即size)=_finish-_start
数组的最大容量(即capacity)=_end_of_storage-_start
将[_start]、[_finish]、[_end_of_storage]
提前声明的形式将它们都初始化为nullptr,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了。
并且我们定义了两种迭代器,在vector中迭代器本质上就是指针,一种是普通指针,一种是const类型的指针,我们使用typedef将指针重命名一下,分别叫iterator和const_iterator
//使用命名空间的目的是将自己实现的vector和库中的vector区分一下
namespace hao //命名空间的名字可以随便取
{
template<class T>//模板
class vector {
public:
typedef T* iterator; //普通迭代器
typedef const T* const_iterator; //const类型的迭代器
// 主要接口函数
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
1. 迭代器
返回值有两种一种是普通版本iterator
另一种是const版本const_iterator
。
- 普通类型的迭代器可以修改迭代器指向元素的值
- const类型的迭代器不允许修改迭代器指向元素的值
在函数后面加个const是为了让const类型的vector对象进行调用
因为const类型的vector对象不能调用不加const的成员函数?????
const_iterator begin() const
//迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
//const类型的迭代器
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
2. 容量操作
(1)size和capacity
size可以获得vector中元素个数,也就是我们在上面所讲的_start和_finish这两个迭代器之间的距离,vector迭代器的底层其实就是指针,那要计算出两个指针之间的数据个数的话让它们做一个相减_finish - _start那对于整个容器的容量【capacity】来说,即为_end_of_storage - _start。
_finish - _start就是size
_end_of_storage - _start就是capacity
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
(2)reserve
reserve就是提前开好空间,避免频繁扩容,提高效率。当然当传入的参数n>capacity时才会执行扩容机制
这个函数的逻辑简单来说就是,新开一块大小为n的空间,然后将原先数组中的元素全部都拷贝到新开的数组中去。
拷贝的逻辑我们可以使用内存函数memcpy(),拷贝完后再去释放原空间,接下去把这些成员变量去做一个更新即可。但是这里需要注意的是memcpy拷贝的时候是原封不动的去拷贝,在拷贝内置类型时没啥问题,但拷贝自定义类型的时候就可能会产生错误。
下面的代码虽然看起来没有问题,但实际上问题不少。
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n]; // 开一块新空间
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
}
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n;
}
}
在执行中间这条语句的时候就有问题。首先会调用size()函数,那size()函数是如何实现的呢?是通过返回return _finish - _start来实现的,可是在调用之前_start已经修改为tmp了,由此计算出来的size就一定有问题,所以我们这里不能在 _start修改之后调用size(),而是要提前将size保存起来size_t sz = size(),以此来保证_finish的计算正确
_start = tmp;
_finish = _start + size();//这条语句的问题
if (n > capacity())
{
// 先保存一下原先的size()
size_t sz = size();
T* tmp = new T[n]; // 开一块新空间
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
第二个问题就是这个memcpy的问题,它会原封不动的进行拷贝,对于内置类型来说没有问题,但如果这里我们使用==vector< string >==呢?
我们知道string在底层维护一个char类型的数组,也就是有个char* 类型的指针指向字符串的首字符的地址。
假如这里有两个string类对象A和B,使用memcpy将B拷贝到A中去,因为它会原封不动的进行拷贝,所以会导致A中的指针和B中的指针指向同一块空间,在A进行释放指针指向的空间的时候就会导致B中的空间也会被释放。
带来的问题就是把A释放了,B也不能用了,这部就出问题了么,所以我们不能使用memcpy,因该使用for循环调用string的赋值重载深拷贝解决这个问题。
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];//调用string的赋值重载进行深拷贝
}
最终正确的代码:
void reserve(size_t n)
{
size_t sz = size();
T* tmp = new T[n];
if (n > capacity())
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
总结
1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是
浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
(3) resize
resize分为三种情况
- 第一个是n < _finish的情况,此时只需要修改_finish的值就可以了
- 第二个是n > _finish && n <= _end_of_storage的情况,第二种和第三种统一交给reserve去判断
- 第三个是n >_end_of_storage的情况;
第一种情况下,直接让==_finish = _start + n==,如果是第二第三种情况,先使用reserve去检查一下是否需要扩容,然后再去通过循环追加对应的数据即可。
这里我们使用了const T& val = T(),给了一个默认的缺省参数T(),当前的形参val的类型使用的就是模版参数类型,采取自动推导的形式去进行自动识别
而T()则是【匿名对象】,这里不可以给0,因为当前的数据类型不一定就是 整型,我们可以根据这个匿名对象去生成不同的默认值
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
3. 元素访问
(1)下标 + [ ]
访问元素最常用的方法就是下标 + [ ]
,也有两个版本,const版本和非const版本
底层实现方法也比较简单,直接返回数组中的值就好了。
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
4. 修改操作
(1)push_back
每次调用push_back,在函数内部我们都需要判断是否需要扩容,在vector的使用方法一文中已经说了,在vs中的扩容逻辑是1.5倍,在Linux下是2倍扩容逻辑。在这里我们采用2倍扩容逻辑,并把扩容操作直接交给reserve函数处理。
void push_back(const T& x)
{
//先判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);//利用reserve扩容
}
*_finish = x;//插入元素x
++_finish;
}
(2)insert
insert函数有两个参数,第一个是一个迭代器,表示要插入的位置,第二个表示要插入的元素,返回值是一个迭代器,只要目的是为了解决迭代器失效的问题。
iterator insert(iterator pos, const T& x)
函数内部首先需要判断一下pos的位置是否合法,可以在_start位置插入,也可以在_finish位置插入,所以pos >= _start && pos <= _finish
assert(pos >= _start && pos <= _finish);
如果pos位置有元素,那就把pos位置以及pos以后的元素全部向后挪一个位置,然后把元素放在pos位置。比如在_start位置插入元素7的话,那就把1、3、4、6、3 全部向后挪一个,然后把7放在_start位置。
既然是插入元素,我们当然要检查是否扩容。
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
但是呢扩容就又会有个新的问题,扩容相当于新开了个空间,但是你pos指向的还是原来空间的位置啊,你还按原来的插入,那不肯定插入错了吗?所以我们要提前计算好pos距离_start的长度len,然后将新_start加上len就获得了,正确的位置pos。
==还有个问题就是我们在调用insert函数肯定会传入一个迭代器比如下面这样,但是如果现在正好发生了扩容,那我们定义的it岂不是指向的不是begin位置了吗?也就说此时这个迭代器就失效了,即发生了迭代器失效问题。==解决方案就是返回一个迭代器指向pos位置即可,所以insert的函数返回值才是iterator类型的。
vector<int> v(5,3);
auto it = v.begin();
v.insert(v.begin,10);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;//计算len
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;//计算新的pos
}
之后就是一个一个的往后挪数据,最后插入了。
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
完整代码:
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
(3)erase
有了insert的铺垫,在理解erase就比较简单了。
首先还是判断一下pos是否在合法的区间内assert(pos >= _start && pos < _finish);
后面就是挪数据了,insert是往后挪,erase就是往前挪了。
那erase也会发生迭代器失效的问题吗,答案是会的。如果我们把pos设置为最后一个元素,那么在调用erase时,元素会往前挪,那pos位置就指向end了,这不就越界了吗?,所以我们依旧返回一个iterator,解决这个问题。
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
(4)swap
就是去调用库里面的这个模版函数swap,去一一交换两个对象中的三个成员变量即可。这个接口我下面在讲赋值重载时会使用到。
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
5. 默认成员函数
(1) 构造函数
逻辑上和resize一样,所以直接调用resize就行了。
// 有参构造
vector(size_t n, const T& val = T())
{
resize(n, val);
}
由于一开始我们就对private成员进行了初始化,所以就不用在构造函数里初始化他们了
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
迭代器初始化
调用这个函数需要传入一个迭代器区间,由于你传入的迭代器不知道是string的迭代器,还是list的迭代器,还是谁谁谁的迭代器,类型不确定,所以我们这里要弄成模板,让编译器去自己推导。
//[first,last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
但是我们指向这条语句的时候就会发现问题
hao::vector<int> v5(10, 1);
指向这条语句我们发现没有调用vector(size_t n, const T& val = T())反而调用了vector(InputIterator first, InputIterator last)。在这里面有个push_back(*first);
,有个解引用操作,众所周知指针和迭代器可以解引用,基本数据类型是不能解引用的,那肯定就会出问题。
那原因是为什么呢?
原来是模版参数会去进行自动类型推导,从而匹配最合适函数模版。因为我们在这里所传入的10和1都是int类型,但是有参构造的第一个形参类型为==size_t,==并不是最匹配的 而迭代器区间初始化其参数类型都是模版参数,所以在匹配的时候它是最优先进行匹配的,因此就导致了上面的事故。
如何解决呢?当然是直接补充一个形参是int类型的函数重载就行了。
vector(size_t n, const T& val = T())
{
resize(n, val);
}
vector(int n, const T& val = T())
{
resize(n, val);
}
那我们在写了这个重载函数后要如何去调用对应的无符号类型size_t呢,此时我们只需要在传递的参数后加上一个u即可,那么编译器在进行识别的时候就会自动将其识别成为无符号整数。
hao::vector<int> v6(10u, 6);
(2) 拷贝构造
如果我们不手动实现的话,默认是浅拷贝,会出很多问题,所以我们这里手搓一个深拷贝出来。注意这里没用memcpy,原因上面也说了。原理比较简单,直接上代码。
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
(3) 复制重载
这里就用到了我们之前实现的swap函数
我们使用到这么一个巧计,那就是使用 传值传参
,首先会去调用一个拷贝构造构造一个临时对象,但是临时对象出了作用域之后肯定是要销毁的此时我们就可以使用【swap】和当前对象去做一个交换,我呢获取到了你里面的内容,你帮我释放了不需要的内容,简直一举两得。
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
(4) 析构函数
释放空间,指针置空,没什么好说的。
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
全部代码参考
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace hao
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
{
}
//[first,last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(size_t n, const T& val = T())
{
resize(n, val);
}
vector(int n, const T& val = T())
{
resize(n, val);
}
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
//迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
//const类型的迭代器
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
size_t size() const
{
return _finish - _start;
}
void reserve(size_t n)
{
size_t sz = size();
T* tmp = new T[n];
if (n > capacity())
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
++_finish;
}
void pop_back()
{
erase(--end());
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
void print(const vector<int>& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
void test_vector1()
{
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 e : v1)
{
cout << e << " ";
}
cout << endl;
for (size_t i = 0; i < v1.size(); i++)
{
v1[i]++;
}
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
print(v1);
}
void test_vector2()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(5);
v1.push_back(5);
v1.push_back(5);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
v1.insert(v1.begin(), 100);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
/*vector<int>::iterator p = v1.begin() + 3;
v1.insert(p, 300);*/
vector<int>::iterator p = v1.begin() + 3;
//v1.insert(p+3, 300);
// insert以后迭代器可能会失效(扩容)
// 记住,insert以后就不要使用这个形参迭代器了,因为他可能失效了
v1.insert(p, 300);
// 高危行为
// *p += 10;
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test_vector3()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
it = v1.erase(it);
}
else
{
++it;
}
}
//v1.erase(v1.begin());
//auto it = v1.begin()+4;
//v1.erase(it);
erase以后,迭代器失效了,不能访问
vs进行强制检查,访问会直接报错
//cout << *it << endl;
//++it;
//cout << *it << endl;
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test_vector4()
{
vector<int> v;
v.resize(10, 0);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
int i = 0;
int j = int();
int k = int(1);
}
void test_vector5()
{
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> v1(v);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
vector<int> v2;
v2.resize(10, 1);
v1 = v2;
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test_vector6()
{
vector<string> v;
v.push_back("111111111111111111");
v.push_back("222222222222222222");
v.push_back("333333333333333333");
v.push_back("444444444444444444");
v.push_back("555555555555555555");
for (auto& e : v)
{
cout << e << " ";
}
cout << endl;
vector<string> v1(v);
for (auto& e : v1)
{
cout << e << " ";
}
cout << endl;
}
void test_vector7()
{
//vector<int> v(10u, 1);
vector<string> v1(10, "1111");
vector<int> v2(10, 1);
// vector<int> v;
//for (auto e : v)
//{
// cout << e << " ";
//}
//cout << endl;
//vector<int> v3(v.begin(), v.end());
//for (auto e : v3)
//{
// cout << e << " ";
//}
//cout << endl;
string str("hello world");
vector<char> v4(str.begin(), str.end());
for (auto e : v4)
{
cout << e << " ";
}
cout << endl;
int a[] = { 16,2,77,29 };
vector<int> v5(a, a + 4);
for (auto e : v5)
{
cout << e << " ";
}
cout << endl;
}
}