c++学习之--- list

目录

​编辑

一、list的定义:

二、list的模拟实现:

1、list的基本框架:

2、list的普通迭代器:

设计思想:

[迭代器的一个特殊需求(c++ 对于重载->的一颗语法糖):](#迭代器的一个特殊需求(c++ 对于重载->的一颗语法糖):)

代码实现:

3、const迭代器(妙用模板)

面临的问题:

装模做样的分析:

​编辑

解决方案:

4,迭代器区间初始化

,三、模拟实现里的注意点:

1、front和back函数的返回值

2、拷贝构造函数实现的注意点:

四、细枝末节:

1、begin()和end()函数对于const的依赖问题:

2,初始化列表初始化:

3、typename:消解程序员和编译器的视角错位(PrintConstainer函数作为例子)


一、list的定义:

list双向带头循环链表,结构复杂,但是用起来嘎嘎嘎香.

二、list的模拟实现:

1、list的基本框架:

一个链表类,负责对链表进行各种操作,同时用来初始化链表的哨兵位头节点。

一个链表节点类,负责维护链表节点的结构。

一个链表类里的push_back函数,用来做最基本的插入数据。

复制代码
//链表节点类
template<class T>
struct listNode
{
	listNode(const T& val = T())
	{
		_val = val;
	}
	//成员变量
private:
	T _val = 0;
	listNode* _next = nullptr;
	listNode* _prev = nullptr;
};
--------------------------------------------
//链表类
template<class T>
class list
{
public:
	using Node = listNode<T>;
	//构造(初始化哨兵位的头节点)
	list()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
		_size = 0;
	}
	//尾插
	void push_back(const T& val)
	{
		Node* newNode = new Node(val);
		Node* lastNode = _head->_prev;
		lastNode->_next = newNode;
		newNode->_prev = lastNode;
		newNode->_next = _head;
		_head->_prev = newNode;
		_size += 1;
	}

private:
	Node* _head;
	size_t _size;
};

2、list的普通迭代器:

设计思想:

有了上述list的基本框架后,我们最起码还得遍历list来访问数据,因此得实现迭代器

但这里得细细斟酌一下,和string以及vector不同,list底层数据的地址空间并非连续 ,不能直接封装原生指针,因此我们在实现时需要借助运算符重载的方式来达成我们想要的效果!!!

迭代器的一个特殊需求(c++ 对于重载->的一颗语法糖):

正常情况下,对于迭代器对象,实现一个 * 的重载即可获得相应的数据。可是,倘若数据类型是有多个元素的自定义类型呢???

于是乎,我们可以为list的迭代器重载一个->操作符

复制代码
T* operator->()
{
	return &(_self->_val); //返回元素的地址
}

接下来,便利访问list数据的形式就变成了下面这样:

代码实现:

复制代码
template<class T>
struct list_iterator
{
	using Node = listNode<T>;
	using self = list_iterator<T>;
	Node* _self;
	//构造
	list_iterator(Node* point)
	{
		_self = point;
	}
	//重载各种操作符
	T& operator*()
	{
		return _self->_val;
	}
	self operator++()
	{
		return _self = _self->_next;
	}
	self operator++(int a)
	{
		self ret = _self;
		_self = _self->_next;
		return ret;
	}
	self operator--()
	{
		return _self = _self->_next;
	}
	self operator--(int a)
	{
		self ret = _self;
		_self = _self->_next;
		return ret;
	}
	bool operator!=(self right)
	{
		return _self != right._self;
	}
	bool operator==(self right)
	{
		return _self == right._self;
	}
	T* operator->()
	{
		return &(_self->_val);
	}
	
};

3、const迭代器(妙用模板)

面临的问题:

单单是一个普通的迭代器还不够,毕竟至少还有const迭代器的需求 。 可是如何实现呢?最简单的做法肯定是拷贝一份普通迭代器类的代码,然后修改一些细节,但这样还让代码过于冗余!!!于是我们可以借鉴c++标准stl库的实现方法,巧用模板

装模做样的分析:

想想const迭代器和普通的迭代器有啥区别?再大的逻辑上他们是一致的,只不过就是对于值的访问有不同的限制,需要修改的结构其实很少,仅仅涉及返回值的引用和指针的控制,所以重新写一份就太亏了

解决方案:

巧妙地利用模板的参数来实现不同的迭代器

复制代码
//list类里实现const和非const的关键语句
template<class T>
class list
{
public:
	using Node = listNode<T>;
	using iterator = list_iterator<T,T*,T&>;  //传递普通的T*和T&,就是普通迭代器
	using const_iterator = list_iterator<T,const T* , T>; //传递const T*和,T就是被限制的 
                                                          //const迭代器

//list_Iterator类里根据实例化不同的模版参数生成不同性质的迭代器
template<class T , class Ptr, class Ref> //关键的模板参数
struct list_iterator
{
	using Node = listNode<T>;
	using self = list_iterator<T,Ptr,Ref>;
	Node* _self;
	//构造
	list_iterator(Node* point)
	{
		_self = point;
	}
	//重载各种操作符
	Ref operator*()
	{
		return _self->_val;
	}
	Ptr operator->()
	{
		return &(_self->_val);
	}
    //其他操作符重载
    ..........
	
};

4,迭代器区间初始化

下面实现一个用迭代器区间初始化的构造函数,不仅仅可以用list的迭代器,其他符合条件的容器的迭代器也可以。

复制代码
void SetHead()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}
-----------------------------------------------------------
template<class InputIterator>
list(InputIterator begin, InputIterator end)
{
	SetHead();           //list比较特殊,他高度依赖哨兵位头节点来简化各种数据的操作
	while (begin != end)  
	{
		push_back(*begin); 
		begin += 1;
	}
}

这就是体现c++模板优势的高光时刻 ,同时运用了封装的设计思想。

  1. 对于各个容器,只要支持迭代器遍历,无论其底层结构如何,在使用时都是调用beign()和end(),统一了遍历的操作
  2. 由于模板的存在,这里又可以根据具体传入的参数实例化各种容器的迭代器,从而使用其他类型对象的值来初始化目标对象,就很棒。。。
  3. 当然啊虽然模板参数里写的是两个InputIterator , 但是在编译器眼里他只是两个同类型的参数,所以,如果list还存在一个list(int i , int val)的构造函数,一定要写成list(size_t i , int val) , 以此避免编译器的调用歧义(两个InputerIterator起初是不确定的类型,而int i和int val里不仅类型相同,而且又能用现场的整形值,会被编译器优先考虑)

,三、模拟实现里的注意点:

1、front和back函数的返回值

front和back仅仅返回链表头部和尾部的元素,同时又可以通过引用返回来实现读写兼备

但如果是const对象,就有所不同了。

复制代码
T& front()
{
	return _head->_next->_val;
}
const T& front() const 
{
	return _head->_next->_val;
}
T& back()
{
	return _head->_prev->_val;
}
const T& back() const
{
	return _head->_prev->_val;
}

分析一下const版本和非cons版本在此处返回值的不同:

  1. 普通的非const版本不必多说,正常的引用返回,既避免拷贝有实现让外部修改的功能
  2. const版本是在调用函数的是const对象时使用,此时只读,不能修改。可我们还是用了引用返回,只不过加了一个const。原因在于此处的函数访问的是并非局部域的list对象,当前函数调用结束后也不会出现任何对象的析构。因此仍然可以使用引用返回来提高效率,同时加上const避免被外部修改。

|--------|-----------------------------------------------------------------|----------------------------------------------------------------|
| | 传值返回 | 引用返回 |
| 编译器的动作 | 编译器通过寄存器保存一份返回值的拷贝,然后销毁这个返回值,接着把寄存器里的值赋值给外部对象 | 编译器直接外部接受返回值的对象作为这个返回值的引用 |
| 特点 | 需要产生拷贝,影响效率 | 不产生拷贝,效率高 |
| 禁忌 | 返回值不涉及动态资源开辟时,编译器默认的浅拷贝没问题 如果返回值是类类型且涉及动态资源开辟时,需要为类类型实现深拷贝的拷贝构造 | 如果函数内部的用于返回的对象涉及动态资源开辟,会导致析构两次(内部函数调用结束时一次,外部接受对象生命周期结束时再调用一次) |

2、拷贝构造函数实现的注意点:

这里挺简单的,之前string和vecotr里提过很多次了,下面只说说我出错的地方(第二次出错了哈哈哈哈)

复制代码
//拷贝构造
list(const list<T>& li)
{
	SetHead();
	/*for (auto& au : li)
	{
		push_back(au);
	}*/
	list<T> tmp(li.begin(),li.end());
	swap(tmp);
}

四、细枝末节:

1、begin()和end()函数对于const的依赖问题:

对于获取迭代器的接口,通常有下图两种。分别是普通迭代器和const迭代器。

正常情况下来说begin,cbegin和end,cend已经够用了,如下图:

复制代码
list<int> li = {4,3,6,2,15,4};

//1,普通迭代器的使用 , 用到了iterator以及begin()接口end()
list<int>::iterator it = li.begin();
while(it != li.end());
{
    cout << *it <<" ";
}
//2,const迭代器的使用, 用到了const_iterator以及cbegin()接口cend()
list<int>::cosnt_iterator it = li.cbegin();
while(it != li.cend());
{
    cout << *it <<" ";
}

可是如果对象本身是const呢,如下图。

复制代码
//li是一个const对象
const list<int> li = {4,3,6,9,12,3};
//直接运行会报错
list<A>::const_iterator it = li.rbegin();
while (it != li.rend())
{
	cout << it->_a1 << " " << it->_a2; //现在的方式,简洁清爽!!!
	//cout << (*it)._a1 << " " << (*it)._a2;//刚才的方式
	++it;
}

这是因为,此时list对象 li 本身就是一个const类型,需要有对应的const版本的函数来与之匹配。其实这和迭代器是不是const没有直接关系,只是这里对象的const和迭代器的const可能引起混淆(没错,晕的人是我哈哈哈哈哈)。解决方法也简单,加两个const版本的函数即可

总结 :其实很多函数都可以加上const版本的,毕竟:const函数不仅可以接受const对象,也可以接受普通对象的

2,初始化列表初始化:

在上面探讨const函数的实例里我用到了像下面这样的初始化方式,是不是很方便和直观,起始这是c++11引入的新语法:列表初始化 。注意,不是构造函数的初始化列表!!!

复制代码
const list<int> li = {4,3,6,9,12,3};

初始化列表是一个类,存在于<initializer_list>头文件里,不过很多时候被其他头文件间接包含了。

这个类在底层维护了一块类似于数组的空间,通过两个成员变量指针来指向这块空间的起始和末尾。

由于这个类底层的物理空间是连续的,所以很自然的也支持了迭代器区间遍历,即begin()和end()这样的接口。

话不多说,接下来就整一个我们自己的list的相应的构造函数

复制代码
list(initializer_list<T> obj)
{
	SetHead(); //别忘了在插入之前先建立哨兵位头结点,咱的push_back简单的逻辑全靠它!!!
	for (auto au : obj) //initializer_list对象支持迭代器,所以直接范围for
	{
		push_back(au);
	}
}

3、typename:消解程序员和编译器的视角错位(PrintConstainer函数作为例子)

下面以一个通用的容器内容打印函数PrintContainer来演示:

复制代码
template<class Container> //container可以是vector、list等等。。。

void PrintContainer(const Container& con)
{
	typename Container::iterator it = con.begin(); //这里的typename很关键哦
	while (it != con.end())
	{
		cout << *it <<" ";
		it++;
	}
	cout << endl;
}

理解这里的关键在于认识到模板参数的灵活性------它可以是普通的类型int、double,也可以是类类型vector、list。看下图的逆向思考:

总结:这里段关键在于认识到程序员和编译器之间存在的"知识诅咒"

  1. 程序员视角:程序员知道 Container这个单词代表容器,也就是类类型;
  2. 编译器视角:但是编译器的眼里Container只是一个普通的模板参数变量,之后尽管可能正确的实例化出list<int>::iterator ,但同样的也可能错误的实例化出int::iterator , 因此会通过提前报错把这样的可能性扼杀在摇篮里。
  3. 我们能做的,就是通过typename关键字来让编译器放心的知道这是一个类类型。
相关推荐
勇闯逆流河21 分钟前
【数据结构】堆
c语言·数据结构·算法
jjkkzzzz24 分钟前
Linux下的c/c++开发之操作Redis数据库
数据库·c++·redis
985小水博一枚呀1 小时前
【AI大模型学习路线】第二阶段之RAG基础与架构——第七章(【项目实战】基于RAG的PDF文档助手)技术方案与架构设计?
人工智能·学习·语言模型·架构·大模型
pystraf1 小时前
LG P9844 [ICPC 2021 Nanjing R] Paimon Segment Tree Solution
数据结构·c++·算法·线段树·洛谷
Funny-Boy1 小时前
菱形继承原理
c++
hello1114-2 小时前
Redis学习打卡-Day3-分布式ID生成策略、分布式锁
redis·分布式·学习
小Tomkk2 小时前
2025年PMP 学习二十 第13章 项目相关方管理
学习·pmp·项目pmp
Nobkins3 小时前
2021ICPC四川省赛个人补题ABDHKLM
开发语言·数据结构·c++·算法·图论
独行soc3 小时前
2025年渗透测试面试题总结-百度面经(题目+回答)
运维·开发语言·经验分享·学习·面试·渗透测试·php
海棠蚀omo3 小时前
C++笔记-红黑树
开发语言·c++·笔记