C C++最全【C++】vector的模拟实现不会怎么办?看过来,2024年最新2024年互联网大厂C C++面经总结

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

请添加图片描述

本文章按照逻辑链来完成vector的模拟实现,重点介绍迭代器失效问题,文末赋上源码.h

开始前,翻阅源码得知:

template<class T, class Alloc = alloc>
class vector 
{
public:
    typedef T\* iterator;
private:
    iterator start;
    iterator finish;
    iterator end_of_storage;
};

其实类似于我们之前的_a_size_capacity ~

在这里插入图片描述

_size = _finish - _start
_capacity = _end_of_storage - _start

一. 构造和析构

  • 无参构造
  • 迭代器区间构造(后面实现)

类似顺序表,初始化都给空,不然push_back就会崩

//无参构造
vector()
	:\_start(nullptr)
	, \_finish(nullptr)
	, \_end\_of\_storage(nullptr)
{}

🧡析构函数

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

二. 一系列基本接口

🌈size & capacity
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}

		size_t size() const
		{
			return _finish - _start;
		}

🌈[]

_start 原生指针,可以用下标访问

		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
			assert(pos < size());
			return _start[pos];
		}

🌈iterator迭代器

普通迭代器 & const迭代器,老生常谈的问题

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

迭代器支持, 范围for同样也能实现

三、尾插尾删

插入数据要考虑扩容问题

reserve & resize

🌊resize
在这里插入图片描述

		void resize(size_t n, const T& val = T() )//0初始化语法
		{
			if (n > capacity())
			{
				reserve(n);
			}
			if (n > size())
			{
				//初始化填值
				while (_finish < _start + n)
				{
					\*finish = val;
					++_finish;
				}
			}
			else
			{
				_finish = _start + n;
			}
		}

注意:

  • 匿名对象具有常性,不可更改,所以加const
  • resize默认填充的是T类型的缺省值,int类型就是0;指针类型就是空指针;若是自定义类型,比如vector,那么就是调用的vector的构造函数,构造vector的匿名对象,是零初始化的语法, x默认值为对应类型的0值
  • C++为了迎合模板需要,内置类型int等被认为有构造函数
    在这里插入图片描述

🌊reserve
同样需要深拷贝,mencpy是浅拷贝

    void reserve(size_t n)
    {
        if (n > capacity())
        {
            T\* tmp = new T[n];
            size_t sz = size();
            //若原来已有空间,则需要释放旧空间、拷贝数据
            if (_start)
            {
                //memcpy(tmp, \_start, sizeof(T)\*sz);
                //delete[] \_start;
                
                for (size_t i = 0; i < sz; i++)
                {
                    tmp[i] = _start[i];
                }
                delete[] _start;
            }
            _start = tmp;
            _finish = _start + sz;
            _endofstorage = _start + n;
        }
    }

注意:

  • 更新_finish的时候,要提前把sz保存起来
// 错误示范,请勿模仿
_start = tmp;
_finish = _start + size();
_endofstorage = _start + n;

  • 因为这样的话,size()是_finish - _start(0),况且_start已经更新了,减完有可能就是负数了

在这里插入图片描述

拷贝数据不能简单地memcpy ,这样虽然vector是深拷贝的,比如vector<vector<int>>,其中的vector<int>仍然是浅拷贝的。对于int没有问题,但是对于自定义类型就出问题了。因此我们需要更改 ——

  • 若T是int,一个一个拷贝,没问题;
  • 如果是自定义类型,一个个的拷贝,就要调用深拷贝赋值了
push_back & pop_back

⚡push_back

    //1、&引用防止深拷贝 2、+const可以右值引用,防止不能引用是常性的临时变量
    void push\_back(const T& x)
    {
        if (_finish == _end_of_storage)
        {
            reserve(capacity() == 0 ? 4 :capacity()\*2);
        }
        \*_finish = x;
        _finish++;
    }

⚡pop_back

    void pop\_back()
    {
        assert(_finish > _start);
        _finish--;
    }

四、构造 & 拷贝构造 & 赋值重载

💦迭代器区间构造

	//写成模板 :可以用其他迭代器区间进行构造 
	template <class InputIterator>
	vector(InputIterator first, InputIterator last)
		:\_start(nullptr)
		,\_finish(nullptr)
		,\_end\_of\_storage(nullptr)
	{
		while(first != last)
		{
			push\_back(\*first);
			++first;
		}
	}

💢注意

  1. 一个类模板的成员函数又可以是一个函数模板,这样既可以用自身类型的迭代器,可以用其他任意迭代器区间进行构造 ,只要这个迭代器解引用的数据能和T匹配。
  2. 一定要记住初始化,没有初始化就是随机值,出现野指针。扩容时会拷贝数据释放空间,就崩溃了
  3. 关于InputIterator的含义:函数模板的模板参数传迭代器区间是有命名规范
    迭代器分为四类,来访问容器——
    在这里插入图片描述

这个后面会讲解到

💦拷贝构造——多种写法

在这里插入图片描述

	void swap(const T& v)
	{
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_end_of_storage, v._end_of_storage);
	}
		
	//现代写法2 : v2(v1) 资本家思维
	vector(const vector<T>& v)
		:\_start(nullptr)
		, \_finish(nullptr)
		, \_end\_of\_storage(nullptr)
	{
		vector<T> tmp(v.begin(), v.end());//打工人
		swap(tmp);//交换
	}

  • 初始化的时候必须置nullptr,不然free的就是一块随机值的空间了

💦赋值重载——现代写法

  • 复用拷贝构造
  • v是局部变量,出作用域也会析构,相当于你叫外卖小哥帮你丢垃圾
		//赋值重载 v1 = v2
		vector<T> operator= (vector<T> v)
		{
			swap(v);//
			return \*this;
		}

这里是传值调用,v是v2拷贝构造来的。到这里我都忘了为什么拷贝构造不能传值必须传引用传送门🎉🎉:类和对象,因为拷贝构造要传参,传参完又是拷贝构造,会无限递归

五、迭代器失效问题

🎨insert

🐋检查空间 —— 挪动数据 ——插入数据。 测试代码,发现insert插入第5个数据(即扩容时),出现了随机值

在这里插入图片描述

        //错误示范
		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= finish);//=就是尾插

			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() \* 2);
			}

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

测试代码 ——

void test\_vector2()
	{
		vector<int> v;
		v.push\_back(1);
		v.push\_back(2);
		v.push\_back(3);
		v.push\_back(4);

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

		auto p = find(v.begin(), v.end(), 3);
		if (p != v.end())
		{
			v.insert(p, 30);
		}

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

	}

出现随机值
在这里插入图片描述

因为有一个隐藏的bug:如果insert时发生了扩容,会导致it指向的空间被释放,此时it还指向着已被释放的空间是野指针,这种问题,我们就叫做迭代器失效

在这里插入图片描述
💗于是,我们通过增容后,提前算出相对位置,更新pos来解决。此时外边的p仍然是失效的,则通过传返回值(An iterator that points to the first of the newly inserted elements.),若之后还需要使用p,再根据需要接收返回值(不能传引用,传引用会引发其他问题:返回常量)

🎉最终版:

		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);//=就是尾插

			if (_finish == _end_of_storage)
			{
			    //扩容会导致迭代器失效,需要提前计算更新pos
				size_t len = pos - _start;//提前算出相对位置
				reserve(capacity() == 0 ? 4 : capacity() \* 2);
				pos = _start + len;
			}

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

同样测试得:

	//测试insert - 迭代器失效问题
	void testVector4()
	{
		vector<int> v;
		v.push\_back(1);
		v.push\_back(2);
		v.push\_back(3);
		v.push\_back(4);

		vector<int>::iterator it = find(v.begin(), v.end(), 2);
		if (p != v.end())
		{
		    // 在it位置插入数据后,不要在访问p,因为p可能失效了
			p = v.insert(it, 20);
			// 若insert时发生了扩容,会导致it指向的空间被释放
			// it本质上是野指针,这种问题,我们就叫做迭代器失效
		}
		v.insert(p, 10);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}


在这里插入图片描述

🎨erase

常规思路:挪动数据覆盖。此处没有新开辟空间,也就没有野指针数据

        // 错误示范,请勿模仿
		void erase(iterator pos);
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator begin = pos + 1;
			while (begin < _finish)
			{
				\*(begin - 1) = \*begin;
				++begin;
			}
			--finish;
		}

erase缩容也可能到时pos失效(具体看编译器实现)

现在,要求实现删除v中所有偶数,测试代码

	//测试erase - 迭代器失效问题2
	void test\_vector4()
	{
		vector<int> v;
		v.push\_back(1);
		v.push\_back(2);
		v.push\_back(3);
		v.push\_back(4);

		//要求删除所有的偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if (\*it % 2 == 0)
			{
				v.erase(it);
			}
			++it;
		}

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

通过一系列测试: 发现

1 2 3 4 5 -> 正常(误打误撞) 
1 2 4 5	  -> 没删干净 		  
1 2 3 4	  -> 崩溃 		  

在这里插入图片描述

  • 以1245为例,偶数相连的情况;删除完2后,++it,导致后一个偶数没判断,跳过了4
  • 再分析1234,当erase最后一个偶数时,++it,此时的it_finish擦边而过了,*it就不断越界访问了,肯定崩溃

总结,erase(it)后,it指向的位置意义已经变了,直接++,会导致一些意料之外的结果,这也是迭代器失效的问题。

💦我们翻阅文档得知:函数调用后会返回刚删除位置的下一个位置,即这个意义已经改变的pos(An iterator pointing to the new location of the element that followed the last element erased by the function call. This is the container end if the operation erased the last element in the sequence.)(我网易翻译的应该没错)

💗终极版erase

		//stl规定erase返回删除位置的下一个位置
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator begin = pos + 1;
			while (begin < _finish)
			{
				\*(begin - 1) = \*begin;
				++begin;
			}
			--_finish;
			return pos;//此时移动完pos就是被删的后一个数据
		}

继续测试代码~

	void test\_vector4()
	{
		vector<int> v;
		v.push\_back(1);
		v.push\_back(2);
		v.push\_back(4);
		v.push\_back(5);

		//要求删除所有的偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if (\*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;
			}
		}
		
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

结果正确

在这里插入图片描述

🎨小总结
  1. 迭代器失效主要发生在inserterase中,用了迭代器并改变了底层的数据结构迭代器失效了,就不要再去访问pos位置;一定要更新,若还需要访问,可先接收返回值更新

💢memcpy拷贝问题

假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

int main()
{
   bite::vector<bite::string> v;
   v.push\_back("1111");
   v.push\_back("2222");
   v.push\_back("3333");
   return 0; 
 }

在这里插入图片描述
在这里插入图片描述
对于内置类型可以用mencpy,对于自定义类型就不能用mencpy了,memcpy是浅拷贝,指向同一块空间,要是析构or一个对象修改数据,就出问题了,就需要一个一个对象进行深拷贝赋值

💢动态二维数组理解

调用函数
在这里插入图片描述

附源码

vector.h
#pragma once
#include<algorithm>
#include <iostream>
#include <vector>
using namespace std;
#include<assert.h>


namespace ljj
{
	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()
			:\_start(nullptr)
			, \_finish(nullptr)
			, \_end\_of\_storage(nullptr)
		{}


		//写成模板 :可以用其他迭代器区间进行构造 
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
			:\_start(nullptr)
			,\_finish(nullptr)
			,\_end\_of\_storage(nullptr)
		{
			while(first != last)
			{
				push\_back(\*first);
				++first;
			}
		}

		//拷贝构造 ~ 传统写法
		//v1(v)
		//vector(const vector<int>& v)
		//{
		// \_start = new T[v.size()];
		// //memcpy(\_start, v.\_start, sizeof(T) \* v.size());//有坑
		// for (size\_t i = 0; i < v.size(); ++i)
		// {
		// \_start[i] = v.start[i];
		// }


![img](https://img-blog.csdnimg.cn/img_convert/cf8389a7c4ecf236ba305f089e0041c4.png)
![img](https://img-blog.csdnimg.cn/img_convert/51e5f5443068023f9a68c20994ea2791.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

tIterator>
		vector(InputIterator first, InputIterator last)
			:\_start(nullptr)
			,\_finish(nullptr)
			,\_end\_of\_storage(nullptr)
		{
			while(first != last)
			{
				push\_back(\*first);
				++first;
			}
		}

		//拷贝构造 ~ 传统写法
		//v1(v)
		//vector(const vector<int>& v)
		//{
		// \_start = new T[v.size()];
		// //memcpy(\_start, v.\_start, sizeof(T) \* v.size());//有坑
		// for (size\_t i = 0; i < v.size(); ++i)
		// {
		// \_start[i] = v.start[i];
		// }


[外链图片转存中...(img-5SPbrRZa-1715699791256)]
[外链图片转存中...(img-enVUQySa-1715699791257)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值