vector的增删改查模拟实现(简单版)【C++】
目录
前言:
1. vector的私有成员
2. 构造函数和析构函数
3. vector遍历
3.1 push_back
3.2 下标访问、迭代器、范围for 遍历
3.2.1 下标访问
3.2.2 迭代器
3.2.3 范围for
3.2.4 print_vector
3.3 测试遍历
4. vector的增删改查
4.1 reserve
4.2 resize
4.3 insert
4.4 erase
4.5 push_back 现代写法
4.6 pop_back
5. 拷贝构造
6. swap
7. operator=
8. 迭代器区间构造
9. 迭代器失效
9.1 关于 insert 迭代器失效
9.2 关于erase迭代器失效
10. 源码
前言:
模拟实现vector是为了更好地掌握vector的使用。
std::vector是C++ 标准模板库(STL)中的动态数组容器。
准备工作:
首先在自己创建的my_vector的命名空间中创建一个头文件vector.h,再定义一个vector类,同时创建一个test.cpp文件来调用测试。
1. vector的私有成员
vector的私有成员 和 string的私有成员不一样,vector主要使用的是迭代器iterator的类型。
#include<iostream>
#include<assert.h>using namespace std;
namespace my_vector
{template <class T>class vector {public:typedef T* iterator;//定义了一个名为 iterator 的类型别名,实际是 T*private:iterator _start = nullptr; //指向动态数组的起始地址(即第一个元素的位置)iterator _finish = nullptr; //指向当前最后一个有效元素的下一个位置(相当于size()的终点)iterator _endofstorage = nullptr; //指向整个存储空间的末尾(即容量 capacity() 的终点)};}
template <class T>
class vector
这是一个类模板,允许用户指定任意类型T(例如 int、string等)来创建不同类型的vector,例如:vector<int>、vector<string>等。
2. 构造函数和析构函数
//constructorvector(){}//destructor~vector() {delete[] _start;_start = _finish = _endofstorage = nullptr;}
3. vector遍历
3.1 push_back
在实现遍历之前,首先保证vector里面有元素,所以先实现一个尾插元素。
传统写法:
先判断是否需要库容,之后再插入元素。扩容的时候,先开辟新空间,再拷贝数据,释放旧空间,指向新空间,修改指针,最后添加数据。
在实现push_back之前要先实现 size() 和 capacity 分别来获取 数据个数和总容量。
size_t capacity() const{return _endofstorage - _start;}size_t size() const{return _finish - _start;}
注意在修改_finish的时候不能单纯的加size(),因为size()是_finish 与 _start两个指针相减的结果,而此时_finish还没有更新仍指向旧空间。
void push_back(const T& val) {//先判断是否要扩容if (_finish == _endofstorage) {size_t old_size = size();//扩容size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];//拷贝数据memcpy(tmp,_start,sizeof(T)*size());//释放就空间delete[] _start;//修改指针_start = tmp;//注意//_finish = tmp + size(); //error _finish = tmp + old_size;_endofstorage = tmp + newcapacity;}//填充数据//_start[size()] = val;*_finish = val;++_finish;}
解决方法:在扩容前,记录好size()
3.2 下标访问、迭代器、范围for 遍历
3.2.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];}
3.2.2 迭代器
迭代器同样分为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;}
3.2.3 范围for
范围for是iterator的替换。
//范围forfor (auto e : v){cout << e << " ";}cout << endl;
3.2.4 print_vector
这里实现一个函数,直接来打印vector。
范围for实现:
template<typename T>void print_vector(const vector<T>& v) {for (auto e : v) {cout << e << " ";}cout << endl;}
注意:
如果想用迭代器实现的时候注意:
编译器编译的时候,语法检查,确保模板实例化,一个类里面的内嵌类型或者内部类,vector<T>没有示例化,无法去找,就区分不出是类型还是静态成员遍历。
解决方法加上typename 告诉编译器是一个类型。
3.3 测试遍历
template<typename T>void print_vector(const vector<T>& v) {//vector<T>::const_iterator it = v.begin(); //errortypename vector<T>::const_iterator it = v.begin(); //truewhile (it != v.end()){cout << *it << " ";++it;}cout << endl;}//遍历void test_vector1() {vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);for (size_t i = 0; i < v.size();i++){cout << v[i] << " ";}cout << endl;vector<int>::iterator it = v.begin();//auto it = v.begin(); //这样写更方便while (it != v.end()){cout << *it << " ";++it;}cout << '\n';//范围forfor (auto e : v){cout << e << " ";}cout << endl;}
4. vector的增删改查
4.1 reserve
reserve用于预分配内存空间,避免频繁的动态内存分配。
- 如果 n > capacity() 就进行扩容
- 如果 n <= capacity() 什么都不做
根据标准库:
注意提前保存好size()。
void reserve(size_t n) {if (n > capacity()) {//扩容时注意提前保存好size()size_t old_size = size();//开新空间,扩容T* tmp = new T[n];//拷贝数据memcpy(tmp,_statr,sizeof(T)*size());//释放旧空间delete[] _start;//修改指针_start = tmp;//_finish = tmp + size(); //error ,因为 _finsih仍指向旧空间_finish = tmp + old_size;_endofstorage = tmp + n;}}
但是仍有问题:
如果T是string的时候就有问题,如图
我们发现 memcpy 是按照字节拷贝
memcpy
是按字节复制数据的,它执行的是浅拷贝。对于简单的数据类型(如 int
、double
),这没有问题。但对于包含动态分配内存的复杂类型(如 string
),memcpy
会复制 string
对象的字节,包括其内部的指针(如 string
的 char*
指针),而不会复制指针指向的实际数据。
虽然_start指向的数据是深拷贝,但是_str指向的数据是浅拷贝
解决方法:使用拷贝构造函数来移动实际的数据。
void reserve(size_t n) {if (n > capacity()) {//扩容时注意提前保存好size()size_t old_size = size();//开新空间,扩容T* tmp = new T[n];//拷贝数据//memcpy(tmp,_start,sizeof(T)*size()); //error,vector<string> 存在隐藏的浅拷贝问题//for (size_t i = 0; i < size();i++){tmp[i] = _start[i];}//释放旧空间delete[] _start;//修改指针_start = tmp;//_finish = tmp + size(); //error ,因为 _finsih仍指向旧空间_finish = tmp + old_size;_endofstorage = tmp + n;}}
4.2 resize
resize 可以直接调整 vector
的有效元素数量(size()
),并可以指定新增元素的默认值。
参考标准库里面的resize
如果 n > size()就扩容并插入数据,否则就删除数据。
这里缺省参数使用匿名函数的原因:
无参的匿名函数,会去调对应的默认构造函数来初始化,因为这样可以给到合适缺省值。
测试:
4.3 insert
在pos位置进行插入值value,首先判断插入位置是否合法,插入数据前要判断是否需要扩容,之后再移动数据,最后把数据插入。
//pos位置进行插入void insert(iterator pos, const T& value) {//判断插入位置是否合法assert(pos >= _start);assert(pos <= _finish);//数据插入前,先判断是否需要库容if (_finish == _endofstorage) {reserve(capacity() == 0 ? 4 : 2*capacity());}//移动数据iterator it = _finish - 1;while (it >= pos) //注意这个时候的pos指向的仍是就空间{*(it + 1) = *it;--it;}//插入数据*pos = value;++_finish;}
但是发现一下问题:
插入多个数据出现了问题,在调试插入元素的时候发现
这里我们发现it的地址编号 比pos的地址编号小,这样就无法进入while循环来移动元素了。
原因:迭代器失效
在判断空间是否需要进行扩容的时候,发生扩容,pos指向的旧空间被释放回收(可以理解为导致了野指针)。
解决方法:让其指向新空间的pos,提前判断是否扩容前,计算好相对的位置。
正确代码:
//pos位置进行插入void insert(iterator pos, const T& value) {//判断插入位置是否合法assert(pos >= _start);assert(pos <= _finish);//数据插入前,先判断是否需要库容if (_finish == _endofstorage) {size_t len = pos - _start; //提前计算好相对的位置reserve(capacity() == 0 ? 4 : 2*capacity());//更新pospos = _start + len;}//移动数据iterator it = _finish - 1;while (it >= pos) //注意这个时候的pos指向的仍是就空间{*(it + 1) = *it;--it;}//插入数据*pos = value;++_finish;}
测试一下;
4.4 erase
参考一下标准库:
先检查删除位置是否合法,再进行移动元素来完成删除。
void erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);//移动元素iterator it = pos+1;while(it < _finish) {*(it - 1) = *it;++it;}--_finish;}
移动元素如图:
测试:
4.5 push_back 现代写法
通过复用其他函数的方式来实现。
void push_back(const T& val) {//先判断是否要扩容if (_finish == _endofstorage) {reserve(capacity() == 0 ? 4 : 2*capacity());}//填数据*_finish = val;++_finish;}
push_back 现代写法plus版:
void push_back(const T& val) {insert(end(),val);}
4.6 pop_back
bool empty() {return _start == _finish;}void pop_back(){//尾删//方式1//assert(!empty());//--_finish;//方式2erase(end()-1);}
测试:
5. 拷贝构造
当没有去手动实现去拷贝构造时
发现 这里对内置类型的拷贝构造仍然时浅拷贝,这里和string一样 都是浅拷贝问题,这里需要手动实现一个拷贝构造。
//v2(v1)vector(const vector<T>& v) {reserve(v.capacity()); //避免频繁扩容for (auto& e : v) //auto& 避免拷贝{push_back(e);}}
6. swap
这里交换实现和string的类似,都是去调用标准库哦里面的swap去交换成员变量。
void swap(vector<T>& v) {std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_endofstorage,v._endofstorage);}
7. operator=
赋值操作符重载
将一个对象赋值给另一个对象,这里复用swap的写法。
vector<T>& operator=(vector<T> v) {swap(v);return *this;}
8. 迭代器区间构造
//迭代器区间构造template<class InputIterator> //类模板里面还可以有函数模板vector(InputIterator first, InputIterator last) {//其他容器的迭代器都可以用while (first != last) {push_back(*first);++first;}}
还需要注意:
vector(size_t n,const T& val = T()) {reserve(n);for (size_t i = 0; i < n; i++) {push_back(val);}}
解决方法: 将size_t 改为int 这样就可以保证参数匹配了
9. 迭代器失效
9.1 关于 insert 迭代器失效
在前面的insert函数里面已经去解决迭代器失效,但是void insert(iterator pos, const T& value),这里的pos参数是形参,形参的改变不影响实参。那给pos加上引用呢?也不可以,在函数调用传参的时候,传的是临时对象,临时对象又具有常性,那再加上const ,如果加上const ,那insert里面的就没有办法修改了。
it 使用insert以后,迭代器it就是失效了,所以就不要使用,重新更新迭代器it。
9.2 关于erase迭代器失效
出现的问题:
在去写删除偶数的函数时,重复的偶数没有删除。
情况1:跳过了一些值
原因:erase的删除是通过元素移动来覆盖前面的元素
情况2:错过了end()
解决方法:
auto it = v.begin();while (it != v.end()) {if (*it % 2 == 0){v.erase(it);}else {it++;}}
但是仅仅这样写是解决不了VS环境下的vector的问题。还需要参考标准库。
这里的erase会返回被删除后面的那个元素。
iterator erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);//移动元素iterator it = pos + 1;while(it < _finish) {*(it - 1) = *it;++it;}--_finish;return pos;}
迭代器失效以后,不要直接使用,如果要使用按规则重新更新后使用。
10. 源码
vector简单实现源码请猛戳这里