文章目录
- 一、接口总览
- 二、vector成员变量
- 三、默认成员函数
- 构造函数① -- 默认无参构造
- 构造函数② -- 迭代器区间构造
- 构造函数③ -- n个val构造
- 拷贝构造函数
- 赋值运算符重载
- 析构函数
- 四、迭代器
- 六、容量以及元素访问的相关接口
- empty
- size和capacity
- reserve
- resize
- 七、增删查改等接口
- push_back
- pop_back
- insert
- erase
- operator[]
- swap
- front 和 back
一、接口总览
namespace ky
{
template<typename T>
class vector
{
public:
//迭代器
typedef T* iterator;
typedef const T* const_iterator;
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//默认成员函数
vector();//默认构造
vector(size_t n,const T& value =T());//n个val构造
//迭代器区间构造
template<typename inputIterator>
vector(inputIterator first,inputIterator last);
ector(const vector<T>& v);//拷贝构造
vector<T>& operator=(vector<T> v);//赋值重载
~vector();//析构
//容量相关
size_t size() const;
size_t capacity() const;
bool empty() const;
void reserve(size_t n);
void resize(size_t n,const T& value= T());
//增删查改
void push_back(const T& x);//尾插
void pop_back();//尾删
iterator insert(iterator pos, const T& x);//插入
iterator erase(iterator pos);//删除
void swap(vector<T>& v);//交换函数
T& operator[](size_t pos);
const T& operator[] (size_t pos) const;
T& front();
T& back();
private:
iterator _start; //指向数据块开始
iterator _finish; //指向有效数据块末尾
iterator _end_of_storage; //指向存储容量的尾
};
}
二、vector成员变量
- _start指向容器的头部
- _finish指向有效数据的尾部(下一个即将放入数据的地方)
- _end_of_storage指向整个容器的尾(容量的末尾)
三、默认成员函数
构造函数① – 默认无参构造
首先要有一个无参的默认构造,只需要把三个成员变量初始化为空指针即可
//直接初始化列表初始化
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
构造函数② – 迭代器区间构造
迭代器区间构造:利用某一段迭代器区间构造初始化vector。
注意:需要设置为模板函数,因为该迭代器区间可以是任意其他容器的迭代器区间。然后只需要把迭代器区间的值依次push_back到vector即可
template<typename inputIterator>
vector(inputIterator first,inputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first); //尾插
++first;
}
}
构造函数③ – n个val构造
有时候想要用n个相同的值 val来构造一个vector,所以vector提供一个这样的构造函数。
先开辟n个空间,然后依次push_back来尾插n个val即可
vector(size_t n,const T& value =T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
//开空间
reserve(n);
//开好空间后直接尾插n次
for (size_t i = 0;i<n ; ++i)
{
push_back(value);
}
}
注意
n个val构造必须加一个重载函数!
//n为int的重载函数
vector(int n, const T& value = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//开空间
reserve(n);
//开好空间后直接尾插n次
for (int i = 0; i < n; ++i)
{
push_back(value);
}
}
为什么呢?
因为对于vector<int> v(10,1)
这个代码来说,我们想用10个1构造一个vector,但是因为10和1显然会被编译器识别为同一种类型,他认为(10,1)更匹配的是一个迭代器区间,而n个val的构造函数中,n是size_t类型,编译器会选择参数最匹配的函数进行调用,因此实际上这个代码片段调用的是构造函数②,就会发生 非法的间接寻址错误
拷贝构造函数
拷贝构造就要涉及到深拷贝问题了,因为vector中的元素类型,既可能是普通的内置类型,也有可能是自定义类型,对于自定义类型,如果只是浅拷贝的话,那么可能在析构的时候发生同一块空间析构两次的情况从而导致崩溃。
写法1:传统写法
传统写法就是,自己去开空间,然后把要复制的对象中的元素一个一个赋值拷贝过来
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
_start = new T[v.size()]; //开_capacity个大小也可以
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size(); //更新结尾和容量
_end_of_storage = _start + v.size();
}
注意:在拷贝的时候不可以使用memcpy!因为memcpy是一种浅拷贝,当vector中的元素是int等内置类型不会发生错误,但是当vector中的元素是需要进行深拷贝类型的时候,使用memcpy就是存在问题的。例如:对<vector<vector<int>>
进行拷贝
比如:vector中的元素 都是一个vector<int>
类型,每一个vector<int>
中都指向一块连续的空间
如果使用memcpy对vector进行拷贝,那么所拷贝出来的vector的空间是新开的空间,但是所拷贝的vector中每一个vector的内容都是和原来一摸一样的,也就是说,指针都完全一样,指向同一块连续空间。那么势必会导致析构两次而崩溃的问题,如下图所示:
正确的写法就是,对于新开辟的空间中的每一个元素,使用 = 运算符依次拷贝过来,因为赋值重载实现的是深拷贝
正确的拷贝结果应该是这样:
所以,对于memcpy千万要慎用!对于内置类型或者不需要开辟动态数组的一些类型,可以使用,但是如果像string,vector等类型,就不能使用memcpy!
写法2
直接先开辟好空间,然后依次把要拷贝的尾插进来即可
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(v.size());
for (const auto& e : v)
{
push_back(e);
}
}
写法3:工具人写法
创建一个临时对象,用迭代器区间构造要拷贝的对象,然后交换一下内部的三个指针即可!同时交换后,临时变量会自动在函数退出的时候销毁。
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
vector<T> tmp(v.begin(), v.end()); //迭代器区间构造
swap(tmp); //然后让此对象和tmp交换
}
赋值运算符重载
赋值运算符和拷贝构造类似,也涉及到深拷贝问题
传统写法
- 首先判断是不是自己给自己赋值,如果是就不用操作
- 如果不是,首先释放旧的空间
- 开辟新空间
- 利用赋值运算符 一个个把数据拷贝过来(不能使用memcpy!)
- 更新_finish和 _end_of_storage的值
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
//1.删除
//2.开空间
//3.拷贝
delete[] _start;
_start = new T[v.size()];//开空间
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();//更新_finish和 _end_of_storage
_end_of_storage = _start + v.size();
}
return *this;
}
现代写法
和拷贝构造的第三种方法类似,形参直接不用引用,把形参作为临时对象,在传参的时候就把要拷贝的对象拷贝构造给形参了,然后让本对象和临时对象交换,最后返回*this即可
//v(v1) 传参的时候 v1就拷贝构造给了tmp
vector<T>& operator=(vector<T> tmp)//值形参作为tmp
{
swap(tmp);//传参的时候,拷贝构造给了形参v
return *this;
}
析构函数
析构的时候,直接就释放_start所指向的连续空间
然后把三个成员变量赋值为nullptr即可
~vector()
{
delete[] _start; //都是指向的同一块内存,只释放开头即可
_start = _finish = _end_of_storage = nullptr;
}
四、迭代器
vector中,底层的实现也是数组,是连续的,所以迭代器就是原生指针
迭代器的声明
typedef T* iterator;
typedef const T* const_iterator;
begin和end
iterator begin()
{
return _start;//返回容器的第一个元素的迭代器
}
iterator end()
{
return _finish;//返回最后一个元素的下一个位置的迭代器
}
//下面是针对const容器来调用的,返回之后只能读不能修改!
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
此时再回看迭代器:实际上就是利用指针访问数组
int arr[] = {1,3,5,7,9};
vector<int> v(arr,arr+sizeof(arr)/sizeof(int));
vector<int>::iterator it = v.begin();
while(it!=v.end())
{
cout << *it << " ";
}
cout << endl;
支持了迭代器,自动就支持了范围for,因为范围for就是傻瓜式的替换,底层就直接把上面的迭代器替换成了下面的范围for,如果你把begin函数改成Begin都会发生报错!(真·傻瓜式替换)
int arr[] = {1,3,5,7,9};
vector<int> v(arr,arr+sizeof(arr)/sizeof(int));
for(const auto& i : v)
{
cout << i <<" ";
}
cout << endl;
六、容量以及元素访问的相关接口
empty
判断是否为空,显然当_start 和 _finish相等的时候vector为空
bool empty() const
{
return _start == _finish;
}
size和capacity
size就是有效数据的个数,等于 _finish - _start(指针-指针得到中间元素的个数)
capacity就是总的容器容量,等于 _end_of_storage - _start
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
reserve
reserve(n)
- 当n大于当前的capacity的时候,将capacity扩容到n
- 当n<=capacity的时候,什么都不做
实现:首先判断n是否大于capacity,如果大于,就开辟大小为n的空间,然后,把原来的数据拷贝到新开辟的空间,最后释放旧空间即可。
void reserve(size_t n)
{
//如果大于当前容量,才会扩容
if (n>capacity())
{
T* tmp = new T[n]; //create new
size_t len = size(); //记录原长度
//如果原空间不为空
if (_start)
{
//拷贝 不可以用memecpy!! (因为涉及到一些更深层次的拷贝)
for (size_t i = 0; i < len; ++i)
{
tmp[i] = _start[i];
}
delete[] _start; //free old
}
//更新成员变量
_start = tmp; //new head
_finish = _start + len; //new finish
_end_of_storage = _start + n; //new end_of_storage
}
}
注意:
1、需要先记录原来的长度,因为在扩容之后,原来的空间被释放了,新的 _start变成了新开辟的空间的首地址,而 _finish = _start+size
, _end_of_storage = _start + capacity
所以需要提前记住原来的size才可以进行更新。否则如果在 _finish没有更新的情况下 利用size()函数通过 _finish-_start
的方式计算有效数据个数,得到的就是一个随机值!
2、 拷贝的时候,需要把原空间的每一个元素利用 赋值 =
来拷贝每一个元素,必须一个一个拷贝,不可以用memcpy。(如果使用memcpy进行拷贝,那么就是浅拷贝,如果是深拷贝类型,那么就会出现两个指针指向同一块空间的问题),而在reserve的时候,是需要及时释放掉旧空间的,这样旧空间释放了,而拷贝出来的每一个元素中的指针也都指向了一块被释放的空间,再进行访问就是非法野指针访问了!
正确的拷贝应该是这样的:
resize
resize(size_t n,const T& val = T())
注意,C++中对内置类型进行了升级,内置类型也有了默认构造,如int()为0,double为 0.0
1、如果n>当前的size,就用val把size扩大到n。如果val没有给,那么默认用vector存储的元素类型的默认构造进行初始化,如:int就用0,string就用空字符。如果val给了,就用所给的val扩大。
2、 如果n == 当前size,什么也不做
3、 如果n < 当前size,就删除元素,使得size变成n
resize的时候,我们还需要进行判断n是否大于当前的capacity。如果大于当前的capacity,优先进行扩容!
void resize(size_t n,const T& value= T())
{
//1.n >= capacity
//2. size <= n < capacity
//3. n < size
if (n > capacity()) //扩容+初始化
{
reserve(n);
}
if (n > size()) //初始化
{
while (_finish < _start + n)
{
*_finish = value;
++_finish;
}
}
else //删除元素
{
_finish = _start + n; //n作为size()
}
}
七、增删查改等接口
push_back
既然要插入数据,首先判断是否需要扩容。然后把数据插入到 _finish位置,++ _finish即可
void push_back(const T& x)
{
//check capacity
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x; //添加数据
++_finish;
}
pop_back
删除数据,首先检查是否为空,如果为空就不需要删除了
如果不为空,直接 – _finish即可!
void pop_back()
{
assert(_finish > _start);
--_finish;
}
insert
insert(pos,val):在所给迭代器位置插入一个元素val
1、首先检查是否需要扩容,和pos的合法性。
2、要提前把size记录下来,因为如果需要发生扩容,pos这个迭代器就被释放了,就找不到该位置了!
3、然后把依次把包括pos往后的元素都向后挪动一格,给pos位置腾出位置,插入val即可
注意:还要有返回值,返回新插入元素位置的迭代器
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
size_t len = pos - _start;
//检查扩容
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
//如果扩容,可能导致原pos指向的位置已经被释放了,所以需要重新计算pos
pos = _start + len; //pos等于新的_start+len
}
//挪动数据
iterator end = _finish;
while (end > pos)
{
*end = *(end - 1);
--end;
}
//插入数据
*pos = x;
++_finish; //末尾+1
return pos;
}
erase
erase(pos):删除迭代器pos位置的元素
1、首先检查pos的合法性
2、然后依次把pos位置后面的元素向前挪动一格,进行覆盖删除
注意:返回删除位置的下一个位置的迭代器(还是pos)
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
//挪动数据
iterator begin = pos+1;
while (begin < _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos; //STL中就是返回删除元素的后面一个元素
}
operator[]
operator[]保证了vector可以像数组一样访问,其实就是指针的解引用
T& operator[](size_t pos)
{
assert(pos < size());
return *(_start + pos);
}
//const对象用的:
const T& operator[] (size_t pos) const
{
assert(pos < size());
return *(_start + pos);
}
swap
交换函数:交换vector的每一个成员变量
void swap(vector<T>& v)
{
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_end_of_storage, v._end_of_storage);
}
::swap指的是在全局范围内找swap,其实就是调用了std中的进行内置类型变量的交换,因为我们包了头文件,已经展开了~,所以不用写std::,直接写 :: 即可
front 和 back
front返回第一个元素,back返回最后一个元素
T& front()
{
return *_start;
}
T& back()
{
return *(_finish - 1);
}
注意:返回引用,这样函数返回值可以直接进行修改~
vector<int> v(5,3);
v.front()*=10;
for(auto i : v)
cout << i <<" ";
cout << endl;
//输出:30 3 3 3 3