vector的增删改查模拟实现(简单版)【C++】

发布于:2025-08-03 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

前言:

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. 构造函数和析构函数

		//constructor
		vector(){}

		//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的替换。

		//范围for
		for (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(); //error
		typename vector<T>::const_iterator it = v.begin(); //true

		while (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';

		//范围for
		for (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 是按字节复制数据的,它执行的是浅拷贝。对于简单的数据类型(如 intdouble),这没有问题。但对于包含动态分配内存的复杂类型(如 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());
				//更新pos
				pos = _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;

			//方式2
			erase(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简单实现源码请猛戳这里


网站公告

今日签到

点亮在社区的每一天
去签到