【C++初阶】vector容器的模拟实现,各接口讲解
1.vector的介绍
使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习,以下是vector的官方文档
2.接口模拟实现总览
为了避免命名冲突,我们还是将实现的vector封装到单独的命名空间里
#pragma once
#include<iostream>
#include<cassert>
#include<string>
namespace gjy
{template<class T>//这是一个类模板类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。class vector{public://—————————迭代器—————————/*using iterator = T*;using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容typedef T* iterator;// 指向数据不能修改,本身可以修改typedef const T* const_iterator;iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}
//——————————1、默认成员函数——————————//1.1.构造函数//无参构造函数,使用缺省值来初始化列表vector(){}//迭代器区间构造函数template<class Inputerator>//模板函数,这里的class不是类的意义,也可以用typenamevector(Inputerator first, Inputerator last)//使用缺省值初始化列表{while (first != last){push_back(*first);first++;}}//有参构造(n个值为val的容器)vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。{reserve(n);//调用reserve函数将容器容量设置为nfor (size_t i = 0;i < n;i++){push_back(val);//利用尾插函数插入数据}}//下面两个构造是跟微信那个问题有关vector(long n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}}vector(int n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}}//拷贝构造//传统写法//vector(const vector<T>& v)//后面不显式写初始化列表,编译器会用缺省值写//{// _start = new T[v.capacity()];//开辟一块和容器V一样大小的空间// for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数// {// _start[i] = v[i];// }// _finish = _start + v.size();//更新有效数据的尾// _end_of_storage = _start + v.capacity();//}//现代写法:拷贝并交换(Copy-and-Swap)vector(const vector<T>& v){// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)vector<T> tmp(v.begin(), v.end()); // 复用已有构造函数,无需重复写拷贝逻辑// 2. 交换当前对象和临时对象的资源swap(tmp); // 需实现swap成员函数}/*vector(const vector<T>& v){reserve(v.capacity());for (auto e : v){push_back(e);}}*/~vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}//赋值运算符重载//传统写法//vector<T>& operator=(const vector<T>& v)//{// if (this != &v)//防止自赋值// {// delete[]_start;//释放原空间// _start = _finish = _end_of_storage = nullptr;// reserve(v.size());//复用reserve开和v一样的空间// for (auto& e : v)// {// push_back(e);// }// }// return *this;//}//现代写法vector<T>& operator=(vector<T> v){if (this !=& v){swap(v);}return *this;}//—————————2.访问操作—————————T& operator[](size_t n){assert(n < size());return *(_start + n);}const T& operator[](size_t n) const{assert(n < size());return *(_start + n);}//—————————3.容量操作—————————size_t size()const{return _finish - _start;}size_t capacity()const{return _end_of_storage - _start;}void reserve(size_t n){if (capacity() < n)//判断是否需要扩容,reserve不对内容修改{size_t Old_size = size(); //记录当前容器当中有效数据的个数T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间if (_start) //判断是否为空容器{for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中{tmp[i] = _start[i];}delete[] _start; //将容器本身存储数据的空间释放}_start = tmp; //将tmp所维护的数据交给_start进行维护_finish = _start + Old_size; //容器有效数据的尾_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;}}}bool empty(){return(_start == _finish);//或者return!capacity()}//—————————4.修改操作—————————// 实现swap成员函数(交换两个vector的资源)void swap(vector<T>& other){// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);}void push_back(const T& val){if (_finish == _end_of_storage){size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacityreserve(newcapcity);//开辟新空间大小}*_finish = val;_finish++;}void pop_back(){assert(capacity());_finish--;}//insert迭代器失效问题void insert(iterator pos,const T& val){assert(pos >= _start && pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;//避免后面的迭代器失效reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了pos = _start + len;//这里是更新后的start}iterator it = _finish - 1;//最后一个数据位置while (it >= pos){*(it + 1) = *it;//往后挪it--;}*pos = val;++_finish;}iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置){assert(pos >= _start);assert(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;};}
类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。vector<int>就是存放int类型的vector容器,甚至还可以vector<vector<int>>创建一个二维数组,并且相对于C语言的二维数据,封装了各种接口可以使用
3、vector的成员变量和默认成员函数
3.1.成员变量介绍
我们先来讲讲vector的成员变量有哪些:_start、_finish、_end_of_storage
vector容器其实就是我们数据结构中学过的顺序表结构,实现这一结构的成员变量如下图

_start指向第一个元素,_finish指向结束的下一个位置,_end_of_storage指向整个容器的结尾。
3.2.构造函数
构造函数为什么还要写一个无参数的,跟后面现代写法swap函数传形式参数那里有关,一会再说有关是因为只要有构造函数,
1.无参构造函数
//无参构造函数
//没有显式写初始化列表就用缺省值
vector()
{
}
C++ 标准规定:如果用户显式定义了任何构造函数(如拷贝构造、赋值运算符等),编译器将不再自动生成默认构造函数(无参构造函数)。
如果不手动定义默认构造函数 vector() {},编译器不会生成默认构造函数。此时,当需要创建无参的 vector 对象时(如 bit::vector<int> v;),会因找不到默认构造函数而编译报错。
确保成员变量的缺省值正常生效
为成员变量设置了缺省值:
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;
这些缺省值需要在对象初始化时生效。对于默认构造函数:
- 如果显式定义了
vector() {},编译器会在该构造函数的初始化阶段,使用成员变量的缺省值对_start、_finish、_end_of_storage进行初始化(本质是编译器为默认构造函数隐式补充了初始化列表)。 - 如果没有显式定义默认构造函数,且编译器也没有生成(因存在其他构造函数),则无法通过无参方式创建对象,成员变量的缺省值也就无从谈起。
2.有参构造函数(利用迭代器)
/迭代器区间构造函数template<class Inputerator>//模板函数,这里的class不是类的意义,也可以用typenamevector(Inputerator first, Inputerator last)//使用缺省值初始化列表{while (first != last){push_back(*first);first++;}}
这里的迭代器使用了模板参数,各种容器的迭代器都可以传参,适用于不同类型的迭代器
3.有参构造函数(n个val值的)
vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。
{reserve(n);//调用reserve函数将容器容量设置为nfor (size_t i = 0;i < n;i++){push_back(val);//利用尾插函数插入数据}
}
该构造函数用于创建一个包含 n 个相同元素 val 的 vector。例如:vector<int> (5, 10):创建一个存储 int 的 vector,包含 5 个值为 10 的元素;vector<string> (3, "hello"):创建一个存储 string 的 vector,包含 3 个值为 "hello" 的元素。
注意:
- 该构造函数明确知道需要存储n个数据,因此建议使用reserve函数一次性分配足够的空间。这样可以避免后续调用push_back时多次扩容,从而提高效率。
- 该构造函数还需要实现两个重载版本,如下:
vector(long n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);} } vector(int n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {reserve(n); //调用reserve函数将容器容量设置为nfor (int i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);} }这两个重载函数的区别在于参数n类型不同,如果不这样写的话,当我们传参如下
vector<int>(10,1)//本意是调用第三个构造函数的会发生如下图一样的问题,我们的编译器会默认选择更合适的函数,因为两个int,就会调用我们使用迭代器的那个构造函数,而int类型不能解引用,因此会报错

-
C++重载决议优先选择"最匹配"的函数,不需要类型转换的版本优先于需要类型转换的版本
-
模板函数能精确匹配时,优先于需要隐式转换的非模板函数
解决方法
1.提供合适的重载类型就是再写一个int类型的,以及long类型的构造函数
2.使用强制类型转换传参,调用时可以使用vector<int>{10u,7},强制转换第一个参数为unsigned。
3.3.拷贝构造
动态开辟了资源因此也需要深拷贝,关于vector的深拷贝构造函数,提供两种实现方式:
1.传统写法:
先开辟与原容器一样大的空间,再逐个拷贝原容器中的数据,最后更新_finish和_end_of_storage指针即可
//传统写法
vector(const vector<T>& v)//后面不显式写初始化列表,编译器会用缺省值写
{_start = new T[v.capacity()];//开辟一块和容器V一样大小的空间for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数{_start[i] = v[i];}_finish = _start + v.size();//更新有效数据的尾_end_of_storage = _start + v.capacity();
}
注意事项:
当在逐个拷贝容器时候应该尽量避免使用memcpy函数,当vector存储的是内置类型或者无需深拷贝的自定义类型时候,还能使用,当需要重新分配资源时就不行,这个在后面reserve那里迭代器失效也有关系。如果拷贝的容器里面存放的是string类型

每一个string都指向自己所存储的字符串

若使用memcpy函数进行拷贝构造,新构造的vector中每个string对象的成员变量将和vector中的string对象完全相同,这意味着两个vector中对应的string成员会指向相同的字符串存储空间。

那我们的代码是如何解决的呢?
for (size_t i = 0; i < v.size(); i++)
{start[i] = v[i];
}
这里的赋值实际上调用的是赋值运算符重载,这里面会重新分配资源
实际流程是当拷贝 vector<string> 时:
- 先为
vector开辟新的内存空间(new T[v.capacity()],这里T是string)。 - 对每个
string元素,通过string的拷贝赋值运算符完成深拷贝。 - 最终得到的新
vector<string>与原vector存储的string元素完全独立,修改其中一个的元素不会影响另一个。
简言之,拷贝 vector<string> 时,会间接调用 string 类的拷贝赋值运算符重载,保证字符串内容的深拷贝。

2.现代写法:
// 现代写法:拷贝并交换(Copy-and-Swap)
vector(const vector<T>& v){// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)vector<T> tmp(v.begin(), v.end()); // 复用已有构造函数,无需重复写拷贝逻辑// 2. 交换当前对象和临时对象的资源swap(tmp); // 需实现swap成员函数
}
// 实现swap成员函数(交换两个vector的资源)void swap(vector<T>& other){// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);}
利用构造函数构造一个临时对象,这个临时对象和要拷贝的对象拥有一样大的空间和数据,我们只需要交换this和这个临时对象的值和空间,出了作用域临时对象销毁顺带释放了this原来的空间。我们调试会发现传参那一步跳转到了拷贝构造函数,然后就出现了错误
注意:

出现错误的原因是因为我们传值传参调用拷贝构造,拷贝构造函数中实参v3传递给形参v,将v,这里reserve是this调用的,因为要拷贝v3的那个对象没有初始化,没有构造那个对象,因此reserve,push_back这些接口使用就会报错,所以需要我们要么初始化列表中显示初始化,要么就给成员变量缺省值
vector(const vector<T>& v):_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{//...
}
3.写法三.
使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。
vector(const vector<T>& v)
{reserve(v.capacity());for (auto e : v){push_back(e);}
}
4.析构函数
~vector()
{delete[] _start;_start = _finish = _end_of_storage = nullptr;
}
析构函数直接删除数据即可,如果vector存放的是自定义类型会调用其内部的析构函数释放资源
5.赋值运算符重载
vector的赋值运算符重载当然也涉及深拷贝问题,我们这里也提供两种深拷贝的写法:
1.传统写法
vector<T>& operator=(const vector<T>& v)
{if (this != &v)//防止自赋值{delete[]_start;//释放原空间_start = _finish = _end_of_storage = nullptr;reserve(v.size());//复用reserve开和v一样的空间for (auto& e : v){push_back(e);}}return *this;
}
复用reserve和尾插,将右值传给左值。
2.现代写法
还是利用swap函数,传值传参调用拷贝构造生成临时对象,交换this和临时对象两个对象的值,出了作用域利用临时对象的销毁释放原空间,就不需要在手动delete[]了.
vector<T>& operator=(const vector<T> v)
{swap(v);return *this;}
4、迭代器函数和访问元素操作
4.1.begin()和end()
依然分为const迭代器和普通迭代器,const迭代器不能直接在普通迭代器前面加const,那是是迭代器不能修改,而不是指向内容不能修改,应该在后面加const代表返回值是const类型
/*using iterator = T*;
using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?
//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容
typedef T* iterator;
// 指向数据不能修改,本身可以修改
typedef const T* const_iterator;iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}
我们实现了迭代器,编译器自己就也实现范围for,可以用范围for对容器进行遍历
4.2.operator[]
因为底层数据结构是数组,所以vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。我们需要重载两个形式,普通的和const类型的函数。
T& operator[](size_t n)
{assert(n < size());return *(_start + n);
}
const T& operator[](size_t n) const
{assert(n < size());return *(_start + n);
}
5、容量操作
5.1.size()和capacity()
我们知道两个指针相减返回的是相差的个数而不是空间大小,比如int*p1-int*p2返回的是(p1-p2)/sizeof(int*),因此可以利用指针相减的值返回size和capacity
size_t size()const
{return _finish - _start;
}
size_t capacity()const
{return _end_of_storage - _start;
}
5.2.reserve()
void reserve(size_t n){if (capacity() < n)//判断是否需要扩容,reserve不修改内容和size{size_t Old_size = size(); //记录当前容器当中有效数据的个数T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间if (_start) //判断是否为空容器{for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中{tmp[i] = _start[i];}delete[] _start; //将容器本身存储数据的空间释放}_start = tmp; //将tmp所维护的数据交给_start进行维护_finish = _start + Old_size; //容器有效数据的尾,加新的start_end_of_storage = _start + n; //整个容器的尾}}
执行扩容操作前需要先保存当前容器中的有效元素数量。 因为最终需要更新_finish指针的位置,而_finish的正确位置等于_start指针加上原始元素数量。如果在_start指针改变后再通过_finish - _start计算size,得到的结果将是无效的随机值。

5.3.resize()

resize规则:
- 当n大于size时,将size扩展到n,新增元素初始化为val,如果没有提供val,则使用该类型的默认构造函数生成默认值。
- 当n小于size时,将size减到n。
先比较n和size的大小,当n小时,直接将_finish置到n的位置,当n比较大时,复用reserve函数,开辟空间,再进行赋值操作,不用操心是否扩容的问题了
void resize(size_t n,const T& val)
{if (n < size()){_finish = _start + n;//直接覆盖掉后面的数据}else{reserve(n);while (_finish != _start + n){*_finish = val;++_finish;}}
}

5.4.empty()
就是简单的判空即可,比较start和finish的值即可,若返回位置相同即为空
bool empty()
{return(_start == _finish);
}
6、修改操作
6.1.push_back()
void push_back(const T& val)
{if (_finish == _end_of_storage){size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacityreserve(newcapcity);//开辟新空间大小}*_finish = val;_finish++;
}
需要注意的点时检查是否已经满了,满了要扩容,这里我们用三目表达式来写
6.2.pop_back()
尾删之前需要检查是否为空vector,若空则做断言处理,若不为空只需要将_finish--即可
void pop_back()
{assert(!empty());//或者finish>start_finish--;
}
6.3.insert()有迭代器失效情况
insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
//insert迭代器失效问题
void insert(iterator pos,const T& val)
{assert(pos >= _start && pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;//避免后面的迭代器失效reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了pos = _start + len;//这里是更新后的start}iterator it = _finish - 1;//最后一个数据位置while (it >= pos){*(it + 1) = *it;//往后挪it--;}*pos = val;++_finish;
}
6.4.erase()有迭代器失效情况
iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置){assert(pos >= _start);assert(pos < _finish);//assert(empty());前面断言了那两种就包含了这一个了iterator it = pos + 1;//记录后一个元素while (it < _finish){*(it - 1) = *it;//往前面挪++it;}--_finish;return pos;}
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器是否为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
6.5.swap()
调用标准库里面的swap函数
void swap(vector<T>& other)
{// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);
}

