C++初阶--vector模拟实现

今天我们不直接上代码了,我们先来分析一下vector这个容器。
我们来看看这个东西
vector
这我们可以大概猜到是一个二维数组。
但并不完全是,二维数组是一个n*n 的矩形结构,而这里的vector套vector可不是这样,
我们来剖析一下它
C++初阶--vector模拟实现_第1张图片

C++初阶--vector模拟实现_第2张图片
当我们执行
vv [ i] [ j ] 这行代码时
首先执行 vv. operator [ i ],也就是vv [ i ] 这时候我们在一个大vector中找到了,很多很多的vector。每一个vector 是用i 控制的,一般用来控制矩形中行的信息。
这时候我们需要访问具体的int,它们存在每个小vector中,那就执行这样的代码
vv [ i ] .operator [ j ] ,这也就等价于 vv [ i ][ j ]
至此我们理解了双v的底层逻辑。
有人会问,为什么不直接用二维数组呢?这个问题很简单,我们的双V什么数据类型都可以存放,而且空间非常的灵活。数组能存什么呢?你不得使用数组指针之类的复杂概念。
那你又说了,这个东西调用来调用去,效率坑定没有数组直接开的快啊,
真是不好意思,我们函数也是有inline 的,展开后并没有什么差别的呦。


我们在上代码之前,再来看看stl源码中是如何组织整个vector的。
C++初阶--vector模拟实现_第3张图片
有了这个图,再看看他们的成员函数和变量就大概懂了,我的能力暂时无法完整的实现stl源码。
所以这里的vector模拟实现,只是简易版的。
接下来就直接上代码了,具体需要解释的地方,我会在代码中解释。

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
#include 
#include 

using namespace std;

namespace xxx
{
	template <class T>
	class Vector
	{
	public:  c++中默认是private,所以要加上pblic
		一:先来搞迭代器   迭代器怎么搞呢		1、先写指针 
											2、 写begin 和end  当然要有const和非const版本了
		
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator cbegin() const
		{
			return _start;
		}

		const_iterator cend() const
		{
			return _finish;
		}

		 二 、我们开始实现默认成员函数
		 构造函数:构造函数要怎么实现呢?函数名和类名一样,Vector,主要完成初始化列表,让他自动调用自动初始化。
		 我们需要初始化什么呢?那就是那几个成员变量而已啦,我们在private中写的只是声明,这里才会真正的定义。顶多加上缺省功能。
		我们注意一下,这里初始化可不是用等号初始化的,要用小括号。而且不需要带类型名。这里就是不用带iterator
		而且只有第一个变量前是 : 其他都是 ,
		
		Vector() = default; //这里是让系统生成默认的构造函数
		
		Vector()
			: _start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			printf("构造函数很好,over\n");
		
		}
		

		析构函数:函数和类同名,前面加上~,函数里面要实现 1if 判空  2delete 注意delete不加=  3、指针赋空
		~Vector()
		{
			
			if (_start)
			{
				delete[]  _start;
				_start = _finish = _end_of_storage = nullptr;

				
			}
			printf("  ~析构函数很好,over\n");
		}
		我们给这两个函数加上const,使调用者缩小权限。
		size_t Capacity()   const
		{
			return _end_of_storage - _start;
		}
		size_t Size()  const           
		{
			return _finish - _start;
		}

		reserve开辟capacity空间
		返回void ,传入开辟的size_t 一般实参就为capacity 
		1,判断是否需要扩容,如果需要扩容,我们开辟一个临时变量存新开辟的数组。2,需要判空 3,若不为空就循环赋值给临时数组4,更新三个变量的值。
		这里我们解释一下这个函数的功能:v2(v1) 这里的rerserve是v2的,当然如果之前有数据的话,size()就不是0,这里开辟新的够大空间,把原来的数据拷贝的新数组里。
		void reserve(size_t n)
		{
		
			
			if (n > Capacity())
			{
				size_t size = Size();
				T* tmp = new T[n]; //这里可以理解为,T类型的数组开n个空间。
				if (_start)     
				{

					memcpy(tmp, _start, sizeof(T)*size);       //这里我们换一种写法。
					for (size_t i = 0; i < size; i++)
					{
						tmp[i] = _start[i];
					 }
					delete[] _start;          //把原来的_start删了,不然还是用的原来的。
					
				}
				_start = tmp;
				_finish = _start + size;
				_end_of_storage = _start + n;
			}
			
		}


		resize 传参为大小和一个缺省构造。1、判断并缩容 2、否则如果n过大就增容,循环给默认值并更新变量

		void resize(size_t n, const T& val = T()) //这里T()为构造函数且是val 的缺省值,默认是0;
		{
			if (n <= Size())
			{
				_finish =_start + n;
			}
			else
			{
				if (n>Capacity())
				{
					reserve(n);
				}

				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
				
				

			}
			这里为什么不更新_endof
		}





		*****拷贝构造****** 难点
		拷贝构造是构造函数的重载,所以函数写法一样,只是参数不一样,参数这里使用类模板并且取引用
		功能如何实现呢?1、首要new一个新空间,T类型的数组,capacity那么大,指向新start。2、然后把传进来的类一个一个赋值给新空间3、修改另外两个变量的值
		Vector(Vector <T> & v)
		{
			printf("我是构造函数的弟弟,我很好,over\n");
			_start = new T[v.Capacity()];
			for (int i = 0; i < v.Size(); i++)
			{
				_start[i] = v._start[i];
			}
			_finish = _start + v.Size();
			_end_of_storage = _start + v.Capacity();
			
		}

		拷贝构造还有第二种实现方法 v2(v1)
		我们传入const 变量,因为我们不会去修改小v的内容,一般传进来权限缩小,变为只读。同时我们进行初始化列表初始化,这里有个问题,这里不是和构造函数的初始化列表一样吗?所以我们可得,调用拷贝构造出来的函数,不会再去调用它的构造函数了。
		 1 ,用reserve开空间  2, 我们用两组迭代器,一组是this的迭代器,一组是const的迭代器,循环赋值 3,更新另外两个变量

		Vector(const Vector<T> & v)
			: _start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(v.Capacity());  

			iterator vi = begin();
			const_iterator cvi = v.cbegin();

			while (cvi != v.cend())
			{
				*vi++ = *cvi++;
			}
			_finish = _start + v.Size();
			_end_of_storage = _start + v.Capacity();
		
		}


		****赋值运算符重载*****
		函数的返回值是类的引用, 参数是const引用,1、判断是否给自己赋值2、删除之前的空间,并开辟新的空间3、然后把数据拷贝过来4、更新变量5、返回值
		Vector<T>& operator=(const Vector<T>& v)
		{
			if (this != &v)        //注意这里是取地址V
			{
				delete[] _start;
				_start = new T[sizeof(T)*v.Size()];  //这里为什么要sizeof(T),不是应该T类型有几个就是几个吗,为什么要算字节呢?
				//reserve(sizeof(T)*v.Size());       //这里为什么不去调用开辟函数呢?
				memcpy(_start, v._start, v.Size()*sizeof(T));
				_finish = _start + v.Size();
				_end_of_storage = _start + _capacity;
			}
			return *this;
		}
		当然还有第二种实现方法
		 我们返回值依然是引用返回,而参数变成非const 非引用。1、我们进行swap,2、返回
		 这里解释一下这个函数,当我们拷贝构造一个v传进来。我们在里面进行交换,有用的已经被this存下来了,没用的出函数栈帧自动销毁了。
		Vector<T>& operator=(Vector<T> v)   
		{
			Swap(v);
			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);
		}
		pushback 1、判断扩容 2、更新变量
		void pushback(const T& val)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = Capacity() == 0 ? 2 : Capacity() * 2;
				reserve(newcapacity);

			}
			*_finish = val;
			_finish++;
			insert(_finish, val);  用insert的时候一定要结合find找新的迭代器,不然开新空间后,直接找不到
		}

		popback 1、断言 2、变量更新
		void popback()
		{
			assert(Size() > 0);
			--_finish;

			//erase(--end());

		}





		insert 插入 :插入不需要返回值、参数是pos位置的迭代器,还有T类型引用,当然还要const1、我们先断言一下pos位置的正确性。2、存一下pos位置(避免迭代器失效) 3、判断空间大小,不够需要扩容并更新 4、迭代器循环移动空位。5,存入数据。6、更新变量
		void insert(iterator pos, const T& val)
		{
			assert(pos <= _finish);
			size_t posindex = pos - _start;
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = Capacity() == 0 ? 2 : Capacity() * 2;
				reserve(newcapacity); 
				pos = _start + posindex;
			}

			iterator end = _finish;
			while (pos < end)    //从后往前挪
			{
				*end = *(end - 1);
				end--;
			}
			*pos = val;
			_finish++;


		}


		Erase 返回下一个位置(这里默认用pos迭代器返回值自动更新) ,参数pos迭代器。
		1、pos给一个迭代器。2、循环给空3、更新变量
		iterator erase(iterator pos)
		{
			iterator begin = pos;
			while (pos != _finish)
			{
				*pos = *(pos + 1);	
				++pos;
			}
			--_finish;
			return begin;
		}




		operator[] 这里应该有两个版本 返回引用,使之可修改,参数传一个位置就行
		1、断言 2、返回
		T& operator [] (size_t pos)    //可读可写
		{
			assert(pos < Size());
			return _start[pos];
		}
		当我们用const对象掉上面的函数时候,是无法调用的,当调用下面的函数时,返回应该是const,不然被动的扩大了权限。本来const调用的你,在你函数里面转一圈我被修改了。
		const T& operator[] (size_t pos)  const    //只可读
		{
			assert(pos < Size());
			return _start[pos];
		}

		void print()
		{
			for (size_t i = 0; i < Size(); i++)
			{
				cout << _start[i] << " ";

			}
			cout << endl;
		}










	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;



		
	};


	void test1()            测试构造、析构、pushback popback 迭代器 ,拷贝构造 .访问
	{
		Vector<int> v1;
		v1.pushback(1);
		v1.pushback(2);
		v1.pushback(3);
		v1.pushback(4);
		v1.pushback(5);
		v1.pushback(6);
		v1.print();
		v1.popback();
		v1.print();
		cout << endl;
		Vector<int>v2(v1);
		v2.print();
		Vector<int>::iterator  it = v1.begin();
		while (it != v1.end())
		{
			cout << *it << "  " ;
			it++;
		}
		cout << "我是循环迭代器";
		cout << endl;
		

		for (auto e : v1)
		{
			cout << e << "   ";
			
		}
		cout << "我是for迭代器";
		cout << endl;

		Vector<int> v3;
		v3 = v2;
		v3.print();
		


	}

	void test2()     //测试insert和erase
	{
		Vector<int> v1;
		v1.pushback(1);
		v1.pushback(2);
		v1.pushback(3);
		v1.pushback(4);
		v1.pushback(5);

		v1.print();

		Vector<int>::iterator pos = find(v1.begin(), v1.end(), 3);
		v1.insert(pos, 250);
		v1.print();
		v1.insert(pos, 0);

		Vector<int>::iterator pos1 = find(v1.begin(), v1.end(), 250);
		Vector<int> ::iterator e_ret = v1.erase(pos1);
		e_ret = v1.erase(e_ret);
		e_ret = v1.erase(e_ret);
		v1.print();

	}


}



在代码的最后我们来总结一下比较难理解的地方:

我们先谈一下构造函数

可以给一个 n ,再给一个模板参数类型的值。这种情况很简单,就是给n个T。
当然还有下图这样的写法:
C++初阶--vector模拟实现_第4张图片
这样写法的意思是,类模板的成员函数还可以再是函数模板。
我们解析一下这句话,类模板的成员函数指的是 InputIterator ,就是指我们的迭代器类型。我们把这些类型当作模板参数,再写一个函数模板。这样就可以根据迭代器类型自动实例化对应的迭代器了。


对于resize函数,我们先来看看stl库中是怎么实现的。
C++初阶--vector模拟实现_第5张图片
这里的第一个参数为开辟容量的大小,第二个参数是我们需要给的默认值。
在这里插入图片描述
我们从官方库中可以看到,这里的value_type 是指第一个模板参数,也就是T。
所以我们的函数可以这样写:
void resize (size_type n , T val = T() );
这里第二个参数是一个缺省参数,我们可以叫做T() 的匿名构造函数,我们不给值默认初始化为0。
int () , int都有默认构造函数 ,默认值也是0.

说完resize ,我们再看看insert 我们需要注意的地方。
C++初阶--vector模拟实现_第6张图片
这里是我们insert的实现,大家看看有什么问题。
我们在reserve中进行开辟空间,并进行原数组中数据的迁移,在reserve中,开辟新空间,我们会手动更新_start变量,
会自动更新pos位置的变量。但是我们可以看到,我们这里函数的参数是非引用的,也就是说这里reserve进行pos的改变并不会影响到我们insert函数中pos的位置,这时候pos可能已经变成一个野指针了。
总而言之,reeserve中对变量的更新为了确保开辟空间和返回的成功,但是无法确保调用它的函数insert中pos也跟着更新,从而reserve释放空间导致pos野指针的产生。这里有一种方法是进行取引用,这样当然可以解决这样的问题。但是存在很多其他问题(埋一个坑,我暂时也不知道)。

当然insert和erase还很容易引起我们的迭代器失效问题,我们简单提一下:
C++初阶--vector模拟实现_第7张图片
当我们写出这样的代码,很明显就有问题了,这个代码再处理偶数会跳过有效数据。
当末尾为偶数会错开循环终止条件。
这里我们处理的方法是接收erase函数的返回值,因为erase的返回值是它下一个数据的迭代器。

这里具体想知道的同学可以选择移步这里:
https://blog.csdn.net/Iiverson2000/article/details/119218457

拷贝构造函数
我们先来说一下拷贝构造的函数名
C++初阶--vector模拟实现_第8张图片
这两种写法都是可以的,当然这里建议加上类模板参数

第二点:当我们不写拷贝构造函数,用编译器默认的时候,它对内置类型变量只会完成浅拷贝。我们知道我们的内置类型变量是三个指针,对指针进行浅拷贝,把指针类型直接拷贝过来,两个vector用的同一套指针。这问题可很严重。所以我们要自己实现深拷贝。其实深拷贝就是调用reserve进行开空间。
C++初阶--vector模拟实现_第9张图片
我们进入拷贝构造内部看看需要有什么注意的地方

C++初阶--vector模拟实现_第10张图片
这是一种实现方法,我们一定要给this也就是被拷贝的对象的指针赋初值,不然我们进入reserve中会操作到随机值的指针。
这里用范围for循环进行赋值真的是很巧妙,还可以用memcpy进行拷贝。
C++初阶--vector模拟实现_第11张图片
但是这里一定要把指针变量_finish进行更新一下,endofstorage是不用更新的,他在reserve开空间的时候已经更新过了。
只有finish在进行赋值的时候会改变。所以更新一下它就可以了。

当然我们可以不用reserve,我们直接自己new,这里把三个变量都更新一下就好了。
C++初阶--vector模拟实现_第12张图片

接下来轮到我们的赋值运算符重载
同样的不写默认是浅拷贝,我们vector中都是指针,怎么能用浅拷贝呢?
这里有复杂写法和nb写法,我们再来总结一下怎么写。当然函数名是一样的,返回值是引用,参数最好写成const 引用。
复杂写法:v1=v2
0、判断是否给自己赋值 1、deletev1原空间 2、new一个v2大小的空间 3、memcpy或其他方式拷贝数据 4、更新变量 5、返回新的指针。
这里理解一下为什么要判断不能给自己赋值,因为第一步删除了空间,那第二步又new新值,这样直接造成野指针问题。

nb写法:
C++初阶--vector模拟实现_第13张图片
这里需要注意的是,我们返回值依旧是this指针,参数变成了非引用非const,
这里的大致意思为:一份有效的数据通过拷贝构造到参数中,我们用没有数据的this和拷贝来的有效数据进行交换,
最后函数结束,出栈帧,拿到无效数据的形参v 随着栈帧释放。
在形象一点吧:我叫外卖员给我送外卖,他来我们家,我拿了我需要的食物,并把家里的垃圾让他带走。我只是多花了点钱,相当于多进行了一次拷贝构造。

当然我们可以把swap进行包装一下:
C++初阶--vector模拟实现_第14张图片
如果不加域限定符,那swap会自动匹配局部我们写的单参数的函数进行调用,这里我们需要用std中的模板函数。
这样使用也是有前提的,我们需要把命名空间展开在头文件之前,不然全局也无法找到std的swap函数。

这里我们把基本的函数的实现和理解都了解的差不多了


更深层次深拷贝的更深理解

当我们要构造一个这样的函数时,会发生什么情况呢?
C++初阶--vector模拟实现_第15张图片
我们构造一个vector ,每个数据都是一个字符串。
我们先来解释一下这里pushback是如何实现的。
v. push_back(“11111”)
v.push_back( string = “11111”)
其中,string = “11111” 就是先进行构造,用11111构造一个隐藏的string,在拷贝构造给我们这里的string。
这叫做隐式类型转换
当然,我们的编译器直接给我们优化成了构造。

这里了解后,我们构造内部是如何实现的呢?
C++初阶--vector模拟实现_第16张图片
当我们这样操作,把每一个v值拷贝给e 。 其中v中都是字符串string。
string我们也实现过的,内部变量还是指针,每一次的赋值给e都是一次深拷贝。
所以我们最好给它加引用,const也可以加上,我们只进行读取操作。
C++初阶--vector模拟实现_第17张图片
我们正式开始验证代码
C++初阶--vector模拟实现_第18张图片
我们发现前四个输出都是正常的,第五个输出是随机值。
当我们把第一个字符串给的很长很长 “11111111111111111”时,也会产生随机值,在析构的时候还会崩溃。
我们可以知道,我们在进行pushback的时候,如果空间不够会进行reserve扩容,其中我们reserve是用memcpy实现的自己原本数据的迁移。

C++初阶--vector模拟实现_第19张图片
每个string内部的指针是指向堆内开辟的空间的。那么1111 等字符串是在堆区域中,并不在我们的vector中,所以当我们大于四个串进来进行扩容的时候,我们会把vector中字节一个一个拷贝过来,当然我们知道,指针怎么能拷贝呢?那不是指向同一个地方,也就是说,我们在新开辟的二倍空间中也会有一个和原来指针一模一样的指针,指向同一个空间,那么这时候析构原空间,我的新空间没地方指了就会发生错误。这也就是为什么当pushback第五个数据的时候报错的原因。

那当我们第一个字符串给很长“111111111111”的时候为什么会报错呢?他没有开辟空间呀?其实不是这样的。
我们在vs中,它的string很有特色,
C++初阶--vector模拟实现_第20张图片
它不仅仅有一个指针ptr,他还有一个16个空间的数组buff [ 16 ].当数据小于16 我们会进行在buf中存,如果大于16就存在堆上的str上。
当我们给的串长度小于16,我们在进行拷贝的扩容拷贝reserve的时候,这里的浅拷贝会因为buf数组把串内的数据拷贝到新空间中,这样也就没问题了,如果当串大于16字节,那么在堆上的串通过浅拷贝是无法拷贝过来的,就和第一种情况一样了。

为什么说是vs特色呢?因为在linux下可是没有这样的buf,而全是_str 的。

我们总结一下上面的内容:
如果T 是内置类型,或者浅拷贝的自定义类型,在增容和拷贝构造时,使用memcoy是完全没有问题的,但是遇到深拷贝的自定义类型比如这里的string。我们就无法用memcpy拷贝。

那么该如何解决呢?
C++初阶--vector模拟实现_第21张图片
我们来看看这样的方法可行吗?
这里其实很容易理解, tmp [ i ] 是vector中的一个又一个字符串,同时_start [ i ]也是,那么字符串给字符串赋值就会调用对应的赋值重载进行深拷贝赋值。这样我们的问题就解决了。

当然我们c++stl中实现的方式是类型萃取的方式,简单的说就是能够区分我们的变量类型。
内置类型就用memcpy,自定义类型就是用我们这里的解决方案:for + 赋值重载的方式。

这里就深刻的体现了c++极致追求效率的品质。

你可能感兴趣的:(C\C++,笔记,数据结构与算法,c++,算法,vector,容器,stl)