C++ vector(一)

༺ 个人主页 · 纪念229 ༻

🏠我的博客主页🏠

༒专栏目录:《数据结构》༒

༒专栏目录:《算法》༒

༒专栏目录:《C++》༒

༒专栏目录:《MySQL数据库》༒

༒专栏目录:《前端开发》༒

༒其它有趣的计算机知识༒

༺世上本没有路,走的人多了自然就有了༻


本篇文章讲述的是C++ 容器vector(数组)的讲解,希望对你有所帮助

主要讲的是vector里的一些函数的实现,用法与string有所差异


文章目录


1.vector.h

先看一下完整的vector类模板代码

这里直接展示vector类模板的代码是因为模板的声明和定义只能写在一块,我们先看一下构成,我们在一个一个看一下(注意:复现代码大多数差不多有问题的我都会标上注释)

c 复制代码
#pragma once
#include<assert.h>//assert头文件有两种#include<cassert>C++版本 #include<assert.h>C语言版本
//模板 声明和定义不能分离定义到两个文件.h.cpp


namespace zjz
{
	template<class T>
	class vector
	{
	public:
		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;
		}

		/*vector()
		{}*/

		//强制编译器生成默认构造
		vector() = default;//有了其它构造就不能使用默认构造,加了类名() = default就可以使用带参构造和默认构造一起用(但是默认构造是将构造交给编译器处理我们应该给成员变量缺省值不给的话成员变量就是随机值))//上面那个也是默认构造也能将默认构造与带参构造一起用只不过没这个好

		//v2(v1)
		vector(initializer_list<T> il)
		{
			reserve(il.size());//这个用参数的size是够用就行没必要多开空间
			for (auto e : il)
			{
				push_back(e);//这里用for循环拷贝不用memcpy拷贝是因为若是拷贝元素不是普通的内置类型是指针/类(深度拷贝)它不会拷贝元素里的所有数据只会拷贝地址没用,所以遇到类/函数模板用范围for/for循环拷贝
			}
		}
		//这个就是之前讲解的当构造函数时右边有一个某类型的数组时首先我们会将该数组变为initializer_list<类型名>这个盒子调用构造函数将数组里的数据放入对象的数组里面这个类(盒子)是只读的,只能用push_back拷贝不能改盒子里的数据

		//v2(v1)
		vector(const vector<T>& v)
		{
			reserve(v.capacity());//这里开空间用参数的capacity是为了将参数和对象的数据基本相同
			for (auto e : v)//范围for给的对象可以是类可以是数组但是调用的都是它们的数组(实际上调用的是它们的begin(),end())
			{
				push_back(e);//直接用push_bakc一个个拷贝方便for循环不方便
			}
		}

		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

		//v1 = v7
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;//这里返回的是vector<T>&类型是因为要支持链式赋值重载(v1 = v2 = v3)(返回类型为void的时候支持不了一行内只可以完成一次赋值重载)
		}

		//函数模板
		//任意类型容器迭代器初始化
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)//这里是类模板里的函数模板目的是用构造函数实现数组内容拷贝(调用的两个参数类型要相同并且要与函数内容相匹配)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

		//vector(iterator first,iterator last)//这些都是调用构造函数要在函数实例化(定义新的对象)时才可以使用
		//{
		//	while (first != last)
		//	{
		//		push_back(*first);
		//		++first;
		//	}
		//}

		vector(int n, T val = T())//这个是刚开始设置n个元素元素值都为val
		{
			resize(n, val);
		}

		vector(size_t n, T val = T())//这个T之类的话就是默认对象是内置类型的话就是0
		{
			resize(n, val);
		}

		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		void reserve(size_t n)
			//这里的扩容是异地扩容(在C语言里的realloc有原地扩也有异地扩)
		{
			//只有当n > capacity才需要扩容
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				//拷贝旧空间数据到新空间(前提是旧空间地址不为nullpter)
				if (_start)
				{
					for (size_t i = 0; i < old_size; ++i)
					{
						tmp[i] = _start[i];
					}
					//memcpy(tmp, _start, sizeof(T) * old_size);//memcpy和strcpy都在头文件#include<cstring>里是一种C语言的函数这里没有为啥能用memcpy(拷贝指针,被拷贝指针,所需字节个数(memcpy可以拷贝任何类型数组要拷贝多少有效类型的元素取决于我们需要多少字节)))//这里能用的原因是.h文件会写在.cpp文件里.cpp文件里有#include<iostream>在vs中这个头文件嵌套了#include<cstring>所以可以用memcpy
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + old_size;
				_endofstorage = _start + n;
			}
		}

		void resize(size_t n, T val = T())
		{
			if (n > size())
			{
				//扩容如果设置的有效数据大于capacity就扩容
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
			else {
				_finish = _start + n;//这里的resize可以引起扩容(reserve无法缩容)利用_finish可以改变有效数据
			}
		}

		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
		}
		const T& operator[](size_t i)const//还有非const类型的
		{
			//下标不能大于有效个数
			assert(i < size());
			return _start[i];//其实就是*(_start + 1)
		}

		void clear()
		{
			_finish = _start;//将_finish赋值为_start就是将数据清0
		}
		size_t size()const
		{
			return _finish - _start;
		}
		size_t capacity()const
		{
			return _endofstorage - _start;
		}
		void push_back(const T& x)
		{
			//判断是否需要扩容(_finish == _endofstorage说明需要扩容)
			if (_finish == _endofstorage)
			{
				reserve(capacity() == 0 ? 4 : 2 * capacity());
			}
			*_finish = x;//解引用以及(&)的优先级大于赋值
			++_finish;
		}

		void pop_back()
		{
			//assert(_finish > _start);
			assert(!empty());
			--_finish;
		}

		iterator insert(iterator pos, const T& x)
		{
			//先判断迭代器是否在范围内再判断是否扩容
			assert(pos >= _start);
			assert(pos <= _finish);//insert可以插入最后一个数据最后一个位置


			if (_finish == _endofstorage)
			{
				//扩容时是异地扩容旧地址上的pos迭代器没用了//所以如果要扩容的话就要更新pos迭代器(首先我们要将迭代器所在的位置的差值(pos - _start)求出)然后扩容后将更新后的_start 加上这个差值就是更新后的pos迭代器
				size_t len = pos - _start;//迭代器减迭代器是有符号整数这里我们隐式类型转换为size_t类型
				reserve(capacity() == 0 ? 4 : 2 * capacity());
				pos = _start + len;//更新迭代器pos将迭代器加size_t类型还是迭代器
			}
			//插入数据先移动再插入
			//移动(移动也是将数据后移不会改变pos后面的数据的位置)  用一个end(最开始指向最后一个元素)迭代器遍历没插入之前pos指向的元素以及后面的有效元素不改变其它迭代器的位子
			
			iterator end = _finish;
			while (end >= pos)
			{
				*(end) = *(end - 1);
				//*(end - 1) = *end;这里是后移不是迁移
				end--;
			}

			//以下方式不好,若是空vecotr end就是_start - 1 = _finish - 1越界
			//iterator end = _finish - 1;
			////利用while循环遍历
			//while (end >= pos)
			//{
			//	*(end + 1) = *end;
			//	end--;
			//}
			//在该位置插入数据
			*pos = x;
			++_finish;

			return pos;//最后返回pos防止扩容后迭代器pos的在旧地址不在有效数据范围内
		}

		iterator erase(iterator pos)
		{
			//删除和插入都一样pos迭代器都不能超出有效数范围
			assert(pos >= _start);
			assert(pos < _finish);//这里应该是pos < _finish(finish指的是最后一个数据的下一个位置)
			//erase删除是前移insert插入是后移赋值pos指向的位置
            //用一个迭代器it(pos + 1)遍历数据前移
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *(it);
				it++;
			}
			--_finish;
			return pos;//最后返回pos迭代器即可
		}

		bool empty()const
			//bool empty() 后面要加consty
		{
			return _start == _finish;//_start == _finish说明数组为空
		}

	private:
		//迭代器不是interator而是iterator
		iterator _start = nullptr;//指向数组的第一个元素
		iterator _finish = nullptr;//指向最后一个元素的下一个位置
		iterator _endofstorage = nullptr;//指向已经分配空间的下一个位置
		//为什么用三个指针(_start  _finish _endofstorage)而不是_size _capacity
		//用三个指针的好处:
			 //1.写起来更简洁,begin()直接返回_start,end()直接返回_finish)
			 //2.指针相减天然得到元素个数,不用维护_size
			 //3.很多STL实现(如GCC的std::vector)采用的方式
	};



}

我们知道vector是函数模板当然可以存放多种类型的数据即vector是数组可以是大多数类型数组

我们先看一下它写在访问限定符private的成员变量

c 复制代码
private:
	//迭代器不是interator而是iterator
	iterator _start = nullptr;//指向数组的第一个元素
	iterator _finish = nullptr;//指向最后一个元素的下一个位置
	iterator _endofstorage = nullptr;//指向已经分配空间的下一个位置
	//为什么用三个指针(_start  _finish _endofstorage)而不是_size _capacity
	//用三个指针的好处:
		 //1.写起来更简洁,begin()直接返回_start,end()直接返回_finish)
		 //2.指针相减天然得到元素个数,不用维护_size
		 //3.很多STL实现(如GCC的std::vector)采用的方式

vector的功能是由三个指针(_start(指向第一个元素)、_finish(指向最后一个元素的下一个位置)、_endofstorage(指向可容纳空间下一个位置))构成的

利用三个指针实现构造函数以及普通成员函数、运算符重载

指针类型是由数组是什么类型决定

template<class T>

c 复制代码
typedef T* iterator;
typedef const T* const_iterator;

iterator迭代器一般是指针、类这里的vector迭代器是指针

c 复制代码
iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

这里begin函数的意思是返回指向第一个元素的地址,end函数的意思是返回指向最后一个元素下一个位置的地址

cbegin函数和begin差不多cend与end差不多只是用它们不能修改对象的数据


1.1构造函数

c 复制代码
/*vector()
{}*/

//强制编译器生成默认构造
vector() = default;//有了其它构造就不能使用默认构造,加了类名() = default就可以使用带参构造和默认构造一起用(但是默认构造是将构造交给编译器处理我们应该给成员变量缺省值不给的话成员变量就是随机值))//上面那个也是默认构造也能将默认构造与带参构造一起用只不过没这个好

//v2(v1)
vector(initializer_list<T> il)
{
	reserve(il.size());//这个用参数的size是够用就行没必要多开空间
	for (auto e : il)
	{
		push_back(e);//这里用for循环拷贝不用memcpy拷贝是因为若是拷贝元素不是普通的内置类型是指针/类(深度拷贝)它不会拷贝元素里的所有数据只会拷贝地址没用,所以遇到类/函数模板用范围for/for循环拷贝
	}
}
//这个就是之前讲解的当构造函数时右边有一个某类型的数组时首先我们会将该数组变为initializer_list<类型名>这个盒子调用构造函数将数组里的数据放入对象的数组里面这个类(盒子)是只读的,只能用push_back拷贝不能改盒子里的数据

//v2(v1)
vector(const vector<T>& v)
{
	reserve(v.capacity());//这里开空间用参数的capacity是为了将参数和对象的数据基本相同
	for (auto e : v)//范围for给的对象可以是类可以是数组但是调用的都是它们的数组(实际上调用的是它们的begin(),end())
	{
		push_back(e);//直接用push_bakc一个个拷贝方便for循环不方便
	}
}

void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

//v1 = v7
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;//这里返回的是vector<T>&类型是因为要支持链式赋值重载(v1 = v2 = v3)(返回类型为void的时候支持不了一行内只可以完成一次赋值重载)
}

//函数模板
//任意类型容器迭代器初始化
template <class InputIterator>
vector(InputIterator first, InputIterator last)//这里是类模板里的函数模板目的是用构造函数实现数组内容拷贝(调用的两个参数类型要相同并且要与函数内容相匹配)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

//vector(iterator first,iterator last)//这些都是调用构造函数要在函数实例化(定义新的对象)时才可以使用
//{
//	while (first != last)
//	{
//		push_back(*first);
//		++first;
//	}
//}

vector(int n, T val = T())//这个是刚开始设置n个元素元素值都为val
{
	resize(n, val);
}

vector(size_t n, T val = T())//这个T之类的话就是默认对象是内置类型的话就是0
{
	resize(n, val);
}

~vector()
{
	delete[] _start;
	_start = _finish = _endofstorage = nullptr;
}  

这里是访问限定符public里的构造函数

c 复制代码
/*vector()
{}*/

//强制编译器生成默认构造
vector() = default;//有了其它构造就不能使用默认构造,加了类名() = default就可以使用带参构造和默认构造一起用(但是默认构造是将构造交给编译器处理我们应该给成员变量缺省值不给的话成员变量就是随机值))//上面那个也是默认构造也能将默认构造与带参构造一起用只不过没这个好

我们知道在一般情况下有构造函数就不会有默认构造

所以我们在构造函数上加一个 vector() = default;默认构造与构造函数就可以共存但是我们写默认构造时vector里的成员变量都要加缺省值nullptr防止读取、销毁随机指针导致报错

c 复制代码
//v2(v1)
vector(initializer_list<T> il)
{
	reserve(il.size());//这个用参数的size是够用就行没必要多开空间
	for (auto e : il)
	{
		push_back(e);//这里用for循环拷贝不用memcpy拷贝是因为若是拷贝元素不是普通的内置类型是指针/类(深度拷贝)它不会拷贝元素里的所有数据只会拷贝地址没用,所以遇到类/函数模板用范围for/for循环拷贝
	}
}

这里的构造函数就是我们之前调用时的一种构造函数(vector v = {1,2,3,4,5,5,6})

它的意思是先将{变量,变量,变量,变量,变量}变成initializer_list<类型名>类型的对象(它是一种盒子我们也可以把它看成数组)然后先扩容再将对象遍历范围for将数据push_back放入vector的对象里

简单来讲就是vector v = {1,2,3,4,5,5,6}调用了构造函数

c 复制代码
vector(initializer_list<T> il)
{
	reserve(il.size());//这个用参数的size是够用就行没必要多开空间
	for (auto e : il)
	{
		push_back(e);//这里用for循环拷贝不用memcpy拷贝是因为若是拷贝元素不是普通的内置类型是指针/类(深度拷贝)它不会拷贝元素里的所有数据只会拷贝地址没用,所以遇到类/函数模板用范围for/for循环拷贝
	}
}

先扩容将initializer_liss类的对象中的数据通过范围for遍历一个个push_back放入vector对象里

这个push_bakc之后会讲意思就是将数据放入最后一个数据后面没有数据就放在第一个


c 复制代码
vector(initializer_list<T> il)
{
	reserve(il.size());//这个用参数的size是够用就行没必要多开空间
	for (auto e : il)
	{
		push_back(e);//这里用for循环拷贝不用memcpy拷贝是因为若是拷贝元素不是普通的内置类型是指针/类(深度拷贝)它不会拷贝元素里的所有数据只会拷贝地址没用,所以遇到类/函数模板用范围for/for循环拷贝
	}
}

这个是拷贝构造也是先扩容再将vector对象的数据放入新的对象里

这里扩容reverse后面会解释


c 复制代码
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

这里的swap函数里面有三个std里的swap将成员变量交换从而实现交换

std里的swap头文件是#include #include嵌套了utility


c 复制代码
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;//这里返回的是vector<T>&类型是因为要支持链式赋值重载(v1 = v2 = v3)(返回类型为void的时候支持不了一行内只可以完成一次赋值重载)
}

这里的赋值重载就是调用了成员函数swap为啥返回该对象是为了实现连续重载

这里我们会问这不是赋值重载吗怎么用交换?

这是因为形参不是引用右边的对象不会有影响纯属只是将右边的数据给了左边


c 复制代码
//函数模板
//任意类型容器迭代器初始化
template <class InputIterator>
vector(InputIterator first, InputIterator last)//这里是类模板里的函数模板目的是用构造函数实现数组内容拷贝(调用的两个参数类型要相同并且要与函数内容相匹配)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

这里我们看到它需要两个迭代器但这里实际上是InputIterator 与 T没啥区别都是什么都可以放但是类型要相同但是我们这里只适用于迭代器方两个同类型的整型解引用会报错所以我们不会用两个int类型或其它的

这里其实也是一种给类赋值


c 复制代码
vector(int n, T val = T())//这个是刚开始设置n个元素元素值都为val
{
	resize(n, val);
}

vector(size_t n, T val = T())//这个T之类的话就是默认对象是内置类型的话就是0
{
	resize(n, val);
}

这两个就是第一个决定对象有效元素个数第二个决定这些有效元素是什么

resize函数后面会讲解设置对象有效元素个数


c 复制代码
~vector()
{
	delete[] _start;
	_start = _finish = _endofstorage = nullptr;
}

最后这个析构函数就是先将_start地址销毁(从 _ start销毁了 _finish _endofstarage就不用delete )

然后将三个指针置为nullptr即可


1.2普通成员函数

c 复制代码
void reserve(size_t n)
	//这里的扩容是异地扩容(在C语言里的realloc有原地扩也有异地扩)
{
	//只有当n > capacity才需要扩容
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		//拷贝旧空间数据到新空间(前提是旧空间地址不为nullpter)
		if (_start)
		{
			for (size_t i = 0; i < old_size; ++i)
			{
				tmp[i] = _start[i];
			}
			//memcpy(tmp, _start, sizeof(T) * old_size);//memcpy和strcpy都在头文件#include<cstring>里是一种C语言的函数这里没有为啥能用memcpy(拷贝指针,被拷贝指针,所需字节个数(memcpy可以拷贝任何类型数组要拷贝多少有效类型的元素取决于我们需要多少字节)))//这里能用的原因是.h文件会写在.cpp文件里.cpp文件里有#include<iostream>在vs中这个头文件嵌套了#include<cstring>所以可以用memcpy
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + old_size;
		_endofstorage = _start + n;
	}
}

vector对象扩容之后地址发生改变

要想扩容首先要判断传入的容量大小是否大于对象的容量大小(capacity()后面解释)

首先得到有效数据个数再向空间申请一个空间空间大小为n(new Tn),然后判断旧空间是否为nullptr不为nullptr就将旧空间的的有效数据(大小old_size)放入tmp里然后将旧空间销毁(delete\[\] _start)更新_start _finish _endofstorage 先更新_start再更新其它指针不然新迭代器会错

所以扩容后迭代器会发生改变所以我们插入insert和erase函数后面会返回pos迭代器因为原来的迭代器已经没用了


文章到此就告一段落,希望对你有所帮助,感谢观看!

相关推荐
垆边人似月.1 小时前
最少的元素个数使和为 target
数据结构·c++·算法
benchmark_cc1 小时前
实时行情系统如何避免无意义的重复请求?从请求去重到缓存设计
大数据·开发语言·python·pandas·量化交易·股票数据·quantdash
田里的水稻1 小时前
Python_两大法宝dir() 与 help() 的妙用
开发语言·python
郝学胜-神的一滴1 小时前
Python浏览器自动化:Selenium与Playwright实战对比,网页爬虫&自动操作一站式指南
开发语言·爬虫·python·selenium·程序人生·自动化
Zip-List1 小时前
工作小记-pytorch的算子优化: 转gpu核函数 python调c
c语言·c++·pytorch·python·cuda
土豆.exe1 小时前
chunked 编码的行尾:Go、h11 与 Tomcat 各错在哪一处
开发语言·网络安全·golang·tomcat
磊 子1 小时前
什么是虚假唤醒(spurious wakeup)?从 POSIX 标准讲到 futex 内核
java·开发语言
暖焰核心2 小时前
多态全解——虚函数、重写、虚表与多态原理
c++
飘然生2 小时前
traceId链路 skywalking java agent
java·开发语言·skywalking