C++类——Vector容器的模拟实现

目录

一.vector类的成员变量:

二.Vector类的初始化方式:

三.vector的基本成员函数

四.vector类的增删查改:

指针失效问题:

insert():

代码解析:

erase():

代码解析:

所以erase()函数的正确写法:

五迭代器:

六:构造函数新写法:

6.2非法寻址报错

解决方法:

七.拷贝构造和赋值重载

7.1拷贝构造:

7.2赋值重载函数


在上篇博客中,我们主要学习了STL的容器之一------vector(顺序表),理解了它的底层原理,在多个例子的测试和运行结果中,我们学会了众多成员函数的特性,感兴趣的小伙伴们可以点击下方的链接看一看:

zzC++STL------vector类_橙予清的zzz~的博客-CSDN博客https://blog.csdn.net/weixin_69283129/article/details/131899708?spm=1001.2014.3001.5502

为了更加深刻的理解vector容器,今天我们就来剖析剖析vector类的底层实现代码!

注:此次实现只是能够大致模拟出vector的底层原理和成员函数实现!

一.vector类的成员变量:

cpp 复制代码
template <class T>
	class vector {
	public:
		typedef T* iterator;

    private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};

由上可知:vector作为顺序表可以存储任意类型的数据,那么就需要用到泛型模板,而成员变量的类型全都是泛型(T*)指针:

成员1:_start指向的是容器vector里指向数组的首地址;

成员2:_finish指向的是容器vector数组中存放的最后一个数据位置的下一个位置;

成员3:_endof_storage指向的是容器vector数组中总容量的下一个位置。

二.Vector类的初始化方式:

cpp 复制代码
template <class T>
	class vector {
	public:
		typedef T* iterator;
         vector()	//构造函数------无参构造
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr)
		{
		}
		//扩容函数
		void reserve(size_t n) {
			if (n > capacity()) {
				size_t len = size();    //标记原来的存储长度
				T* tmp = new T[n];      //默认为异地扩容
				//memcpy(_start, tmp, sizeof(T) * len);		
				for (size_t i = 0; i < len; ++i) {	
						tmp[i] = _start[i];    //将原来空间的数据传到新空间中
					}
				}
				delete[] _start;    //释放旧空间
				_start = tmp;      
				_endof_enocrage = _start + n; //重新赋值
				_finish = _start + len;
			}
		}
		~vector() {    //析构
			delete[] _start;
			_start = _finish = _endof_enocrage = nullptr;
		}

     private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};   

因为成员变量都是指针的缘故,构造函数对其成员变量做初始化只需要先构造为空指针即可------利用初始化列表进行。

若是对vector类对象增添数据时,需要提前判断,并且使用reserve扩容函数进行即可。

而析构函数中只需要删除_start指向的堆区空间即可,因为_start永远指向空间的首元素地址,_start一释放,_finish和_endof_enocrage就全变为了野指针,到时候只需要置空即可。

三.vector的基本成员函数

根据这些成员变量我们可以轻松的写出这些函数的底层实现:

cpp 复制代码
    bool empty() const {        //判空
			return _finish == _start;
		}

    void clear() {
    		_finish = _start;	//只清数据,不释放空间,也不清容量
		}

	size_t size()const {        //获取数据存储长度
			return _finish - _start;
		}

	size_t capacity() const {    //获取数组现有容量
			return _endof_enocrage - _start;
		}

    size_t max_size() const {    //获取数组能存储的最大数据数量
		    size_t max = -1;
		    return (max / 4);
	    }

    void resize(size_t n, T val = T()) {    //调整数组已存数据大小,分三种情况
		if (n > capacity()) {
			reserve(n);
		}
		if (n > size()) {
			while (_finish < _endof_enocrage) {
				*_finish = val;
				++_finish;
			}
		}
		else {
			_finish = _start + n;
		    }
		}

对于size()和capacity()函数来说,使用的都是指针-指针的方式求出整型数据量。

对resize()函数底层看不懂的,可以去上边我发的链接中了解了解resize()函数的功能特性!

四.vector类的增删查改:

cpp 复制代码
template <class T>
class vector {
    public:	 
    void push_back(const T& val) {    //尾插
		//若插入的时候空间不够,先扩容
		if (_finish == _endof_enocrage) {
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);
		}
		*_finish = val;    //插入数据
		++_finish;
	}

	void pop_back() {    //尾删
		assert(!empty());
		--_finish;
	}
    //[]重载运算符函数------用于查看或者修改vector类对象的数据
    T& operator[](size_t pos) {
	    assert(pos < size());
		return _start[pos];
	}
};

指针失效问题:

这个问题关系到insert()和erase()两个函数的使用:

insert():

cpp 复制代码
iterator insert(iterator pos, const T& val) {
			assert(pos >= _start);	  
			assert(pos < _finish);	 
			size_t len = pos - _start;
			if (_finish == _endof_enocrage) {
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);    //扩容

				//更新pos位置
				pos = _start + len;    
			}
			//挪动位置
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//插入数据
			*pos = val;
			++_finish;
			return pos;		//返回pos即可
		}

代码解析:

语句1: assert(pos >= _start); //允许头插,中间插
语句2: assert(pos < _finish); //不允许尾插,因为有push_back()

语句3: size_t len = pos - _start; //记录扩容前的pos位置,这是为下文埋下的伏笔!
语句4: pos = _start + len; //因为扩容后pos的位置会失效,所以需要重新更新pos的指向:pos的类型是指针,且pos也是指向vector数组空间的某个位置的,若是当前vector数组可能已经满容量了,需要进行扩容,意味着会发生异地扩容的情况,异地扩容后,系统会为其重新分配一块空间,那么原来的地址空间就失效了, pos原本指向旧地址空间的特定,旧空间的地址也就不属于vector了,pos成为野指针!,所以需要重新变更pos位置的值才行!!!------这就是指针失效的解决方法。

剩下的就是挪动pos位置的数据,为pos位置留下一个数据空位,供该位置增添数据。

举个例子:

如上图:就是在该数组的元素3位置(pos)处增添一个数据9,pos的值应该是:_start+8处(_start指向的数组首元素地址,pos位置的值就需是指向_start的两个元素(int是4字节)之后的地址)

现在由于该数组容量已满,需要扩容,重新划分一块堆区空间:

那么pos的值就不能再指向旧空间的0x1122334c了,需要重新根据_start的地址进行+8处理。,否则就是产生insert指针失效问题!


erase():

cpp 复制代码
void erase(iterator pos) {
			assert(pos >= _start);
			assert(pos < _finish);

			iterator end = pos + 1;
            //挪动数据
			while (end < _finish) {
				*(end - 1) = *end;
				++end;
			}
			--_finish;
		}

代码解析:

erase函数的指针失效问题在于案例测试上,代码没有什么需要注意的地方,因为删除元素不会影响扩容缩容等问题,_start和pos位置也就不需要变更。

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上指针不应该会失效。

但是,若是出现了下面这种情况:即pos刚好是指向最后一个元素位置,那么删完之后,成员变量_finish指针就会往前挪一个元素位置,pos也就与该指针指向同一块位置,而endof_enocrage的位置是没有元素存储的,那么pos现在就处于越界状态。

这种特殊情况会导致erase后指针失效,那么之后想要再使用之前pos指针就会发生报错等问题!为了统一普通情况和特殊情况pos位置的安全性,我们就认为erase后,pos位置指针失效了!

所以解决方法为:加上erase的函数返回值类型为pos指针类型,每次使用完erase函数后都及时在外部更新pos位置。这样就不会导致其失效了。

所以erase()函数的正确写法:

cpp 复制代码
iterator erase(iterator pos) {
			assert(pos >= _start);
			assert(pos < _finish);

			iterator end = pos + 1;
			while (end < _finish) {
				*(end - 1) = *end;
				++end;
			}
			--_finish;
			return pos;
		}

加上返回值和函数返回值类型。

cpp 复制代码
试验代码:
void Test15() {
	Cheng::vector<int> v1;
	for (int i = 1; i < 5; ++i) {
		v1.push_back(i);
	}

	for (auto& e : v1) {
		cout << e << " ";
	}
	cout << endl;	

	//删除所有偶数
	auto pos = v1.begin();
	while (pos != v1.end()) {
		if (*pos % 2 == 0) {
			pos=v1.erase(pos);	//更新it,否则it会失效,出现错误
		}
		//若*it%2!=0,则++it(跳过)
		else {
			++pos;
		}
	}

五迭代器:

cpp 复制代码
template <class T>
	class vector {
	public:
		typedef T* iterator;
    //迭代器
		iterator begin() {
			return _start;
		}

		iterator end() {
			return _finish;
		}

		//const迭代器
		const_iterator begin() const {
			return _start;
		}

		const_iterator end() const {
			return _finish;
		}

      private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};

由于迭代器的begin、end等都是指针,那么类型自然与成员变量的类型相同都是泛型指针。

迭代器的价值:

这里讲一下迭代器的价值!

迭代器是每一个STL容器中都配备的一个组件,它的作用相当大!

作用1:封装底层实现,不暴露底层实现的细节。

作用2:提供统一的访问方式,降低了成本。

六:构造函数新写法:

如上图,这是C++官方库中给出的关于vector类的构造函数,有很多种构造函数,有了这些构造函数,可以让我们构建不同的vector对象,如下图:

所以我们也可以构建几个常用的vector构造函数进行实现:

cpp 复制代码
        vector()	//无参构造法
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr)
		{
		}

        //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

        //迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

6.2非法寻址报错

在测试使用这些构造函数时,我发现了一个问题,当我执行下面这两句代码时,代码产生了编译报错!:

vector<int> v1(5);

vector<int> v2(5,'a');

cpp 复制代码
class vector{
public:
 //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

//迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
        }

     void push_back(const T& val) {    //尾插
		//若插入的时候空间不够,先扩容
		if (_finish == _endof_enocrage) {
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);
		}
		*_finish = val;    //插入数据
		++_finish;
	        }
 private:
	iterator _start;
	iterator _finish;
	iterator _endof_enocrage;
    };

int main(){
    vector<int> v1;
    for (size_t i = 0; i <4; ++i) {
	    	v1.push_back(i);
    }

    vector<int> v2(5,1);    //本意:创建5个元素,且全赋值为1

}

传两个参数报错:非法的间接寻址

原因在于将vector类对象实例化时,编译器会根据类对象后面给出的参数进而寻找最匹配的构造函数,5,1都是int类型,和半缺省的size_t和T虽不是最匹配,但能用;而和迭代器区间构造函数InputIterator最匹配,但这俩参数并不能代入迭代器区间中,编译器不能理解,产生了编译报错!

解决方法:

cpp 复制代码
  //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

	//vector带参构造函数
		vector(int n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {

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

        //迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

再写一个int类型的vector带参构造函数,这样创建类对象时vector<int> v2(5,1); 编译器寻找最匹配的构造函数时会先匹配int类型的构造函数,这样就不会报寻址错误了!


七.拷贝构造和赋值重载

7.1拷贝构造:

cpp 复制代码
vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endof_enocrage(nullptr)
		{
			_start = new T[v.capacity()];
			size_t len = v.size();
			memcpy(_start, v._start, sizeof(T) * len);
			_finish = _start+len;
			size_t capa = v.capacity();
			_endof_enocrage = _start + capa;
		}

拷贝构造的本质上就是拷贝形参v的所有数据,而默认构造函数的拷贝方式是浅拷贝,对于内置类型来说,浅拷贝是正常的做法;但对于有指针开辟堆区空间的成员来说,浅拷贝无疑会造成内存泄漏,析构同一块空间多次的情况,所以浅拷贝得自己写才行!

所以针对深拷贝的情况,那么就得让拷贝的对象拥有一块自己的空间,剩下的就是复制被拷贝对象的数据了。

上面的代码为传统写法的代码,而拷贝构造还有一种更间接方便的形式:

cpp 复制代码
    //交换函数
	void Swap(vector<T>& v) {
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_endof_enocrage, v._endof_enocrage);
	}

	//拷贝构造
	vector(const vector<T>& v)
		:_start(nullptr)
		, _finish(nullptr)
		, _endof_enocrage(nullptr)
	{
		//引用传参就得新创建对象tmp,否则直接传swap(v)就是浅拷贝
		vector<T> tmp(v.begin(), v.end());
		Swap(tmp);
	}

首先,指派一个打工人帮你把被拷贝对象的数据做构造,你什么都不需要干,等打工人帮你把事情做好后,你就可以获取打工人所做的一切成果,然后把你自己的一无所有给了打工人,实现两者的交换,这样就完成了拷贝构造。

注:而对于两者的交换函数,我采用的是std库中的swap函数,利用this指针与形参的各个成员变量值做交换。

创建临时对象tmp时,编译器调用的是迭代器区间构造函数,意味着,tmp是自己开辟的一块堆区空间,并不是指向的v对象的_start空间,tmp也只是拷贝了对象v除_start以外的数据罢了,不会发生一块空间析构两次的情况!


7.2赋值重载函数

赋值重载函数与拷贝构造有着异曲同工之妙,所以赋值重载函数的传统写法我也就不写了。

直接展示新写法:

cpp 复制代码
vector<T>& operator=(const vector<T>& v) {    
            //(引用传参不会在函数中开空间)
			//所以需要创建临时对象(实例化空间)去swap拷贝
			vector<T> tmp(v.begin(), v.end());	
			this->Swap(tmp);
			return *this;
		}

赋值重载的新写法也是如出一辙,都是利用打工人去帮你做事情,然后使用交换函数,将各个成员变量进行交换,依次得到拷贝好的数据!

相关推荐
ROC_bird..1 小时前
STL - vector的使用和模拟实现
开发语言·c++
机器视觉知识推荐、就业指导1 小时前
C++中的栈(Stack)和堆(Heap)
c++
Mr_Xuhhh3 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
无敌岩雀3 小时前
C++设计模式行为模式———命令模式
c++·设计模式·命令模式
爱吃生蚝的于勒5 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据7 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
versatile_zpc9 小时前
C++初阶:类和对象(上)
开发语言·c++
小鱼仙官9 小时前
MFC IDC_STATIC控件嵌入一个DIALOG界面
c++·mfc
神仙别闹9 小时前
基本MFC类框架的俄罗斯方块游戏
c++·游戏·mfc
娅娅梨10 小时前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++