目录
1.构造模块
(1)无参构造
(2)半缺省构造
(3)区间构造的必备知识
(4)区间构造
(5)拷贝构造
(6)赋值运算符重载
(7)析构函数
2.迭代器模块
(1)正向迭代器
(2)反向迭代器
3.容量模块
(1)获取有效元素个数
(2)获取容量大小
(3)判空函数
(4)扩容函数
(5)设置有效元素个数
4.元素访问模块
(1)获取首元素
(2)获取尾元素
(3)重载[]
5.修改模块
(1)尾插
(2)尾删
(3)任意位置插入
(4)指定位置删除
(5)清空有效元素
6.总体代码
vector是这是C++中的典型容器,它是动态类型的顺序表,任何类型的元素都可以存放。接下来将在以下几个方面进行模拟实现:构造模块、迭代器模块、容量模块、元素访问模块、修改模块。
因为类中的函数非常多,所以只着重实现一些常用的函数。(本文代码均在win10系统下的vs2019中验证)
在构造模块,先来思考一下该设置些什么成员变量?首先要有一个 _start 指针指向一块空间,其次一个 _finish 指针指向这块空间中最有一个有效元素的下一个位置,_end指针指向空间的最后一个位置的下一个位置。而空间中的有效元素个数和容量可以通过指针相减得到。
用下面这幅图理解一下三个指针的位置:总空间有8个位置,前五个位置有元素,_finish指向第六个位置,_end需要指向第八个位置后面的空间。当有效元素把位置占满的时候,_finish和_end自然就指向同一块空间。
由于vector类是可以存放任意类型的元素的,要模拟实现它自然也要做到存放任意类型。那么就需要用到模板了。T是存放的元素类型,T*是指向T类型元素的指针。
代码一:注意创建对象时必须声明存储什么类型的对象。语法:Vector
既然是空参构造,那么说明现在并不给对象中存放元素,那直接把三个指针都置空就好。
//代码一
#include "iostream"
using namespace std;
//Vector类存储的数据是任意类型,所以需要设置模板参数
template
class Vector {
public:
T* _start;//指向T类型元素的首元素的指针
T* _finish;//指向最后一个有效元素下一个位置
T* _end;//指向顺序表空间最后一个位置的下一个位置。
public:
Vector()
:_start(nullptr)
,_finish(nullptr)
,_end(nullptr)
{}
};
int main() {
Vectorv;
}
代码二:这里n的类型一定要给成int,不可以给成size_t。不然运行时会遇到错误,在区间构造时就知道了。
//代码二
Vector(int n, const T& val = T())
:_start(new T[n])//在初始化时直接从堆上申请空间
{
for (int i = 0; i < n; i++) {
_start[i] = val;
}
//_finish指向最后一个有效元素的下一个位置
_finish = _start + n;
//_end指向顺序表最后一个位置的下一个位置
_end = _finish;
}
vector类的区间构造很强大,不仅是在使用方法中介绍的利用数组来进行区间构造,也可以用链表来进行区间构造,所以实现时要格外注意不可以直接拷贝,需要将区间内元素遍历一遍才可以。
在区间构造时候因为可以用链表来构造,所以传入的迭代器自然也是链表的迭代器,这样一来,T类型的迭代器就不能满足条件了,所以我们也要为迭代器设置模板参数,让任意类型的迭代器都可以进行区间构造。
代码三:这里搭配测试用例解释一下第二个循环。
看一下区间构造,还记得上面为什么需要把参数类型给成int吗?如果你想构造对象时初始化10个5,是不是这样写 Vector
模板不会进行类型转换,根据你的参数推演,如果能找到匹配的就使用。检测到size_t发现不匹配就继续找,找到区间构造后发现可以使用,因为它可以把 Iterator 当成int类型。
可是原本的迭代器是需要解引用的,现在对整形数字解引用,可不就出错了么?
//代码三
template
Vector(Iterator first, Iterator last) {
Iterator it = first;
size_t num = 0;//统计元素个数
while (it != last) {
num++;
it++;
}
_start = new T[num];
_finish = _start;
while (first != last) {
*_finish = *first;
_finish++;
first++;
}
_end = _finish;
}
int main() {
int arr[] = { 1,2,3,4,5,6 };
Vectorv(arr,arr+sizeof(arr)/sizeof(arr[0]));
}
搭配图片理解一下第二个循环:
arr指向的是首元素,sizeof(arr)/sizeof(arr[0]) 是求数组元素个数,共6个。arr+6就指向了数组外的空间。当first移动到last位置说明元素遍历完毕,此时对应的_finish也指向了最后一个有效元素的后一个位置。(同时这也是顺序表的最后一个位置的后一个位置了,因为申请时只申请了这么多空间)
重点来了:注意first迭代器也是可以++的,这是因为对应的类中现在基本都会重载++,这样链表也就不需要用指针指向next这样的操作来访问了,会方便很多,也更直观。
代码四:
//代码四
Vector(const Vector& v)
:_start(new T[v.Size()]) //这里用到的Size函数后面会实现的
{
size_t num = v.Size();
for (size_t i = 0; i < num; i++) {
_start[i] = v._start[i];
}
//让对应的指针指向应该指向的位置
_finish = _start + num;
_end = _finish;
}
代码五:因为这里传递的是值,会发生值拷贝,产生临时对象,那么我们把两个对象的指针指向的空间交换即可。一定要返回,因为要实现连续赋值。
//代码五:
Vector& operator=(const Vector v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end, v._end);
return *this;
}
代码六:这里检查指针是否为空,虽然经过测试指针指向空值时,delete[] 指针不会报错,但出于谨慎,还是加上检查比较好。
//代码六
~Vector() {
if (_start) {
delete[] _start;
}
_start = _finish = _end = nullptr;
}
迭代器模块的模板参数要单独给出,不可以使用区间构造那块的,因为迭代器不涉及多种元素,它只返回本类的元素迭代器,只有一种。
代码七:
//代码七
typedef T* iterators;
iterators Begin() {
return _start;
}
iterators End() {
return _finish;
}
代码八:反向迭代器可以直接复用正向迭代器。
//代码八
typedef T* iterators;
iterators Rbegin() {
return End();
}
iterators Rend() {
return Begin();
}
这里就可以体会到指针的便捷了,直接相减就得到了元素个数。
代码九:
//代码九
size_t Size() const{
return _finish - _start;
}
代码十:道理同上。
//代码十
size_t Capacity()const {
return _end - _start;
}
代码十一: 这两个指针相同说明并没有元素的存在。
//代码十一
bool Empty()const {
return _start == _finish;
}
代码十二:只考虑扩大,不考虑缩小。
//代码十三
void Reserve(size_t newCapacity) {
size_t oldCapacity = Capacity();
size_t oldSize = Size();
T* temp = new T[newCapacity];
//当要设置的新空间大于旧空间
if (newCapacity > oldCapacity) {
//判断首指针是否是空
if (_start) {
//进行深拷贝
//思考:memcpy可以使用吗?
for (size_t i = 0; i < oldSize; i++) {
temp[i] = _start[i];
}
//释放旧空间
delete[] _start;
}
_start = temp;
_finish = _start + oldSize;
_end = _start + newCapacity;
}
}
在扩容函数中思考一个问题,为什么没有使用memcpy函数?原因是memcpy是按字节拷贝,会有程序崩溃的风险。使用下面的例子来验证。
代码十三:举例,假设使用扩容函数中使用memcpy函数,并且vector中存储S类型的对象。这里因为是举例子,所以代码给的很粗糙,理解一下意思就好。
首先使用s1和s2构造v对象,然后进行尾插,这样会导致扩容。
//代码十三
class S {
public:
char* str;
};
int main(){
S s1;
S s2;
vector v{s1,s2};
S s3;
v.push_back(s3);
}
下面这幅图就是验证图。
S对象内存储的是指针,构造Vector时里面有两个S类型的元素,两个s元素中分别有一个指针指向一个有"\0"元素的空间。然后尾插一个元素,导致扩容。
扩容后使用memcpy按字节将vector中的元素拷贝到新空间中。一切看起来都那么完美。可是旧的vector对象中地址指针(蓝色的地址线)需要释放,释放后新空间中的指针(红色地址线)却依然指向被释放的空间。这不就是野指针吗?这样一来,一旦试图访问这块空间,必然程序崩溃。
这里要告诉大家,不要使用memset函数。因为memset函数也是按字节将元素直接放到指定空间中。C 库函数 void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
字符的字节是1字节,如果你要用它放n个T类型数据到指向的空间,它是怎么做的?它会把T类型的数据往空间中的前n个字节,每个字节都放一个!
代码十四:
//代码十四:
void Resize(size_t newSize, const T& val = T()) {
size_t oldCapacity = Capacity();
size_t oldSize = Size();
//要设置的有效元素个数大于现在的有效元素
if (newSize > oldSize) {
//要设置的有效元素个数大于现有空间,扩容
if (newSize > oldCapacity) {
Reserve(newSize);
}
//思考:memeset可以用吗?
for (size_t i = oldSize; i < newSize; i++) {
_start[i] = val;
}
}
_finish = _start + newSize;
}
代码十五:这里给出两种类型,以便于const对象使用。
//代码十五
T& Front() {
return _start[0];
}
const T& Front()const {
return _start[0];
}
代码十六:两种类型,道理同上。
//代码十六
T& Back() {
return *(_finish - 1);
}
const T& Back()const {
return *(_finish - 1);
}
代码十七:也是两种类型。注意assert函数使用时需要包含头文件。
//代码十七
#include
T& operator[](size_t index) {
//需要判断是否越界
assert(-1
代码十八:尾插可能需要扩容,一旦扩容,就给的空间大一点,尽量减少扩容次数。
//代码十八
void Push_back(const T& val) {
//判断顺序表是否已经存满了
if (_finish == _end) {
Reserve(Capacity() * 2);
}
*_finish = val;
_finish++;
}
代码十九:
//代码十九
void Pop_back() {
if (Empty()) {
return;
}
_finish--;
}
代码二十: 需要考虑越界问题
//代码二十
iterators Insert(iterators index, const T& val) {
//判断迭代器是否越界
if (index < _start || index > _finish)
return End();
//判断空间是否存满
if (_start == _finish)
Reserve(Capacity() * 2);
auto it = _finish;
while (it != index) {
*it = *(it - 1);
it--;
}
*index = val;
_finish++;
return index;
}
代码二十一:需要考虑越界问题
//代码二十一
iterators Erase(iterators index) {
//判断迭代器是否越界
if (index < _start || index > _finish)
return End();
auto it = index;
//将index后的元素往前搬移一位
while (it != _finish) {
*it = *(it + 1);
it++;
}
_finish--;
return index;
}
代码二十二:
//代码二十二
void Clear() {
_finish = _start;
}
将代码进行调整后改成整体代码如下:
#include
#include "iostream"
using namespace std;
//Vector类存储的数据是任意类型,所以需要设置模板参数
template
class Vector {
private:
T* _start;//指向T类型元素的首元素的指针
T* _finish;//指向最后一个有效元素下一个位置
T* _end;//指向有效空间的下一个位置。
public:
//构造模块
//
//无参构造
Vector()
:_start(nullptr)
,_finish(nullptr)
,_end(nullptr)
{}
//半缺省构造
Vector(int n, const T& val = T())
:_start(new T[n])//在初始化时直接从堆上申请空间
{
for (int i = 0; i < n; i++) {
_start[i] = val;
}
//_finish指向最后一个有效元素的下一个位置
_finish = _start + n;
//_end指向顺序表最后一个位置的下一个位置
_end = _finish;
}
//区间构造
template
Vector(Iterator first, Iterator last) {
Iterator it = first;
size_t num = 0;//统计元素个数
while (it != last) {
num++;
it++;
}
_start = new T[num];
_finish = _start;
while (first != last) {
*_finish = *first;
_finish++;
first++;
}
_end = _finish;
}
//拷贝构造函数
Vector(const Vector& v)
:_start(new T[v.Size()]) //这里用到的Size函数后面会实现的
{
size_t num = v.Size();
for (size_t i = 0; i < num; i++) {
_start[i] = v._start[i];
}
//让对应的指针指向应该指向的位置
_finish = _start + num;
_end = _finish;
}
//赋值运算符重载
Vector& operator=(const Vector v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end, v._end);
return *this;
}
//析构函数
~Vector() {
if (_start) {
delete[] _start;
}
_start = _finish = _end = nullptr;
}
//迭代器模块
//
typedef T* iterators;
//正向迭代器
iterators Begin() {
return _start;
}
iterators End() {
return _finish;
}
//反向迭代器
iterators Rbegin() {
return End();
}
iterators Rend() {
return Begin();
}
//容量模块
//
//有效元素个数
size_t Size() const{
return _finish - _start;
}
//容量大小
size_t Capacity()const {
return _end - _start;
}
//判空函数
bool Empty()const {
return _start == _finish;
}
//扩容函数
void Reserve(size_t newCapacity) {
size_t oldCapacity = Capacity();
size_t oldSize = Size();
T* temp = new T[newCapacity];
//当要设置的新空间大于旧空间
if (newCapacity > oldCapacity) {
//判断首指针是否是空
if (_start) {
//进行深拷贝
//思考:memcpy可以使用吗?
for (size_t i = 0; i < oldSize; i++) {
temp[i] = _start[i];
}
//释放旧空间
delete[] _start;
}
_start = temp;
_finish = _start + oldSize;
_end = _start + newCapacity;
}
}
//设置有效元素函数
void Resize(size_t newSize, const T& val = T()) {
size_t oldCapacity = Capacity();
size_t oldSize = Size();
//要设置的有效元素个数大于现在的有效元素
if (newSize > oldSize) {
//要设置的有效元素个数大于现有空间,扩容
if (newSize > oldCapacity) {
Reserve(newSize);
}
//思考:memeset可以用吗?
for (size_t i = oldSize; i < newSize; i++) {
_start[i] = val;
}
}
_finish = _start + newSize;
}
//元素访问模块
//
//获取首元素
T& Front() {
return _start[0];
}
const T& Front()const {
return _start[0];
}
//获取尾部元素
T& Back() {
return *(_finish - 1);
}
const T& Back()const {
return *(_finish - 1);
}
//重载[]
T& operator[](size_t index) {
//需要判断是否越界
assert(-1 _finish)
return End();
//判断空间是否存满
if (_start == _finish)
Reserve(Capacity() * 2);
auto it = _finish;
while (it != index) {
*it = *(it - 1);
it--;
}
*index = val;
_finish++;
return index;
}
//任意位置删除
iterators Erase(iterators index) {
//判断迭代器是否越界
if (index < _start || index > _finish)
return End();
auto it = index;
//将index后的元素往前搬移一位
while (it != _finish) {
*it = *(it + 1);
it++;
}
_finish--;
return index;
}
//清空有效元素
void Clear() {
_finish = _start;
}
};
int main() {
Vector v(10, 5);
v.Push_back(9);
v.Erase(v.End());
}