C++ STL 容器深度剖析:vector 与 list 的模拟实现及对比

🌈个人主页@ꪔ小林Y

个人专栏《C++小白闯关日记》《C语言小白闯关日记》,《数据结构入门------从原理到实战》,《拾光Linux》

🍀代码信条 :每一行代码都是成长的脚印👣,每一次调试成功都是对坚持的回应

目录

vector

1.vector的使用

vector 是 C++ 标准模板库(STL)中的动态数组容器,它能够自动管理内存,支持随机访问,并在尾部高效地插入和删除元素。其底层结构由三个指针控制内存块:

  • _start:指向内存块的起始位置。
  • _finish:指向当前有效数据的末尾。
  • _end_of_storage:指向已开辟内存块的末尾。

size() 返回 _finish - _startcapacity() 返回 _end_of_storage - _start

下面通过几个测试函数来演示 vector 的常见用法:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
void Print(const vector<int>& v)
{
	// 方式一:下标访问
	for (size_t i = 0;i < v.size();i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	// 方式二:范围 for(C++11)
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	// 方式三:迭代器访问
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
void test_vector1()
{
	vector<int> v1;                 // 默认构造,空 vector
	vector<int> v2(10,1);           // 构造 10 个值为 1 的元素
	vector<int> v3(v2.begin(),v2.end()); // 用迭代器区间构造
	string s1("XXXXXX");
	vector<int> v4(s1.begin(), s1.end()); // 用 string 的迭代器构造(char 隐式转 int)
	vector<int> v5(v3);             // 拷贝构造

	vector<int> v6 = {1,2,3,4,5};   // 用初始化列表构造(C++11)
	vector<int> v7 = {1,2,3,4,5,1,1,1,1};

	Print(v2);
	Print(v4);
	Print(v6);

	auto il = { 10,20,30,1,2,3 };   // initializer_list 对象
	for (auto e : il)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_vector2()
{
	vector<int> v1;
	v1.reserve(100);   // 提前扩容到 100,避免多次扩容带来的开销

	size_t old = v1.capacity();
	cout << v1.capacity() << endl;
	size_t begin = clock();
	for (size_t i = 0;i < 100;i++)
	{
		v1.push_back(i);
		/*if (old != v1.capacity())
		{
			cout << v1.capacity() << endl;
			old = v1.capacity();
		}*/
	}
	size_t end = clock();
	cout << end - begin << endl;   // 统计插入耗时

	vector<int> v2;
	v2.resize(100, 1);   // 调整大小为 100,新元素用 1 填充
	Print(v2);
}
void test_vector3()
{
	vector<int> v1 = { 1,2,3,4,5 };
	v1.push_back(6);   // 尾插
	Print(v1);

	// 头部插入
	v1.insert(v1.begin(),0);
	Print(v1);

	// 指定位置插入
	v1.insert(v1.begin()+3, 0);
	Print(v1);

	// 删除指定位置元素
	v1.erase(v1.begin()+3);
	Print(v1);
}
struct AA
{
	int _a1 = 1;
	int _a2 = 1;
	AA(int a1 = 1, int a2 = 1)
		:_a1(a1)
		, _a2(a2)
	{	}
};
void test_vector4()
{
	AA aa1 = { 0,0 };
	vector<AA> v = { aa1,{1,1},{2,2},{3,3} };//隐式类型转换
	auto it = v.begin();
	while (it != v.end())
	{
		cout << it->_a1 << ":" << it->_a2 << endl;
		++it;
	}
	cout << endl;

	v.push_back(aa1);
	v.emplace_back(aa1);
	//差异
	v.emplace_back(1,1);//可以传AA对象,也可以传构造AA对象的参数
	v.push_back({ 2,2 });//只能传AA对象
}
int main()
{
	test_vector1();
	return 0;
}

要点总结

  • reserve 只扩容不改变 sizeresize 会改变 size 并可能扩容。
  • emplace_back 支持直接传入构造参数,在底层直接构造对象,理论上比 push_back 更高效(省去一次移动/拷贝)。
  • string 的迭代器构造 vector<int> 时,char 会被隐式转换为 int

2.vector迭代器失效问题

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

对于vector可能会导致其迭代器失效的操作有:

  1. 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、 assign、push_back等。
  2. 指定位置元素的删除操作--erase
    erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理
    论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end
    的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素
    时,vs就认为该位置迭代器失效了。
  3. 注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

补充说明erase 返回的是被删除元素的下一个位置的迭代器,因此正确的删除写法是 it = v.erase(it);,这样即使删除后迭代器失效,也能通过返回值重新获得有效迭代器。此外,insert 也会导致插入位置之后的所有迭代器失效(因为可能发生扩容导致空间重新分配),因此使用 insert 后应使用其返回值更新迭代器。

3.例题:杨辉三角

题目要求生成杨辉三角的前 numRows 行,每一行的首尾元素为 1,中间元素等于上一行相邻两元素之和。这里用 vector<vector<int>> 来存储,外层 vector 的每个元素又是一个 vector,表示一行。

cpp 复制代码
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        // 先确定行数,每一行初始化为空 vector
        vv.resize(numRows,vector<int>());
        // 再确定每一行的列数,并将所有元素初始化为 1
        for(size_t i=0;i<numRows;++i)
        {
            vv[i].resize(i+1,1);
        }
        // 从第 3 行开始(下标 2),中间元素等于上一行相邻两元素之和
        for(size_t i=2;i<vv.size();++i)
        {
            for(size_t j=1;j<vv[i].size()-1;++j)
            {
                vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
            }
        }
        return vv;
    }
};

思路解析

  1. resize 外层 vector,确定行数。
  2. 再对每一行 resize,确定列数并全部初始化为 1(首尾自然为 1)。
  3. 从第 3 行开始,用上一行的数据填充中间元素。

4.vector模拟实现

4.1 reserve 的两种实现方式

方式一:使用 memcpy(浅拷贝,有缺陷)

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * sz);
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

缺陷分析memcpy 是逐字节拷贝,属于浅拷贝。对于 int 等内置类型没有问题,但如果 Tstring 等自定义类型,memcpy 只会拷贝指针(即 string 内部的 _str 指针),导致新旧两个 string 指向同一块堆内存。当旧空间被 delete[] 时,string 的析构函数会释放这块内存,新空间中的 string 就变成了野指针,程序崩溃。

方式二:使用赋值或 swap(深拷贝,正确)

赋值更靠谱,对内置类型可以,对自定义类型也可以

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			for (size_t i = 0; i < sz; i++)
			{
				//tmp[i] = _start[i]; // 如果是string,调用string的赋值深拷贝
				std::swap(tmp[i], _start[i]);  // 如果是string,调用string的交换,交换资源指向
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}
		

说明tmp[i] = _start[i] 会调用 string 的赋值运算符,进行深拷贝;而 std::swap(tmp[i], _start[i]) 则交换两个 string 的内部指针,旧空间中的 stringdelete[] 时析构,释放的是原本属于 tmp 的空间(即新空间中的旧数据),而新空间中的 string 拿到了原本属于 _start 的数据。两种方式都能正确完成深拷贝,但 swap 方式效率更高。

4.2 完整实现(vector.h)

cpp 复制代码
//vector.h
#include<assert.h>
namespace Lin
{
	template<class T>
	class vector
	{
	public:
		//typedef T* iteator;
		using iterator = T*;
		using const_iterator = const T*;

		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
		vector()//会走初始化列表
		{}

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

		//析构实现,防止内存泄漏
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}

		//传统写法:开空间,拷贝数据都得自己来
		//v2(v1),拷贝构造
		/*vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (const auto& e : v)
			{
				push_back(e);
			}
		}*/

		//v0=v1=v3
		//v1=v3
		//vector<T>& operator=(const vector<T>& v)
		//{
		//	if (this != &v)
		//	{
		//		clear();
		//		reserve(v.capacity());//若v1空间比v3大,则不用扩容
		//		for (const auto& e : v)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}

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

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

		//函数模板,迭代器不一定是vector迭代器,也可以是其他容器的迭代器
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			//reserve(last-first);
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//现代写法:开空间,拷贝数据借助其他对象
		vector(const vector<T>& v)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}

		//v1=v3
		vector<T>& operator=( vector<T> tmp)
		{
			swap(tmp);
			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);
		}
		void clear()
		{
			_finish = _start;
		}

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

		//判断空间是否足够,不够则扩容
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < sz; i++)
					{
						//tmp[i] = _start[i]; // 如果是string,调用string的赋值深拷贝
						std::swap(tmp[i], _start[i]);  // 如果是string,调用string的交换,交换资源指向
					}
					//memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + 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 capacity() const
		{
			return _end_of_storage-_start;
		}
		size_t size() const
		{
			return _finish - _start;
		}

		T& operator[](size_t i)//针对普通对象,可读可写
		{
			assert(i < size());
			return _start[i];
		}
		const T& operator[](size_t i)const//重载,仅读 
		{
			assert(i < size());
			return _start[i];
		}

		//插入数据
		void push_back(const T& x)
		{
			//满了需要扩容
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finish = x;
			++_finish;
		}

		//删除数据
		void pop_back()
		{
			assert(!empty());
			--_finish;
		}
		//pos位置插入值
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _end_of_storage)//如果满了就扩容
			{
				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;
			return pos;
		}
		//删除pos位置的值
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}
	private:
		iterator _start=nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

设计要点

  • 拷贝构造(现代写法) :先构造一个临时对象 tmp,再调用 swap 交换三个指针,代码简洁且异常安全。
  • 赋值运算符 :参数直接传值 vector<T> tmp,利用拷贝构造生成副本,再 swap,同样简洁安全。
  • insert 返回迭代器 :因为插入可能触发扩容,导致原 pos 失效,所以先记录偏移量 len,扩容后重新计算 pos,最后返回 pos 供外部更新。
  • erase 返回迭代器:返回被删除位置的下一个迭代器,方便外部在循环中安全删除。

4.3 测试代码(test.cpp)

cpp 复制代码
//test.cpp
#include<iostream>
using namespace std;
#include"vector.h"
namespace Lin
{
	void Print(const vector<int>& v)
	{
		/*for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;*/

		for (size_t i = 0;i < v.size();i++)
		{
			//v[0]++;不能这样写
			cout << v[i] << " ";
		}
		cout << endl;
	}
	void test_vector1()
	{

		Lin::vector<int>  v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);

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

		Lin::vector<int>  v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);

		v.insert(v.begin(), 0);
		Print(v);

		auto it = v.begin() + 3;//对于vector而言,insert以后,it失效,不能使用(第一个迭代器失效)
		v.insert(it, 30);
		Print(v);

	}
	void test_vector3()
	{

		Lin::vector<int>  v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);

		v.erase(v.begin());
		Print(v);

		auto it = v.begin() + 2;
		//对于vector而言,erase以后, it失效,不能访问,访问结果未定义
		v.erase(it);
		Print(v);
	}
	void test_vector4()
	{
		Lin::vector<int>  v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

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

		v.resize(20);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector5()
	{
		Lin::vector<int>  v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
		Lin::vector<int> v2(v1);
		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

		//bit::vector<int> v3  ({ 10,20,30,40 });
		Lin::vector<int> v3 = { 10,20,30,40 };//经典浅拷贝
		v1 = v3;//经典浅拷贝
		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		Lin::vector<int> v4 = { 10,1 };
		for (auto e : v4)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	void test_vector6()
	{
		Lin::vector<string>  v1;
		v1.push_back("111111111111");
		v1.push_back("111111111111");
		v1.push_back("111111111111");
		v1.push_back("111111111111");
		v1.push_back("111111111111");
		for (auto& e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

int main()
{
	Lin::test_vector6();


	//C++内置类型定义
	//C++98
	int i = 0;
	int j = int();
	int k = int(1);
	//C++11扩展
	int y = {};
	int t = { 1 };
	int z{};
	return 0;
}

测试说明

  • test_vector1:验证 push_backPrint 和范围 for。
  • test_vector2:验证 insert 后原迭代器失效,需重新获取。
  • test_vector3:验证 erase 后迭代器失效,访问结果未定义。
  • test_vector4:验证 resize 缩小和扩大。
  • test_vector5:验证拷贝构造和赋值运算符(现代写法)。
  • test_vector6:验证 vector<string> 的深拷贝,若 reserve 使用 memcpy 此处会崩溃。

List

本文基于 C++ 标准库中的 std::list,手把手带你模拟实现一个带头结点的双向循环链表 ,并深入理解其迭代器、插入删除等核心机制。建议先掌握 C++ 类与模板的基础知识再阅读本文。

list模拟实现

std::list 的底层结构是带头结点的双向循环链表 。在动手实现之前,我们先定义链表节点结构。节点中除了存储数据的 _data 成员外,还需要 _next_prev 两个指针分别指向后继节点和前驱节点。

cpp 复制代码
template<class T>
struct list_node
{
	list_node<T>* _next;
	list_node<T>* _prev;
	T _data;

	list_node(const T& x = T())
		:_next(nullptr)
		, _prev(nullptr)
		, _data(x)
	{
	}
};

说明:节点构造函数中 _data(x) 使用了拷贝构造 ,因此 T 类型需要支持拷贝构造与默认构造(或传入显式初值)。

构造实现

下面给出 list 类的主体框架,包含默认构造、迭代器区间构造、initializer_list 构造、拷贝构造、赋值运算符重载、析构、clearsize 等基础接口。其中 empty_init 用于创建哨兵位头节点,使链表在空状态下也满足 _head->_next == _head_head->_prev == _head

cpp 复制代码
template<class T>
class list
{
	using Node = list_node<T>;
public:
	using iterator = list_iterator<T, T&>;
	using const_iterator = list_iterator<T, const T&>;

	// using iterator = list_iterator<T>;
	// using const_iterator = list_const_iterator<T>;

	iterator begin()
	{
		return iterator(_head->_next);
	}

	iterator end()
	{
		return iterator(_head);
	}

	const_iterator begin() const
	{
		return const_iterator(_head->_next);
	}

	const_iterator end() const
	{
		return const_iterator(_head);
	}

	void empty_init()//哨兵位
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}

	list()
	{
		empty_init();
	}

	list(initializer_list<T> il)
	{
		empty_init();
		for (auto& e : il)
		{
			push_back(e);
		}
	}

	template <class InputIterator>
	list(InputIterator first, InputIterator last)
	{
		empty_init();
		while (first != last)
		{
			push_back(*first);
			++first;
		}
	}

	list(size_t n, T val = T())//n个val的构造
	{
		empty_init();
		for (size_t i = 0; i < n; ++i)
		{
			push_back(val);
		}
	}

	list(int n, T val = T())
	{
		empty_init();
		for (int i = 0; i < n; ++i)
		{
			push_back(val);
		}
	}

	~list()//析构
	{
		clear();

		delete _head;
		_head = nullptr;
		_size = 0;
	}

	// 传统写法
	// lt2(lt1),构造
	list(const list<T>& lt)
	{
		empty_init();
		for (auto& e : lt)
		{
			push_back(e);
		}
	}

	// lt1 = lt3;
	list<T>& operator=(const list<T>& lt)
	{
		if (this != &lt)
		{
			clear();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		return *this;
	}

	void swap(list<T>& lt)
	{
		std::swap(_head, lt._head);
		std::swap(_size, lt._size);
	}

	void clear()
	{
		iterator it = begin();
		while (it != end())
		{
			it = erase(it);
		}
	}

	size_t size() const
	{
		return _size;
	}

private:
	Node* _head;
	size_t _size = 0;
};

说明:

  1. begin() 返回第一个有效节点的迭代器,end() 返回哨兵位头节点的迭代器,因此 end() 指向的位置是最后一个有效节点的下一个位置 ,符合左闭右开区间 [begin, end) 的语义。
  2. 同时提供 iteratorconst_iterator 两个类型别名,二者通过模板参数 Ref 区分,const_iterator 解引用后返回 const T&,从而禁止通过 const 迭代器修改元素。
  3. 构造函数同时提供 size_tint 两个重载,是为了避免 list<int> lt(5, 1) 这类调用与迭代器区间构造产生二义性。
  4. clear() 中通过 erase 逐个删除节点,erase 返回下一个有效位置的迭代器,从而保证循环安全推进。

list迭代器实现

由于 list 的底层是链表,节点在内存中不连续 ,因此不能像 vector 那样直接使用原生指针作为迭代器。我们需要对节点指针进行封装,重载 operator*operator++operator--operator==operator!= 等运算符,使其行为与指针一致。大家可暂时将迭代器理解成一个指针,该指针指向 list 中的某个节点。大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

cpp 复制代码
//迭代器实现
template<class T, class Ref>
struct list_iterator
{
	using Self = list_iterator<T, Ref>;
	using Node = list_node<T>;
	Node* _node;

	list_iterator(Node* node)
		:_node(node)
	{
	}

	// *it = 1
	Ref operator*()
	{
		return _node->_data;
	}

	// ++it,迭代器++
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;
		return tmp;
	}

	// --it
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	//判断两个迭代器相不相等
	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}

	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}
};

说明:

  1. 模板参数 Ref 用于区分普通迭代器与 const 迭代器:当 Ref = T& 时,operator* 返回可修改的引用;当 Ref = const T& 时,返回只读引用,从而禁止通过 const 迭代器修改元素。
  2. operator++operator-- 都提供了前置与后置两个版本,后置版本通过返回临时对象来区分,符合 C++ 迭代器规范。
  3. 迭代器内部只保存一个 _node 指针,比较两个迭代器是否相等,本质是比较它们指向的节点地址是否相同。

erase实现

erase 用于删除 pos 位置的节点。删除后,pos 迭代器指向的节点已被释放,继续使用该迭代器属于未定义行为 ,即所谓的「迭代器失效」。因此 erase 需要返回被删除节点的下一个有效位置的迭代器,方便调用方安全地继续遍历。

cpp 复制代码
iterator erase(iterator pos)//迭代器失效
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	// 将前驱与后继直接相连,跳过当前节点
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	--_size;

	// 返回下一个有效位置的迭代器
	return next;
}

说明:删除节点后,pos 本身已失效,但 next 指向的节点仍然有效,因此返回 next 可让 clear() 等场景安全地继续循环。

insert实现

insert 用于在 pos 位置之前插入一个新节点。与 vector 不同,list 的插入不需要搬移元素 ,只需修改相邻节点的指针即可,因此时间复杂度为 O(1)。

cpp 复制代码
	void insert(iterator pos, const T& x)
	{
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* newnode = new Node(x);

		// prev newnode cur
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = cur;
		cur->_prev = newnode;

		++_size;
	}

push_back模拟实现

cpp 复制代码
//尾插
void push_back(const T& x)
{
	Node* newnode = new Node(x);
	Node* tail = _head->_prev;
	tail->_next = newnode;
	newnode->_prev = tail;
	newnode->_next = _head;
	_head->_prev = newnode;
}

实现insert和erase之后直接一复用即可

cpp 复制代码
void push_back(const T& x)//尾插
{
	insert(end(), x);
}

void push_front(const T& x)//头插
{
	insert(begin(), x);
}

void pop_back()//尾删
{
	erase(--end());
}

void pop_front()//头删
{
	erase(begin());
}

测试

cpp 复制代码
#include<iostream>
using namespace std;
#include"List.h"

void test_list1()
{
	bit::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	bit::list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//支持迭代器就支持范围for
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_list2()
{
	bit::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_front(-1);
	lt.push_front(-2);

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

	lt.pop_back();
	lt.pop_back();
	lt.pop_front();
	lt.pop_front();

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

	cout << lt.size() << endl;
}

void Print(const bit::list<int>& lt)
{
	bit::list<int>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it = 1;//const迭代器的特点就是不能给常量赋值
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test_list3()
{
	bit::list<int> lt1 = { 1,2,3,4,5,6 };
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;

	bit::list<int> lt2(lt1);
	for (auto e : lt2)
	{
		cout << e << " ";
	}
	cout << endl;

	bit::list<int> lt3 = { 10,20,30 };
	lt1 = lt3;
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;

	Print(lt1);
}

int main()
{
	test_list3();
	return 0;
}

list和vector的区别

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:

vector list
底层访问 动态顺序表,一段连续空间 带头结点的双向循环链表
随机访问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素效率O(N)
插入和删除 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器 原生态指针 对原生态指针(节点指针)进行封装
迭代器失效 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问

🎊到这儿,《C++ STL 容器深度剖析:vector 与 list 的模拟实现及对比》的内容就结束了。如果文中有表述不准的地方,或是你有更清晰的理解思路,强烈欢迎在评论区留言交流------ 技术路上多碰撞,才能更快进步

觉得内容对你有帮助的话,别忘了点赞❤️➕收藏🌟,方便后续回顾复习;想跟着一起系统学习数据结构的朋友,也可以点击关注,下一期我们会聚焦更进一步的学习。不见不散✌️

相关推荐
lsylalalala1 小时前
多线程(4)
java·开发语言·多线程
小白不想画工图1 小时前
银河麒麟V10系统安装QT5.12
linux·开发语言·qt·银河麒麟
IT_Octopus2 小时前
JSON 日志里的 `{“$ref“:“$.xxx“}`:从 fastjson 兼容包到原生 fastjson2 的迁移实录
开发语言·python·json
geovindu2 小时前
python:HandWriting Recognition using paddlepaddle
开发语言·后端·python·paddlepaddle
mengzhi啊2 小时前
QEventLoop loop配合loop.exec();替代while(1)。电脑cpu占用为0,使用while(1)cpu占用率100%
c++·qt
HugoStudio_SWAN2 小时前
洛谷 P1319 / P1320 压缩技术——同一枚硬币的正反面
c++·学习·程序人生·算法
wuyk5552 小时前
Python 零基础入门第九章:用户输入与 While 循环
开发语言·python
兮动人2 小时前
Python变量与常量
开发语言·python·机器学习·python变量与常量
风筝在晴天搁浅3 小时前
解题代码清爽版
java·开发语言