C++ STL--> vector的模拟实现!

发布于:2025-08-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

     从vetor的模拟实现开始,我们就会开始涉及到泛型编程,模板类。作为模板类你要知道,vectro的元素可以有基本的数据类型: 字符,整形,空类型,浮点。 和自定义类型: 库里面的就有string,甚至可以vector<vector<int>>这样二维的。

  所谓万事开头难? 我们该如何实现它呢? 从确定成员变量开始!

1. 成员变量和迭代器

   这个思路其实很像顺序表升级为了模板类,原来只能放int 但是现在能放基本数据类型和自定义类型了,一一对应原来的( int*_arr(数组的首地址), size_t  _size() // 个数,_capacity // 顺序表容量)

  但是现在是模板类,不能很多时候可能不能用这些下标来操控,要时候指针,所以就定义三个指针: T* 类型的,分别指向 vector的首地址,和元素的末尾下一个, 以及容器的容量的位置。  但是为了统一容器操作,所以封装T*为 iterator 类型的。

   其次为了更加方便的实现begin, end 这些迭代器,可以提取把 size() 和capacity实现

     成员变量
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
  迭代器声明 和析构函数实现:

   这里也可以写一个const的迭代器,会好一些符合库里面写的标准: 

class vector {

public:
	typedef T* iterator;
	typedef const T* const_iterator;

	iterator begin() {
		return _start;
	}

	iterator end() {
		return _finish;
	}
	// const 迭代器

	const_iterator begin() const
	{
		return _start;
	}

	const_iterator end() const {
		return _finish;
	}

	~vector() {
		delete[]_start;
		_start = nullptr;
		_finish = nullptr;
		_end_of_storage = nullptr;
	}
  size和capacity: 

    因为我们的成员变量都是指针嘛,所以我的建议是最好把这个提前写出来 后面用起来就很方便。

size_t size()  const
{
	return _finish - _start;
}
size_t capacity() const
{
	return _end_of_storage - _start;
}

然后写完这些,我觉得开始写一个reserve,然后再写一个push_back. 会更好哦

2.扩容的reserve(size_t n)和push_back(T val)

reserve的实现:

    reserve的实现思路,最好提前存一下size,标记sz ,然后判断是否需要扩容。

   扩容的操作是,1. 提前开好n个空间, 2.如果原来空间不是空的(所以需要用_start来判空)注意这里最好不要用memcpy, 你会发现我这样是循环遍历然后一个一个的赋值的操作。    下面会讲解原因 ) 不是空的,那就赋值,最好是销毁delete原来空间 。

   最后是更新成员变量。

    

   为什么把原来的值拷贝的时候不用memcpy:

      这需要你对memcpy有一定的了解,memcpy是按照字节,一个一个的拷贝到新的数组中,有浅拷贝的意味,它并不会调用自定义函数的构造函数,这就是为什么要赋值操作一个一个的原因。     c中的内存管理都没有对自定义类型调用构造函数的功能,所以它的拷贝几乎都是浅拷贝。

    所以我们要用赋值,举个例子,如果这里是 vector<string>, 成员变量是string,如果是通过memcpy过去的它拷贝的时候按照string的成员变量拷贝的是它的指针数组的首地址,这是浅拷贝

当我们析构原来数组的时候,这时候这块string指向的内存已经被销毁了,所以出现二次析构的现象,会造成程序崩溃。 这就是浅拷贝的危害。

void reserve(size_t n)
{
	size_t sz = size();
	if (n > capacity())
	{
		T* temp = new T[n];
		if (_start) {
      //  不要使用 memcpy(temp,_start)
			for (int i = 0;i < size();i++)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = temp;
		_finish = _start + sz;
		_end_of_storage = _start + n;

	}

}
void push_back(T val)的实现

    这个实现思路很简单的,就是 让finish指向的内存为插入的值,值得注意的地方就是,需要我们对他进行扩容判断。

   不同编译器传递扩容的形式是不一样的,简单来看课vs编译器下对库里面的扩容是什么样的 和g++编译器 (linux x86内核)

g++ 

和vs下的: 

 显然linux中g++编译器使用的策略是每次都是2被扩容,vs中比较灵活,几乎都是1.5倍扩容,这是为了减少内存的浪费。

  下面我的思路是效仿g++的2被扩容

void push_back(T val) {
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : 2 * capacity());
	}
	_start[size()] = val;
	_finish++;

}

前面的 基本的迭代器啊,扩容搭建好了,后面写其他的就很顺手了,我这里是先写resize,然后进一步拷贝构造和赋值重载写了,再顺手把 insert 和eraser写了。

 顺便把resize写了:

     思路就是  判断n,然后小于等于size()自然就是之间覆盖就行了。

   另一种情况则需要扩容然后就是赋值。 这里的建议就是赋值的时候之间利用_finishi这个指针就好了不用创建其他变量 处理结束赋值 _finish也到了指定位置了。 

   还有就是这里的为什么是小于等于 ,等于这种情况也算进去就因为方便,不浪费资源没必要去扩容了。

void resize(size_t n, T val = T())
{
	if (n <= size())
	{
		_finish = _start + n;
	 }
	else {
		// 扩容 和赋值
		reserve(n);
		while(_finish <_start+n)
		{
			*_finish = val;
			_finish++;
		}

	}

}

	

3.构造函数:    

1.

主要写了3种。  空参构造 :我们这里就没有初始化列表了,因为在声明的时候就已经写好了,其次就是迭代器构造,这个是最典型的 

2.

    提前写一个迭代器类型哦,因为你并不知道这个迭代器会是什么样的,它可能是一个数组传过来的,还可能是另一个vector等等     vector(InputIterator begin,InputIterator last)

  实现起来就是遍历这个迭代器然后push_back一下

3.

   还有两种就是 vector (size_t n,T val=T()) ,含义就是 构造一个具有n个的元素都是Val的vector但是有一个前提就是,这里对val的缺省值是用了T()这是一个比较新的东西对刚接受实现vector来说,这是因为你传递的值还有可能是自定义类型的值,对应内置类型也是有基类的所以这样调用的构造函数是没有问题的。  这里的本质就是调用一个匿名函数然后拷贝构造给val。 这是很常见用于缺省值的。

 存在一个经常报经过的问题传递参数是vector(5,1) 这里的数字5默认是int的,然后被强转为size_t 所以同时还可以重载一个vector (int n,T val=T())

// 1. 空参构造  初始化在成员变量声明就已经初始化了
vector() {
  }

template<class InputIterator>
//  迭代器插入
vector(InputIterator begin, InputIterator last) {

	while (begin < last)
	{
		push_back(*begin);
		begin++;
	}
}
// n  val   为了兼容同时可以写一个int n 
 
vector(size_t n, T val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
		push_back(val);

}
vector(int  n, T val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
		push_back(val);

}

4.现代写法的拷贝构造和赋值重载 以及swap 

    其实写法跟string的实现很类似,先把swap实现了 ,然后就是拷贝构造中利用构造函数创建了一个temp中间变量。 然后和现在对象swap。

    赋值重载则是在传参的时候不穿引用会利用拷贝构造创建的temp 然后在swap一下

  swap的实现 

 swap的实现 就是把当前对象的所有成员变量的值一一交换一下就行。

void swap(vector<T>& v)
{
	std::swap(_start,v._start);
	std::swap(_end_of_storage,v._end_of_storage);
	std::swap(_finish,v._finish);
}
拷贝构造和赋值重载 

   你看吧挺巧妙的,层层利用: 拷贝构造利用构造 

    赋值重载 利用拷贝构造 

//拷贝构造和赋值重载
 vector(const vector<T>&v)
 { 
	 vector temp(v.begin(), v.end());
	 swap(temp);
  	 }

 vector& operator=(vector<T>temp)
 {
	 swap(temp);
	 return *this;

 }

5.erase和insert

   首先你心里面有这张图很重要。

 erase  实现:

eraser, iterator eraser(iterator pos)  

  删除指定位置的参数。

  首先最后是断言一下pos在_start 和_finish之间,其次只需要进行一个覆盖就行从_finishi-1到pos覆盖就行了。   

 可是从后往前覆盖会丢失数据使用可以从前往后覆盖 写一个临时迭代器it=pos+1  

  然后 *(it-1 )=*it   一直到it==finish 

  最后记得让_finish--

      

// resize(n,T val=T());
iterator eraser(iterator pos)
{
	assert(pos>=_start&&pos<_finish);
	iterator it = pos+1;
	while (it<_finish)
	{
		*(it - 1) = *it;
		it++;
	  }
	_finish--;
return pos;

 }

insert的实现 

      

      iterator insert(iterator pos,const T&val)

  在pos的位置插入val的值,那就从后往前覆盖把位置空处理。然后进行插入即可 

迭代器失效问题 

  迭代器失效问题还有好几种,下面我会说明的。这里的修改后的结果:

// insert的实现 
iterator insert(iterator pos, const T& val)
{
	
	
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
	
		reserve(capacity() == 0 ? 4 : 2 * capacity());
		pos = _start + len;
	}
	
	iterator it = _finish;
	while (it > pos)
	{
		*it = *(it - 1);
		it--;
	}
	*pos = val;
	_finish++;
	return pos;

       


网站公告

今日签到

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