前言

上篇文章带大家认识了vector的一些基本接口,了解了这些接口时如何使用的,本篇博主带大家手搓一个vector类,从底层的角度认识、理解和掌握vector,让我们对内存管理、深浅拷贝有更深的理解!

目录

前言

(一)vector源码怎么看?

成员变量都有哪些?

push_back源码

如何理解函数体里的construct?

(二)手搓myvector可能出现的问题

1.迭代器失效

什么是迭代器失效?

insert时迭代器失效

erase时迭代器失效(删除顺序表的偶数)

从一个翻车场景切入

2.Gcc编译器下如何判定迭代器失效

删除顺序表的偶数:

(三)构造函数错配问题

解决方法

(四)本篇完整代码

1.myvector.h

2.Test.cpp


(一)vector源码怎么看?

链接:Source Insight 代码编辑器-中文网站

注意:阅读源码对新手来说是个巨大的挑战,我们不求把每一行代码都搞懂,大概了解每个函数是用来做什么的就行。

成员变量都有哪些?

这三个成员变量都是迭代器,看到命名就大致能猜到每个迭代器指向顺序表的哪个位置了,为验证猜想,我们先在源码里找到vector的begin()/ end()是怎么定义的:

证明我们猜想正确:start指向顺序表的首元素地址,finish指向顺序表最后一个元素的后一个地址。

通过capacity()容易得到:end_of_storage指向开辟好的空间的下一个位置。

push_back源码

如何理解函数体里的construct?

vector在类中自己实现了一个construct.h,它的作用就是:在已经分配好但尚未初始化的原始内存上,调用类型T的构造函数,构造出一个对象。意义是:使用定位new,为一个未初始化的空间进行初始化。(因为vector自己实现的申请空间是从内存池申请的,不像我们直接new出来的对象,会直接初始化)

template <class _T1, class _T2>
inline void _Construct(_T1* p, const _T2& value) {
    new (p) _T1(value); // placement new:在 p 指向的内存上构造对象
}

定位new在前文介绍过,这里就不再过多赘述了!详细内容见下面链接:

【C++初阶】内存管理:从 malloc 到 placement-new 的全解析-CSDN博客

(二)手搓myvector可能出现的问题

注意:

1.vector是一个模板类,模板不支持声明和定义分离,所以博主只实现了一个myvector.h,而不像string那样又搞一个.cpp文件。

2.为延续以前类的代码风格,myvector中的成员变量前也加一个下划线做区分,三个成员变量分别为_start , _finish , _end_of_storage。

3.myvector不使用内存池,直接用new开空间,不用像源码一样考虑construct的问题。

模板在前文也已经介绍过了,这里就不再过多赘述!模板内容见前文:

【C++初阶】一文讲透:如何使用模板实现泛型编程-CSDN博客

vector接口的实现步骤和string类似,博主在这里就不一一带大家实现了,文章结尾会贴出完整代码,手搓过程中出现问题可以看博主的代码,希望可以帮到你!

1.迭代器失效

迭代器失效是所有容器都存在的问题,string类之所以不介绍迭代器失效的问题,是因为我们一般直接使用指针,而不用迭代器封装,vector中迭代器失效分为两类:insert迭代器失效和erase迭代器失效。

什么是迭代器失效?

迭代器本质是一个指向容器内部元素的“指针”(或封装)。当容器内部存储结构发生变化时,原有的迭代器可能不再指向预期的元素,甚至变成野指针,这就是迭代器失效。

容易看出,vector的insert和erase的返回值不像string一样是void,而是返回了一个迭代器。

下面,博主会详细解释为什么这样设计:

insert时迭代器失效

案例:

错误的insert定义:

void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);

		Print(v);

		v.insert(v.begin(), 0);
		Print(v);

		auto it = v.begin() + 3;
		// insert以后,it是否失效?
		// it失效了,也就意味着,insert以后,it失效了,it就不能使用了
		v.insert(it, 30);
		Print(v);
	}

我们给的空间大小首次给4,先push_back四个元素,在insert一个元素就会触发扩容机制,reserve会开辟一个新的空间,把_start/_finish/_end_of_storage都给拷贝走,只有pos在原来的空间,原空间被释放后,pos就变成野指针了。

所以要更新pos,也就有了下面的写法:

它解决了什么?

扩容(reserve)会重新分配内存,原来传入的 pos 迭代器会失效。代码通过记录偏移量 len,在扩容后用 _start + len 重新定位 pos,使得后续挪动数据时 pos 仍然有效。

它没解决什么?

MSVC编译器默认使用过的迭代器就会失效:扩容后,所有指向原 vector 的迭代器都失效了,这是无法避免的。

erase时迭代器失效(删除顺序表的偶数)

从一个翻车场景切入

先用一段代码引出问题——看起来很合理的代码,运行却崩溃了。

auto it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				v.erase(it);
			}
			else
			{
				++it;
			}
		}

前面已经说明,MSVC编译器下,一个迭代器一旦被使用则会失效,不能再次调用,上述代码里while循环中频繁调用迭代器v,直接运行崩溃。

解决办法:重置迭代器

auto it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;
			}
		}

2.Gcc编译器下如何判定迭代器失效

删除顺序表的偶数:

运行结果:

1 2 3

结果正确,说明使用过的迭代器还能被再次使用。

vs2019对迭代器会严格检查,被使用后就不能再使用了,若想使用必须重置迭代器,而Gcc没有这么严格,但也会检查。

(三)构造函数错配问题

上图是两个拷贝构造,图一:拷贝迭代器区间,图二:拷贝n 个 val

我们在常规调用第二个拷贝构造的写法是:

运行结果!

我们本意是想调用第二个拷贝构造,它却调用了第一个拷贝构造,原因就是vector(size_t n, const T& val=T()),第一个参数是size_t类型,第二个是T,我们传过去的参数两个类型是一样的,都是int,而第二个迭代器区间的拷贝构造的两个参数是相同的,所以第二个更匹配我们传入的参数,编译器就优先选择调用第二个。

解决方法

vs2011是利用函数重载修复的这个问题,vs2019及以后有特殊的处理方法:C++11 引入了 SFINAE 原则和 <type_traits> 库。核心思路是:让范围构造函数仅在 InputIt 是“真正的迭代器”时才“现身”,否则就让它“消失”。这里就不再详细介绍,后续介绍现代C++时再展开聊聊。

(四)本篇完整代码

1.myvector.h

#pragma once
#include<iostream>
#include<algorithm>
#include<assert.h>
using namespace std;
namespace myvector {
	template <class T>
	class vector
	{
	public:
		void Print()
		{
			for (auto ch : *this)
			{
				cout << ch << " ";
			}
			cout << endl;
		}
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{ }
		~vector()
		{
			delete[] _start;
			_start = _finish = _end_of_storage = 0;
		}
		size_t capacity()
		{
			return _end_of_storage - _start;
		}
		size_t size()
		{
			return _finish - _start;
		}
		using const_iterator = const T*;
		using iterator = T*;
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
		void reserve(size_t n)
		{
			assert(n >= capacity());
			size_t sz = size();
			iterator tmp = new T[n];
			if (_start)
			{
				memcpy(tmp, _start, sizeof(T) * sz);
				//strncpy(tmp, _start, sizeof(T) * sz); err
				delete[] _start;
			}
			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : 2 * capacity());
			}
			*_finish = x;
			++_finish;
		}
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
		}
		const T& operator[](size_t i) const
		{
			assert(i < size());
			return _start[i];
		}
		void pop_back()
		{
			assert(!empty());
			_finish--;
		}
		bool empty()
		{
			return _start == _finish;
		}
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				pos = _start + len;  //  更新pos
			}
			auto it = _finish;
			while (it != pos-1)
			{
				*it = *(it - 1);
				--it;
			}
			*pos = x;
			++_finish;
			return pos;
		}
		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);
			auto it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}
		void resize(size_t n,T val=T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else {
				reserve(n);
				while (_finish < _start + n)
				{
					push_back(val);
					_finish++;
				}
			}
		}
		//vector()
		//	:_start(nullptr)
		//	, _finish(nullptr)
		//	, _end_of_storage(nullptr)
		//{
		//}
 
		//vector<T> v1(v2)
		vector(vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& r : v)
			{
				push_back(r);
			}
		}
		//拷贝迭代器区间
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		//{}初始化
		//v1={1,2,3}
		vector(initializer_list<T> il)
		{
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		vector(size_t n, const T& val=T())
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}
		void clear()
		{
			_start = _finish;
		}
		//赋值运算符重载
		vector<T>& operator=(const vector<T>& v)
		{
			clear();
			reserve(v.capacity());
			for (auto& r : v)
			{
				push_back(r);
			}
			return *this;
		}
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		//现代写法
		//vector(const vcetor<T>& v)
		//{
		//	vector<T> tmp = (v.begin(), v.end());
		//	swap(tmp);
		//}
		vector<T>& operator=(vector<T> tmp)
		{
			swap(tmp);
			return *this;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

2.Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"Myvector.h"
namespace myvector {
	void Test1()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(1);
		v.push_back(1);
		v.push_back(1);
		v.push_back(1);
		v.push_back(1);
		v.Print();
	}
	void Test2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.insert(v.begin(),5);
		auto it = v.begin() + 1;
		it = v.insert(it, 0);
		v.Print();
		v.erase(it+1);
		v.Print();
	}
	void Test3()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(4);
		//v.resize(4, 1);
		//v.Print();
		//v.resize(2);
		//v.Print();
		vector<int> v2 = v;
		v2.Print();
	}
	void Test4()
	{
		vector<int> v(5, 1);
		v.Print();
	}
}
int main()
{
	myvector::Test4();
	return 0;
}

创作不易,请读者读后点赞、收藏、评论!下篇博主会向大家介绍STL:list,敬请期待~

更多推荐