C++:vector类(default关键字,迭代器失效)

目录

前言

成员变量结构

iterator定义

size

capacity

empty

clear

swap

[]运算符重载

push_back

pop_back

reserve

resize

构造函数

默认构造函数

default

迭代器构造

拷贝构造函数

赋值重载函数

析构函数

insert

erase

迭代器失效问题

insert失效

erase失效

resize失效

clear失效

总结

完整代码


前言

vector其实就是数据结构中的顺序表,建议先学会顺序表

C数据结构:顺序表-CSDN博客

成员变量结构

cpp 复制代码
template<class T>
class vector
{
public:

private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _end_of_storage = nullptr;
};

这里是使用了iterator迭代器来定义了三个成员变量

_start指向顺序表的开始位置

_finish指向顺序表最后元素的下一个位置

_end_of_storage指向顺序表开辟的最后一个空间

当然我们也可以使用顺序表里那种方法用一个*a,size,capacity来完成vector,但库里是用这三个变量,那么就保持一致好了

那么这个iterator是什么?

iterator定义

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

private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _end_of_storage = nullptr;
};

这个iterator其实本质就和上一节string类的iterator一致,只不过这里是T*

C++:string类(auto+范围for,typeid)-CSDN博客

有iterator就顺带把const_iterator定义了,方便稍后实现const迭代器的函数

既然有了迭代器,那么begin和end也就很容易实现出来了

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

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

size

cpp 复制代码
size_t size() const
{
	return _finish - _start;
}

指针相减是指针之间的偏移量个数,首尾指针相减即是顺序表的大小

capacity

cpp 复制代码
size_t capacity() const
{
	return _end_of_storage - _start;
}

与size同理

empty

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

开始和结束指针指向同一块即为空

clear

cpp 复制代码
void clear()
{
	_finish = _start;
}

swap

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

与string类一致

[]运算符重载

cpp 复制代码
T& operator[](size_t i)
{
	return _start[i];
}

const T& operator[](size_t i) const
{
	return _start[i];
}

这里是实现了两个版本

第一个是vector<T>对象使用,第二个是给const vector<T>对象使用

push_back

cpp 复制代码
void push_back(const T& x)
{
	*_finish = x;
	_finish++;
}

直接在finish指针指向的位置填值即可

但我们哪来的空间给我们放数据?所以这里需要一个扩容逻辑,我们可以封装成与库里一样的函数

cpp 复制代码
void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = x;
	_finish++;
}

当空间满时则需要扩容(reserve)

pop_back

cpp 复制代码
void pop_back()
{
	_finish--;
}

--秒了

reserve

cpp 复制代码
void reserve(size_t n)
{
	if (capacity() < n)
	{
		size_t len = size();
		T* tmp = new T[n];
		memcpy(tmp, _start, sizeof(T) * size);
		delete[] _start;

		_start = tmp;
		_finish = tmp + len;
		_end_of_storage = tmp + n;
	}
}

这里扩容的思路是用tmp指针创建一个大小为n的空间,将旧空间的内容拷贝到新空间中,释放旧空间,然后让_start,_finish,_end_of_storage指向相应的位置

为了记录finish在原空间中的偏移量,我们需要用一个len变量来保存

但是这段代码是有问题的!!!

首先我们要知道memcpy是浅拷贝(值拷贝),不了解的可以先看下面深浅拷贝的章节

C++:类和对象 II(默认成员函数,深浅拷贝)-CSDN博客​​​​​​

这里如果T是int,double这些内置类型就没什么问题

但是如果T是string,vector这些需要再开空间的容器就会出问题

假设T是string类

那么_start每一块空间都是一个string类型,都有一个指针又指向一串字符串

这时候如果用memcpy会发生值拷贝,只会将string里的指针拷贝给tmp新空间中,这个浅拷贝会导致大家都指向一块空间

所以我们需要一个深拷贝,具体实现方法如下:

cpp 复制代码
void reserve(size_t n)
{
	if (capacity() < n)
	{
		size_t len = size();
		T* tmp = new T[n];
		//memcpy(tmp, _start, sizeof(T) * size);
		for (size_t i = 0; i < len; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;

		_start = tmp;
		_finish = tmp + len;
		_end_of_storage = tmp + n;
	}
}

用一个for循环将里面的值赋值给新空间

如果是string,我们会调用string的赋值重载,而string的赋值重载也是深拷贝,所以就完成了

总结:T类型如果是内置类型,则正常赋值,若为自定义类型,调用自定义类型的赋值重载函数

resize

cpp 复制代码
void resize(size_t n, T val = T())
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
}

reserve是扩容,调整的是_end_of_storage,而resize是调整大小,变的是_finish

若resize的值小于容量大小,那么直接调整_finish到相应位置即可

若resize的值大于容量大小,那么不仅需要变_finish,还需要变_end_of_storage扩容

扩容只需要复用reserve即可

构造函数

默认构造函数

cpp 复制代码
vector() = default;

由于我们的成员变量是三个指针,只需要让它们走初始化列表初始化为nullptr即可,系统自动生成的就够用了

default

default关键字用于显式地要求编译器为特殊成员函数生成默认实现

可以用于6个默认成员函数中的任意一个,使用方法都如上一样

迭代器构造

cpp 复制代码
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);	
		first++;
	}
}	

该构造函数可以用其他容器的迭代器来构造自己

所以为了能够接收其他类的迭代器我们需要新用一个模板来接收

让迭代器里的值依次push_back到当前vector中即可

cpp 复制代码
vector(size_t n, const T& val = T())
{
	reserve(n);
	while (n--)
	{
		push_back(val);
	}
}

该构造函数也是扩容+复用push_back与上面相同的逻辑

但是这里有个问题!!

若是我们要调用该函数是这样的:

cpp 复制代码
vector<int> v(5, 1);

它的意思是构造顺序表空间为5值为1

看起来这里会调用到该函数,其实它会调用到上面的迭代器构造函数

这是为什么呢?

分析:

5的类型是int,1的类型也是int,但是我们定义的是size_t和T类型,实例化后就是size_t和int类型

但是这个InputIterator如果实例化也是int就是两个int,显然这个构造会让我们编译器选择,自然就货不对板了

所以我们需要自己写其他类型来构成重载

cpp 复制代码
vector(int n, const T& val = T())
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

vector(unsigned int n, const T& val = T()) 
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

这样无论是int,还是size_t,还是unsigned int都可以正常调用了

拷贝构造函数

cpp 复制代码
vector(const vector<T>& v)
{
	reserve(v.size());
	for (auto& e : v)
	{
		push_back(e);
	}
}

扩容

将v里的数据遍历并复用push_back到当前vector类中

赋值重载函数

写法1:

cpp 复制代码
vector<T>& operator=(const vector<T>& v)
{
	if (this != &v)
	{
		clear();

		reserve(v.size());
		for (auto& e : v)
		{
			push_back(e);
		}
	}
	return *this;
}

与前面的拷贝构造思路一致

写法2:现代写法

cpp 复制代码
vector<T>& operator=(vector<T> v)
{
	swap(v);

	return *this;
}

与string类中的写法思路一致

C++:string类(auto+范围for,typeid)-CSDN博客

析构函数

cpp 复制代码
~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

清理工作:释放空间

insert

cpp 复制代码
iterator insert(iterator pos, const T& x) 
{
	if (_finish == _end_of_storage)
	{
		size_t n = pos - _start; 
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + n;
	}

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

	*pos = x;
	_finish++;
	return pos;
}

首先是扩容逻辑,但这里并不是简单的扩容就可以了,这里的扩容存在迭代器失效问题

首先我们的pos是在旧空间的,若是贸然扩容那么pos的位置就无法在新空间找到了

所以我们需要先用一个变量n来记录pos相对于_start的偏移量

然后只需要重新让pos指向新空间相应位置即可

剩下的就是从后往前挪动数据,最后在pos位置插入x即可,与string的insert逻辑相同

erase

cpp 复制代码
void erase(iterator pos)
{
	iterator cur = pos + 1;
	while (cur != _finish)
	{
		*(cur - 1) = *cur;
		cur++;
	}
	_finish--;
}

挪动覆盖数据即可,与顺序表逻辑一致

迭代器失效问题

insert失效

insert的使用是存在迭代器失效的问题的

cpp 复制代码
int main()
{
	vector<int> v = { 1, 2, 3, 4, 5 };
	auto it = v.begin() + 2;
	v.insert(it, 99);

	for (auto x : v)
		cout << x << " ";
	cout << endl;

	cout << *it;
	
	return 0;
}

在这段代码中,如果在vs的环境下运行是会报错的!

因为我们用it这个迭代器插入后,it这个迭代器就失效了,此时再访问则会有问题,vs的选择是直接报错!

为什么会失效?

迭代器失效问题具体是因为内存管理机制所导致的

当在任意位置插入新元素时,如果当前容量不足以容纳更多元素,会重新分配内存。这会导致所有指向旧内存的迭代器、引用和指针失效。即使容量足够,插入点之后的迭代器也会因为元素移动而失效

所以在使用insert后迭代器一定不能使用!!!

erase失效

erase也存在迭代器失效问题

cpp 复制代码
int main()
{
	vector<int> v = { 1, 2, 4, 5 };
	auto it = v.begin();
	
	for (auto x : v)
	{
		if (x % 2 == 0)
		{
			v.erase(it);
		}
		it++;
	}

	return 0;
}

这是一段删除偶数的代码

这里迭代器失效就体现在连续偶数的情况下

若删除2,此时4会在2的位置,那么it还要++就会跳过这个4,所以这也是一种迭代器失效的体现

总结:当从容器中删除元素时,虽然通常不会导致内存重新分配,但删除点之后的迭代器会因为元素的移动而失效

在vs中运行会直接报错!和insert一样

正确写法:

cpp 复制代码
int main()
{
	std::vector<int> v = { 1, 2, 3,4, 5 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			it++;
		}
	}

	return 0;
}

若进行了删除操作,我们只需要让it指向删除的位置即可,反之it++

resize失效

当使用resize函数改变vector的大小时,如果新大小大于当前容量,vector会重新分配内存,导致所有迭代器、引用和指针失效。如果新大小小于当前大小,则会发生元素删除,删除点之后的迭代器会失效

clear失效

虽然clear函数不会减少vector的容量,但它会删除所有元素,因此所有指向元素的迭代器、引用和指针都会失效

总结

为了避免迭代器失效问题,建议在执行插入、删除、调整大小或清空操作后,重新获取或更新迭代器。例如,insert和erase函数都会返回指向新位置的有效迭代器,可以使用这些返回的迭代器进行后续操作

完整代码

cpp 复制代码
#pragma once

#include<iostream>
#include<vector>
#include<list>
#include<string>
using namespace std;

namespace lyw
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//C++11 前置生成默认构造
		vector() = default;

		vector(const vector<T>& v)
		{
			reserve(v.size());
			for (auto& e : v)
			{
				push_back(e);
			}
		}

		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);	
				first++;
			}
		}	
			
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}

		vector(int n, const T& val = T()) // test6的v7
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		vector(unsigned int n, const T& val = T()) // test6的v7
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		void clear()
		{
			_finish = _start;
		}

		//vector<T>& operator=(const vector<T>& v)
		//{
		//	if (this != &v)
		//	{
		//		clear();

		//		reserve(v.size());
		//		for (auto& e : v)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}

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

		vector<T>& operator=(vector<T> v)
		{
			swap(v);

			return *this;
		}

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

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		void reserve(size_t n)
		{
			if (capacity() < n)
			{
				size_t len = size();
				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * size); 浅拷贝 vector<string>...中会报错
				for (size_t i = 0; i < len; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;

				_start = tmp;
				_finish = tmp + len;
				_end_of_storage = tmp + n;
			}
		}

		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

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

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

		bool empty() const
		{
			return _finish == _start;
		}

		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			*_finish = x;
			_finish++;
		}

		void pop_back()
		{
			_finish--;
		}

		iterator insert(iterator pos, const T& x) // 存在迭代器失效问题
		{
			if (_finish == _end_of_storage)
			{
				size_t n = pos - _start; // 记录偏移量,否则一旦扩容pos的位置就无效了
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + n;
			}

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

			*pos = x;
			_finish++;
			return pos;
		}

		void erase(iterator pos)
		{
			iterator cur = pos + 1;
			while (cur != _finish)
			{
				*(cur - 1) = *cur;
				cur++;
			}
			_finish--;
		}

		T& operator[](size_t i)
		{
			return _start[i];
		}

		const T& operator[](size_t i) const // 为了给const vector<T>对象使用
		{
			return _start[i];
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

相关推荐
我们的五年3 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
kitesxian5 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode
zwjapple10 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five12 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省13 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
一棵开花的树,枝芽无限靠近你27 分钟前
【PPTist】添加PPT模版
前端·学习·编辑器·html
凡人的AI工具箱27 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
做人不要太理性30 分钟前
【C++】深入哈希表核心:从改造到封装,解锁 unordered_set 与 unordered_map 的终极奥义!
c++·哈希算法·散列表·unordered_map·unordered_set
程序员-King.39 分钟前
2、桥接模式
c++·桥接模式
chnming198743 分钟前
STL关联式容器之map
开发语言·c++