【C++强基篇】学习C++就看这篇--->STL之vector使用及实现

主页:HABUO主页:HABUO

C++入门到精通专栏

如果再也不能见到你,祝你早安,午安,晚安


目录

一、vector的介绍

二、vector的使用 

✨2.1 vector的定义

✨2.2 vector iterator (迭代器)的使用

✨2.3 vector 空间增长问题 

✨2.4 vector 修改

✨2.5  迭代器失效问题

三、vector的简单模拟实现 

四、总结


【C++强基篇】学习C++就看这篇--->STL之vector使用及实现_第1张图片

前言: 

上篇博客我们了解了STL中的string类,本篇博客我们继续对STL中的容器进行学习,这篇博客我们来了解vector,学习思路与string是一样的,第一步就是是什么怎么用,第二步就是我们能不能自己实现一个简单的vector(即为了了解它的底层原理),经过这两步的学习之后,应对绝大多数的场景已经足够使用。经过上篇博客string的学习,vector是相对比较轻松的,因为各种接口的使用和实现逻辑是相差不大的。希望大家有所收获!

一、vector的介绍

核心概念:动态数组 

  • vector 是 STL 中最常用、最基础的序列容器之一。

  • 它本质上是一个动态数组。这意味着:

    • 内存连续: 它的元素在内存中是连续存储的(就像普通数组一样)。这是它最核心的特性,带来了高效的随机访问能力。

    • 动态大小: 它的大小(size())可以在运行时根据需要自动增长或缩减。你不需要像使用原生数组那样预先指定一个固定的大小,或者手动管理内存的重新分配。这是它相对于原生数组的最大优势。

  • 它被定义在  头文件中。

  • 它是一个类模板,你需要指定它要存储的元素类型:

#include 
std::vector myIntVector;       // 存储 int 的 vector
std::vector names;    // 存储 string 的 vector
std::vector objects;      // 存储自定义类 MyClass 对象的 vector

总结

事实上vector就类似于我们数据结构中的顺序表,里面可以存放各种类型的数据,当然同一个vector中只能存放同种类型的数据,string就有点vector中存放字符的韵味,但是它们还是有差别的,例如 '\0'  

二、vector的使用 

类似于string,我们学习使用主要针对的就是,定义:构造(无参的有参的)、拷贝构造。修改:insert、erase、push_back、pop_back、resize、reserve、operator=。输出:operator[]。结束:析构。下面我们;来一一介绍。

✨2.1 vector的定义

构造函数声明 接口说明
vector()(重点) 无参构造
vector(size_type n, const value_type& val = value_type()) 构造并初始化 n 个 val
vector(const vector& x)(重点) 拷贝构造
vector(InputIterator first, InputIterator last) 使用迭代器进行初始化构造
vector v1;
v1.push_back(1);
v1.push_back(2);
vector v2(v1);

✨2.2 vector iterator (迭代器)的使用

接口名称 接口说明
begin + end(重点) begin:获取第一个数据位置的 iterator/const_iterator
rbegin + rend rbegin:获取最后一个数据位置的 reverse_iterator
//迭代器遍历方式
vector::iterator it = v1.begin();
while (it != v1.end())
{
	cout << *it;
	++it;
}
cout << endl;

✨2.3 vector 空间增长问题 

接口名称 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize(重点) 改变 vector 的 size
reserve(重点) 改变 vector 的 capacity
vector v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.resize(4);
v1.resize(6);
v1.resize(8, 2);

这里resize与reserve使用与string没有什么区别,唯一需要注意的就是这里提供了size与capacity的接口,这是因为vector底层私有变量仅有三个变量都是指针,没有像string那样直接提供size与capacity。

补充知识

对于容量部分:

capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。 具体增长多少是根据具体的需求定义 的。vs是PJ版本STL,g++是SGI版本STL。

它们是各有利弊,像1.5倍是空间利用率高了但是效率相对于2倍是更低了。

✨2.4 vector 修改

接口名称 接口说明
push_back(重点) 尾插
pop_back(重点) 尾删
find 查找(注意:这是算法模块实现的,不是 vector 的成员接口)
insert 在 position 之前插入 val
erase 删除 position 位置的数据
swap 交换两个 vector 的数据空间
operator[](重点) 像数组一样访问
vector v1;
v1.push_back(1);
v1.push_back(2);
v1.pop_back();
for (size_t i = 0; i < v1.size(); ++i)
{
	cout << v1[i];
}
cout << endl;

注意vector没有提供find接口我们需要用到算法当中的find:

vector::iterator pos = find(v1.begin(), v1.end(), 2);//注意这个函数的返回值如果没找到会返回v1.end()即所给范围的最后一个
if (pos != v1.end())
{
    v1.erase(pos);
}

✨2.5  迭代器失效问题

  • 迭代器失效: 重要! 以下操作会使指向 vector 元素的迭代器、指针和引用失效:

    • 任何导致 reallocation 的操作(如 push_back 导致容量不足时、resize 增大超过容量、reserve 小于当前容量等)。

    • 在迭代器指向位置之前进行 insert 或 emplace

    • 对迭代器指向位置或其之后的元素进行 erase

    • 使用失效的迭代器等会导致未定义行为。务必小心操作后迭代器的有效性。

如:

情况1
vector::iterator it = v1.begin();
v1.push_back(5);
v1.push_back(6);//push_back、insert、resize、reserve等只要涉及扩容都有可能导致迭代器失效的问题
while (it != v1.end())//这里就会遇到迭代器失效的问题,本质就是你迭代器已经给了it,但是你再插入数据,是不是就有可能
{					//导致扩容,一经扩容,原来的it所指向的空间还在吗?是不是不在了,你再访问当然就错了
	cout << *it;	//所以当使用迭代器的时候一定要在这之前将数据弄好
	++it;
}
cout << endl;
情况2
//假如我们想删除上述数据中的偶数
vector::iterator it = v1.begin();
while (it != v1.end())
{
	//if (*it % 2 == 0)//可以发现也是报错的,原因就在于,例如删除2时,后面的数据紧接着就往前移,但是我们下面就进行了++
	//	v1.erase(it);//那3还进行了检查吗?当然没有,如果这个也是个偶数,那就直接跳过去了,以此编译器直接检查了出来
	//++it;
	//else
	//	++it;//这样写也是报错的,但是这样写在linux下是不会报错的,而且是正确的,但是一段代码总不能在windows是错误的
	//在linux下是正确的吧,所以这种写法本质就是错误的
	//所以错误的本质原因是迭代器失效:当调用 v1.erase(it) 删除元素时:
	//被删除元素之后的所有元素会向前移动(内存位置改变)
	//it 迭代器立即失效(指向无效内存)
	//后续操作 ++it 或解引用* it 会导致未定义行为(程序崩溃或数据错误)
	//逻辑错误:
	//删除元素后,容器大小减小,但循环仍尝试递增已失效的迭代器
	//若删除最后一个元素,it 会指向 end() 之后的位置,++it 将越界
	if (*it % 2 == 0)
		it = v1.erase(it);//使用 erase() 的返回值更新迭代器。erase(it) 返回指向被删除元素下一位置的有效迭代器。
	else
		++it;
}

简单学习之后大家可以通过以下几道题检验一下自己:

只出现一次的数字i 

只出现一次的数字 II 

只出现一次的数字 III 

杨辉三角 

三、vector的简单模拟实现 

reserve和resize 

		void reserve(int n)
		{
			if (n > capacity())
			{
				size_t sz = size();//这里要注意为什么在这里给了个sz,因为如果下面再调用size()来获取大小,
				T* temp = new T[n];//一旦_start变了,size()函数调用就会有问题,因为外面size本质就是通过两个指针相减来获取的
				if (_start)
				{
					for (size_t i = 0; i < sz; ++i)
					{									
						*(temp + i) = *(_start + i);			
					}
					delete[] _start;
				}
				_start = temp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}
		void resize(size_t n, const T& val = T())//模板参数给缺省值就是这样给的
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
				
			}
		}

insert与erase 

		void insert(iterator pos, const T& n)
		{
			assert(pos <= _finish);
			if (_finish == _endofstorage)
			{
				size_t npos = pos - _start;
				size_t newcapacity = capacity() == 0 ? 2 : 2 * capacity();
				reserve(newcapacity);
				pos = _start + npos;
			}//这里会出现pos失效的问题,原因还是因为开辟了新的空间但是pos却没有进行更新
			iterator end = _finish - 1; 
			while (pos <= end)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = n;
			++_finish;
		}
		iterator erase(iterator pos)
		{
			assert(pos < _finish);
			T* it = pos;
			while (it < _finish)
			{
				*it = *(it + 1);
				++it;
			}
			--_finish;
			return pos;
		}

需要格外注意我们之前string用了memcpy等一系列接口,但是要注意这些接口用是有一定代价的,非常容易导致错误。本质原因就在于,mem一系列的接口是按照字节进行操作的。

如:

int a[10];
memset(a, 0,sizeof(int)* 10);
memset(a,1,sizeof(int)* 10);
memset(a,2,sizeof(int)* 10);

对于第二个,我们希望它给我们设置成10个1,但是是吗?肯定不是因为按字节进行处理

 00000001 00000001 00000001 00000001,这个数是不是一个很大的数,怎么可能是1?

注意小知识:

在C++中,类的成员函数可以访问该类的所有成员,包括私有成员(private),无论这些成员是属于哪个对象。也就是说,在`vector::operator=`这个成员函数内部,不仅可以访问当前对象(即`this`所指的对象)的私有成员,也可以访问同类型其他对象(比如参数`v`)的私有成员。 

更详细的请见本人代码库:

https://gitee.com/hanaobo/c-learningcode/tree/master/vector_simulate 


四、总结

本篇博客我们了解学习了veector。 std::vector 是 C++ STL 中的动态顺序容器,内部以连续内存存储元素,支持随机访问、自动扩容。常用构造:无参、n 个 val、拷贝、迭代器区间。遍历可用迭代器(begin/end、rbegin/rend)或 operator[]。容量接口:size、capacity、empty、reserve(预分配)、resize(调 size)。修改接口:push_back / pop_back、insert / erase(配合迭代器使用)、swap。注意迭代器失效:扩容或 erase 后原迭代器可能悬空,erase 需接收返回值更新迭代器。模拟实现要点:三指针(_start、_finish、_endofstorage);reserve 重新分配并拷贝旧数据;resize 按需填充或截断;insert 检查扩容并重置 pos;erase 移动元素并返回下一位置。


【C++强基篇】学习C++就看这篇--->STL之vector使用及实现_第2张图片

你可能感兴趣的:(C++入门到精通,c++,c语言,开发语言,后端,学习)