C C++最全Learning C++ No(1),腾讯T3亲自讲解

img
img

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

需要这份系统化的资料的朋友,可以添加戳这里获取

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

void Function()
{ 
	T x = T();//这个就直接用一个int类型来辅助理解就行(int x = int();) 可以看出此时的int();就是我们之前学的匿名对象;其中int 空表示的就是匿名对象,而int()后面的括号其实表示的是,此时拿着这个int空,匿名对象去调用某一个函数,()括号中存放的本就应该是你要调用的这个函数需要的参数
	cout << x << endl;
}
void test\_my\_vector3()
{
	int i = int();//这个的意思就是:使用int(),匿名对象,然后去调用构造函数,然后延长匿名对象的声明周期(把它给给新的对象)
	int j = int(1);//支持这种写法,本质上还是为了支持模板可以使用(也就是上述resize的缺省参数的写法;很好的解决模板初始化问题)
	int\* p = int\*();//指针类型不支持直接把匿名对象给给指针类型
	Function<int>();
	Function<int\*>();
	Function<double>();
}

##### 总而言之:这种写法的发明就是为了可以更好的配合泛型编程中模板的使用,让模板变得更加的无懈可击!


搞定了resize函数,此时我们就来看一下`reserve`函数,如下图:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/37d34a0e1772407688c9eb6565aa88ed.png)  
 这个函数唯一要注意的点就是开辟完新空间之后,进行的一些列的赋值操作,这些赋值操作,一不小心就会出问题,所以要注意。


#### 搞定了扩容问题,此时我们就正式进入,数据的插入,也就是增


头插和尾插我们直接复用任意位置插入,所以这边我们重点学习一下任意位置插入就行,如下图:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/7e906d83782b4b1a972e76469fbaa049.png#pic_center)  
 以上就是一个插入函数的正常代码理解,但是也有不正常理解,我们目前还没有搞定(也就是:**迭代器失效问题**),也就是上述为什么要记录len,为什么要更新pos,为什么insert函数有返回值等一系列奇奇怪怪的写法,在增这个位置我们先不做详细的学习,先了解就行,我们把这个问题集中起来放在博客的最后,一起解决。


### 其次就是删


![在这里插入图片描述](https://img-blog.csdnimg.cn/bdd27737853347118a262b15ef02eab1.png)  
 删除数据明面上是非常的简单的,但是暗地里还涉及了迭代器失效的问题,此时想要解决这个问题和上述一样,还是通过使用返回值的方式来解决,这里不多做学习,下面一起搞定。


### 


## 迭代器失效问题


这个问题是自我实现vector类中最为关键的一个问题,解决这个问题,可以为我们以后理解迭代器打下很好的基础,谁让迭代器是C++大佬发明的一个神器呢?不深入学习不行啊!  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/0bb5cc003ded4de0b7e16169adb2c0ba.png)  
 如上图,和我们之前在string类中学习的迭代器是一样的,在vector类中,我们依然可以把它给抽象理解成是两个指针,一个是头指针,一个是尾指针(指向最后一个数据的后一个位置(类似于string类中的`\0`的位置)),这就是大佬发明的迭代器,分分钟给它搞定(浅显)


#### 认识了迭代器,正式进入迭代器失效问题(浅谈)


1. 首先是迭代器失效的原因,可以发现,我们在上述的插入和删除中都碰到了迭代器失效的问题,所以原因如下:

 **插入是因为扩容(异地扩),所以导致pos自己本身的地址没有改变,但是pos指向的空间发生了改变(异地扩,原空间被delete),pos指针此时指向的空间并不是原来的那个迭代器区间,而是一个内存中的未知空间(经典的野指针问题)。**  
 **删除是因为位置关系会发生改变(本质上是因为地址不连续),所以也会导致迭代器失效问题**
2. 其次是迭代器失效的解决方法,从上述可知,是使用给返回值的方式解决这个问题,结合上述迭代器失效的原因,可以知道,给给返回值的原因  
 **插入函数是因为,返回pos的位置,就是更新pos的位置,让pos指向的空间保持迭代器区间的地址  
 删除使用返回值的原因是,将被删除数据的后一个数据的地址返回给我,这样我依然可以保持迭代器区间地址的连续性,只要能保持连续性,问题就解决了**


感兴趣的同学,可以去看看下面这个链接中的文章:[详细迭代器内容可见该博客](/https://www.cnblogs.com/maluning/p/8570717.html)


并且此时还会涉及到双向迭代器、单向迭代器和随机迭代器(string类中的迭代器就是属于随机迭代器),基本使用原理:



> 
> 一个双向迭代器,此时就不允许传单向迭代器,但是允许传随机迭代器  
>  一个单向迭代器,此时就允许传双向迭代器,也允许传随机迭代器  
>  反正随机就是什么迭代器都可以传,双向不允许传单向
> 
> 
> 


##### 所以上述的迭代器失效问题就是经典的迭代器失效问题中的一个,说明C++中还有非常多的坑在等着我们踩哦!(讨厌!!!)


![在这里插入图片描述](https://img-blog.csdnimg.cn/7efc0196e8fa4635a36340e589295d94.png)  
 上图参考下面这个链接:[迭代器的本质和基本使用](https://bbs.csdn.net/topics/618668825)


### 简单的vector自我实现代码



namespace wwx2
{
template
class my_vector
{
public://可以看出上述是把T给直接定义成了一个value_type,然后使用value_type定义了const指针和普通指针(所以本质上:都只是T类型而已)
typedef T* iterator;//有普通版本的迭代器,此时无论是因为模仿STL还是真的会用到,我们都应该要给一个const类型的迭代器
typedef const T* const_iterator;
typedef T& value_type;
typedef size_t size_type;

	my\_vector()
		:\_start(nullptr), \_finish(nullptr), \_end\_of\_storage(nullptr)//初始化列表初始化
	{}
	iterator begin()
	{
		return _start;
	}
	iterator end()
	{
		return _finish;
	}
	const_iterator begin()const
	{
		return _start;
	}
	const_iterator end()const
	{
		return _finish;
	}
	size_type capacity()const//像这种函数一定要记得加上我们的const
	{
		return _end_of_storage - _start;
	}
	size_type size()const
	{
		return _finish - _start;
	}
	void resize(size_type n, T val = T())//实现了reserve,此时resize复用就行,只是初始化的细节注意一下和size的大小注意一下就行 (所以此时我们就需要多给一个缺省参数,来进行默认的初始化新开辟的空间),就是防止你没有给第二个参数,只给了第一个参数,那此时我也可以把空间初始化
	{//上面那个缺省值的初始化是非常的神奇的(目的:因为此时的vector是一个泛型编程,是没有具体类型的,所以不可以用0来初始化,所以就要用一个模板参数,然后就是通过一个匿名对象的方式去调用默认构造函数)
		if (n < size())//这个不是缩容(这个属于删除数据,因为size是和数据紧紧挂钩的)
		{
			_finish = _start + n;//这个条件在尾插的时候,扩容,是不可能存在缩容删除数据的(唯一可能的就是我们直接调用这个函数,然后进行传参的时候,只有这种情况才有可能导致删除)
		}
		else
		{
			if (n > capacity()) //这个条件就是传说中的只有你比我小,我才扩容 
			{
				reserve(n);//开空间
			}
			while (_finish != _start + n)//加 初始化
			{
				\*_finish = val;//此时已经有空间了,所以就不需要使用定位new的方法,直接赋值就行
				++_finish;
			}
		}
	}
	value_type operator[](size_type pos)//并且此时返回的是一个引用,就是返回别名,目的:节省构造,防止临时变量的常属性
	{
		assert(pos < size());
		return _start[pos];//就是返回这个pos位置在\_statr中的那个下标位置(因为此时是指针,所以准确的来说,应该是一个地址位置)
	}
	const value_type operator[](size_type pos)const//就是多重载一个const类型的,给给const对象使用(萝卜青菜给有所爱)
	{
		assert(pos < size());
		return _start[pos];
	}
	void reserve(size_type n)
	{
		//这边肯定是要涉及深浅拷贝问题的(并且为了防止缩容问题,这边还要进行判断)
		if (n > capacity())
		{
			//使用下面调换两个指针的顺序是可以解决的,但是不是很好,所以我们这边直接先把size()的值给接收起来就行
			size_type sz = size();//这样直接使用sz就行了,不需要再使用size(),也就是不需要再考虑finish和start的位置(随便它去变,跟我都没关系)
			iterator tmp = new T[n];//此时这里不需要考虑加1的问题(只有像string这种,需要存一个\0的这种,我们才需要多开一个空间给给它使用)
			if (_start != nullptr)//此时的new是不需要想malloc一样就行开辟成功和失败的判断的,这个只是为了单纯判断一下\_start中是否有数据
			{
				memcpy(tmp, _start, sizeof(T) \* size());
				delete[]_start;//这个就是经典的防止空间太大太小问题,直接开空间,然后拷贝,然后直接把原空间给删除
			}
			//\_start = tmp;//注意:因为此时是重新开空间,释放旧空间,所以此时的tmp就是我们的start
			//注意:此时下述的size()是不会因为finish和start的地址改变而改变的 (不会因为重复调用而改变),一直都是同一个值,也就导致可以直接使用tmp+size()
			//\_finish = tmp + size(); //如果此时是先把tmp给给\_start,然后再加加size(),此时就会导致finish还是0,而start已经是tmp,然后又因为size就是finish-start,就会导致size是一个负值,也就是-start,然后再用start+size,那么刚好就是0,最后赋值给finish,此时finish就还是0,所以就会导致后面push\_back的时候,对空指针解引用的问题,所以此时为了解决这个问题,此时就不敢直接先给给start

			_start = tmp;//先给给finish,再给给start就行,很好的解决finish为空的问题
			_finish = tmp + sz;//提前记录size的好处
			_end_of_storage = tmp + n;//此时的这个是通过tmp指针的首地址,然后加上16个字节,就是首地址向后走走16个字节,得到的就是此时的容量
		}

	}
	void push\_back(const T& x)
	{//因为使用的是模板类,所以这里的参数类型都是直接给一个T参数类型就可以了
		//从刚刚的STL源码中,我们可以发现的是,它的push\_back使用的是内存池开空间的形式(就是定义new加定义构造)
		//我们这里使用不了,我们就直接使用正常的形式就行(如果想要使用的话,就要在定义模板参数的位置给一个内存池的参数)
		if (_finish == _end_of_storage)
		{
			reserve(capacity() == 0 ? 4 : capacity() \* 2);
		}
		\*_finish = x;//这步就是尾插的经典步骤,上述的判断只是为了防止空间不足而已(但是由于我们的\_end是一个原生指针,所以这里想要直接在尾部赋值,就需要对这个尾部指针进行解引用)
		++_finish;
	}
	void pop\_back()
	{
		assert(!empty());//切记assert给的是真
		--_finish;//这种如果直接用,不检查的话,就会导致删多了的话有越界问题(并且因为此时是迭代器的写法,地址问题),就会导致循环找地址,然后就导致无限打印地址,直到地址匹配到
	}
	iterator insert(iterator pos, const T& val)
	{
		assert(pos >= _start);
		assert(pos <= _finish);//还是那个道理,assert中的条件是真条件
		//并且由于这边,我们使用了\_finish这个位置,就会导致,如果容量刚好等于\_finish的时候,越界,所以这个位置也要进行一个容量的检查
		if (_finish == _end_of_storage)
		{
			size_type len = pos - _start;//先记录(len的作用就是解决迭代器失效问题,目的:更新pos指针)
			reserve(capacity() == 0 ? 4 : capacity() \* 2);
			pos = _start + len;//扩容后再更新(解决迭代器失效问题)
		}
		iterator end = _finish - 1;
		while (end >= pos)//此时因为pos的类型是地址,所以不存在-1变成无符号(size\_t),所以不存在死循环,没有问题
		{
			\*(end + 1) = \*end;//此时的意思就是把最后一个数据赋值给0,然后把刚刚那个位置腾出来,然后循环
			--end;
		}
		\*pos = val;
		++_finish;

		return pos;
	}
	void erase(iterator pos)
	{
		assert(pos >= _start);
		assert(pos < _finish);

		iterator start = pos + 1;
		while (start != _finish)
		{
			\*(start - 1) = \*start;
			++start;
		}
		--_finish;

	}
	bool empty()
	{
		return _start == _finish;//当\_finish--到\_start的时候,就是空,就是不允许的
	}

private:
	iterator _start;//因为STL源码中是直接使用三个原生指针,所以这边我们模仿它,我们也直接使用三个原生指针就行
	iterator _finish;
	iterator _end_of_storage;

};

}


### 测试代码



void test\_my\_vector1()
{
	wwx2::my_vector<int> v;
	v.push\_back(1);
	v.push\_back(2);
	v.push\_back(3);
	v.push\_back(4);
	v.push\_back(5);

	for (size_t i = 0; i < v.size(); ++i)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	my_vector<int>::iterator it = v.begin();//所以本质,我发现,使用三指针的写法,好像就是为了可以更好的使用迭代器(因为迭代器的本质就是地址的加加减减,前提:地址要连续)
	while (it != v.end())//不连续就会导致迭代器失效问题
	{
		cout << \*it << " ";
		++it;
	}
	cout << endl;

	for (auto e : v)//有了迭代器,就有迭代器的小儿子范围for
	{
		cout << e << " ";
	}
	cout << endl;

}
void test\_my\_vector2()
{
	wwx2::my_vector<int> v;
	v.push\_back(1);
	v.push\_back(2);
	v.push\_back(3);
	v.push\_back(4);
	v.push\_back(5);

	v.pop\_back();
	v.pop\_back();

	my_vector<int>::iterator it = v.begin();
	my_vector<int>::const_iterator cit = v.begin();//反正就是萝卜青菜给有所爱,数据类型是什么就调用什么,不怕没有,想要什么就有什么
	while (it != v.end())
	{
		cout << \*it << " ";
		++it;
	}
	cout << endl;
}
template<class T>
void Function()
{ 
	T x = T();//这个就直接用一个int类型来辅助理解就行(int x = int();) 可以看出此时的int();就是我们之前学的匿名对象;其中int 空表示的就是匿名对象,而int()后面的括号其实表示的是,此时拿着这个int空,匿名对象去调用某一个函数,()括号中存放的本就应该是你要调用的这个函数需要的参数
	cout << x << endl;//总而言之:目的就是为了让任意类型都可以调用默认构造,然后任意类型都可以初始化
}
void test\_my\_vector3()
{
	//int i = int();//这个的意思就是:使用int(),匿名对象,然后去调用构造函数,然后延长匿名对象的声明周期(把它给给新的对象)
	//int j = int(1);//支持这种写法,本质上还是为了支持模板可以使用(也就是上述resize的缺省参数的写法;很好的解决模板初始化问题)
	//int\* p = int\*();//指针类型不支持直接把匿名对象给给指针类型
	Function<int>();
	Function<int\*>();
	Function<double>();

img
img

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

需要这份系统化的资料的朋友,可以添加戳这里获取

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

uble>();

[外链图片转存中…(img-xtR4zM3A-1715694203463)]
[外链图片转存中…(img-lp87xz5P-1715694203464)]

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

需要这份系统化的资料的朋友,可以添加戳这里获取

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值