C++STL初阶(8):list的简易实现(上)

经过对list的使用的学习,我们模拟STL库中的list,实现自己的list

目录

1.观察库中的链表的实现          

2.list的实现

2.1 基本结构   

2.2 基本接口

3.迭代器(本篇重点)

3.1 迭代器的构建

3.2 联系list

3.3 ListIterator的完善

3.3.1 不需要写析构函数

3.3.2 拷贝构造和赋值运算符

3.3.3  operator-> (双箭头变单箭头)

4.const迭代器

5. list中的insert和erase


1.观察库中的链表的实现          

         与观察库中的vector相同,list依然是只观察最主要的部分即可:  stl_list.h

       (需要代码压缩包的可以评论区留言或者私信我)

首先一上来就是一个结构体模版 , 此处库中将void_pointer(也就是指向自己的指针)设计成一个void* , 所以在之后的使用中必定涉及到强转来使用,我们更建议直接设计成T*

                

然后是一个超长的和iterator有关的结构体模版的定义:

                                        (此处没有截图完整,在压缩包中48~94行)

由此我们可以推测,双向迭代器是一个类封装的,不再是vector中简单的原生指针。

 最后来到链表,通过观察我们可发现,主要成员变量就是一个node:

                                                        

     而node的类型是一个节点的指针                       

初始化:无参构造就是实现一个哨兵位,哨兵位的next和prev都指向自己

                

get_node:通过内存池(为了效率)或得空间

其它插入方法:

push_back   以及   insert

都是将原来的元素往后顺移一个,将希望放入的元素放在position的位置

我们还能观察出,position被当作一个结构体一样,通过.去访问对应的元素。

在学习C语言时期使用的单链表需要单独考虑为空的时候的操作,但是当我们使用带头双向循环链表时就能发现,只有头结点的时候这样操作也是没有问题的。


2.list的实现

2.1 基本结构   

在C语言的实现中,链表中的每一个元素都是通过结构体包含的,我们此处也使用struct而非class,具体原因我们之后再解释。

节点:

由头尾指针,一个数据构成。                     

链表:

             

ListNode<T>这个名字太长了,我们将其typedef一下(库中也是这么实现的)。

类成员只需要一个哨兵位节点的指针,也就是_head即可。

因为通过一个哨兵位的节点就能找到所有的节点

2.2 基本接口

对于现在的list, 编译器自动生成的构造函数就是生成一个指针_head,

但是list的默认构造一定是需要开出一个确切的空间的,否则_head指向的是什么位置呢?

需要我们自己实现默认构造

因此,默认构造就是生成一个哨兵位。调用此函数一定是该链表刚开始被创建的时候,我们应当让此时在堆上开出的哨兵位的next和prev都指向自己

list() {
	_head = new Node();
	_head->_next = _head;//_head是Node类型的变量,我们此处访问他的成员变量
	_head->_prev = _head;//如果Node(也就是ListNode<T>)是一个class
}                        //成员变量默认就会变成私有,会被拒绝访问 

new Node()相当于new了一个ListNode<T> ,  也就是调用了ListNode<T> 的 默认构造

               

复习如何push_back:

先找到当前的tail , 记录此节点的prev , 然后改变tail的next的指针指向,指向新加入的元素。

void push_back(const T& x) {
	Node* tmp = new Node(x);
	Node* pretail = _head->_prev;

	pretail->_next = tmp;
	tmp->_next = _head;
	tmp->_prev = pretail;
	_head->_prev = tmp;

为什么用struct而不是class:

如果我们之前的节点用的是class,写这样一段代码之后在主程序运行会报错(并且不实例化就不会报错,在运行之前编译器不会发现这个错误),原因如下:

                  

1. 模版不会被编译。只有在实例化以后才会编译模版类(因此不会提前报错)

并且不是所有的成员函数都会实例化,调用哪个才实例化哪个(按需实例化),因此其他没有被使用的函数可能会有语法错误,但是不会被发现。只有当使用这个有错误的语法后,错误才会被报出

2. ListNode没有构造函数

因为我们push_back中写了一个new Node(x);

而此时我们没有写这样的传参构造。

★★★我们此时不写push_back是不会报错的,再来梳理一下我们的构造:

只写上图中43行的默认构造的代码是没问题的,我们执行list<int> li1;时,直接调用list()构造,也就是我们刚刚自己写的将_head的头和尾都指向自己的函数

在new Node()的过程中,因为结构体模版是可以实现自动生成默认构造的,所以我们直接不传参即可。

在new出来但是执行26、27行代码之前,_next和_prev都是走的编译器默认生成的结构体中的构造函数,作为指针被赋值为0x00000000000000000000

然后我们执行26 27行代码,将开出来的Node的地址赋给_next和_prev,完成哨兵位节点的创建。list的初始化是和Node的初始化分不开的,两者相互联系。

注意:new Node()和new Node是不一样的,后者不会调用默认构造函数,只是开空间 

3.访问权限问题

原因在于class下默认的都是private,是不能被访问的。

假如我们用class来实现节点:

然后我们又在list的默认构造和push_back下访问了_next和_prev,所以建议用struct,否则:

解决如下:             

template<typename T>
struct ListNode {
	ListNode<T>* _next;
	ListNode<T>* _prev;
    T data;

	ListNode(const T& x = T())
		:_next(nullptr),//一个新创建的节点的next和prev不需要指向任何地方,赋给nullptr即可
		_prev(nullptr),
		_data(x)
	{}
};

或者class加上修饰限定符public也可以:

                   

当一个类需要全部被访问,不希望有private修饰的成员时,cpp多直接用struct


关于哨兵位的初始化:不能用0,因为不确定T到底是int还是string或者vector等。。。。

​​​​​

建议使用匿名构造:

                       

或者全缺省( 反正我们在ListNode<T>里面是加了参数T()的 )

                         


想要观察push_back或者做测试,就需要实现迭代器。 

3.迭代器(本篇重点)

3.1 迭代器的构建

之前都是使用的原生指针T*作为iterator

因为其物理空间都是连续的,所以可以直接用指针来完成迭代器的工作。

而我们刚才由源码中观察得到,链表的指针是封装过的自定义类型

由上一文得,因为我们的iterator是需要能够++和--的,倘若iterator是一个Node* 的话,

原生指针的++--都是加减一个类型的大小,Node*的++--只能跨过一个相同大小的空间去到相邻的一个应该不存在的Node

但是,Node*是有办法找到下一个节点或者上一个节点的,如果我们能让这个iterator的++是去移动到(*iterator)._next对应的那个元素,--是移动到_prev对应的那个元素就好了。

因此,我们的做法是:

自己封装一个iterator,然后重载运算符

为了避免混淆名字,别直接取iterator————因为每一种数据结构都包含iterator

封装Node* , 因为其作为一个类,是可以重载++ 和 --的,这样就达到了我们的目的

        

      然后再在list类内部用typedef规范我们的迭代器即可(将ListIterator的名字改成所有容器统一的iterator)。

typedef ListIterator iterator ;

实现iterator:

先实现框架:

template<typename T>
class ListIterator {//此处是用class还是struct呢?
    
	typedef ListNode<T> Node;
	typedef ListIterator<T> _self;//这里是模仿库中的写法,将自己的名字缩短一点
    Node* _pnode;
public:
	ListIterator(Node* pnode) //之后在list中就可以通过iterator it(li1.begin())等构造
		:_pnode(pnode)// iterator it = li1.begin()也可以,这是隐式类型转换
	{}

	operator++
	operator--	
};

再来实现++(要在参数处加int才是后置加加):

_self& operator++() {
	_pnode = _pnode->_next;
	return *this;
}

 this是指向ListIterator的指针,我们让iterator指向了下一个节点,并且传引用返回了它自己

小练习:

_self operator++() {
	return _self(_pnode->_next);//测试一下能不能跑,并阐述理由
}

 答案是不会编译报错,也能运行,但是这样并没有起到++的作用,而是+1的作用。

这样实现的话,必须要再用this对应的iterator接受这个返回值才行。

同理实现-- 以及 解引用访问该iterator对应的节点:

_self& operator--() {
	_pnode = _pnode->_prev;
	return *this;
}
T& operator*() {
	return _pnode->_data;
}

 使用迭代器访问各种元素时,经常用到!=判断是否走到了末尾:

bool operator!=(const _self& it) {
	return this->_pnode != it._pnode;
}

除了前置++--,还有后置的:

                            

让pnode先往下走一个,再通过prev指向的位置构造一个新的。


3.2 联系list

再回到list中:                           

begin()需要指向的是head->next

end()需要指向的是最后一个节点的下一个位置,所以就是头结点

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

 同时,在调用测试时:

因此还需要把 ListIterator中的几个函数变为公有。

(!=在重载的实现过程中需要访问_pnode ,如果按照class实现,就需要改用public修饰,不如直接将iterator写成一个struct)


如果此时只有一个头结点也不要紧,it表示的是begin->next,是自己,end也是自己,就不会进入该循环,逻辑是自洽的

并且,由于for循环就是自动去找iteraotr和begin()与end()三个名字,我们也能使用循环for

我们不能直接改变内置类型的行为,但是可以通过封装的方式来将该内置类型封装成想要的样子。

理解清楚这张图:

this是一个指向self(也就是ListIterator<T>)的指针 ,return *this就相当于把这个iterator传回去了


3.3 ListIterator的完善

3.3.1 不需要写析构函数

Node说到底其实是链表的东西,所以在封装的iterator中不能直接给他释放了

3.3.2 拷贝构造和赋值运算符

都使用浅拷贝即可,系统自动生成的就是浅拷贝

因为我们的类成员只有一个指针,在对iterator使用=时就是希望能够让两个iterator指向同样的Node 。

3.3.3  operator-> (双箭头变单箭头)

就像刚才观察的position一样,除了使用*解引用来获得iterator指向的节点的_data  ,  还需要能够通过->来访问

先实现一个箭头访问: 

                             

                      (注意,_node的类型是Node*   ,  箭头优先级也是高于取地址的)


假设我们的T是一个控制坐标的自定义类型Pos

                     

我们再对自己的链表进行测试:

it解引用之后是一个Pos,但是Pos没法直接被留提取<<给输出,因此报错。

我们当然可以“投降”,一个一个的访问元素:

但是我们更应该直接通过iterator去访问pos的成员变量:

关于箭头的理解,此处比较麻烦:

首先,对于(*it)._row 是比较好理解的。 我们重载的*返回的是一个T的引用,这个引用的类再通过.来访问自己的成员_row

其次,对于it->_row  就有点麻烦了。->作为我们重载的一个单目运算符,it->返回的是_data的地址,也就是一个T*  ,   相当于就变成了 T*_row    ,   按照我们之前的学习,这样是不能访问的。应当是两个箭头:(指针通过箭头访问内部成员)

显式调用的话应该是:

   但是为了代码的可读性,编译器不支持这样使用,而是选择省略了一个箭头。

两个箭头的意义是不一样的,第一个是运算符重载的调用,第二个是原生指针访问内部成员。

(日常的迭代器用operator->的会不多,之后在学习图(map)的时候会比较多)

特殊现象:

关于为什么begin()的返回值可以++,也就是

++li1.begin();

                   

虽然返回值具有常性,但是编译器会对内部函数的返回值特殊处理。有常性,但不是只有常性。这样的返回值任然可以调用那些非const的成员函数

我们稍加总结:

匿名对象的const和临时对象的const是不一样的,虽然不能直接在返回值给引用,但是可以调用非静态的成员函数 

        

        


4.const迭代器

在使用场景中,一定有使用const_iterator的时候,比如传引用(传引用时都建议加上const),我们都建议传一个const修饰的链表,该链表的迭代器就需要是const_iterator

              


不能直接在list<int> :: iterator 前面加const  , 

const list<int>::iterator it2 = li1.begin();//错误样例

这样写的话,it2就不能++和改变自己的值了(而不是 不能改变自己对应的值)

这样的iterator是不能修改的,而我们希望的是有一个指向的内容不能修改但是自己是可以修改的const_iterator。

那么到底是什么内容在控制iterator是否能被修改呢?

核心模块:

       

iterator对应的值是否能修改,就看operator* 返回的是T& 还是 const T&;

operator->返回的是T*还是const T*

因此我们可以实现两个类,一个是刚刚的ListIterator ,另一个是我们再实现一个ListConstIterator

改一改名字,修改一下两个核心模块的函数返回值即可。 

        

template<typename T>
class ListConstIterator {

	typedef ListNode<T> Node;//与class list中的命名保持一致性
	typedef ListConstIterator<T> _self;
	Node* _pnode;
public:
	ListConstIterator(Node* pnode)
		:_pnode(pnode)
	{}

	//_self operator++() {
	//	return _self(_pnode->_next);//测试一下能不能跑
 //   }
	_self& operator++() {
		_pnode = _pnode->_next;
		return *this;
	}
	_self operator++(int) {
		_pnode = _pnode->_next;
		return _self(_pnode->_prev);
	}
	_self& operator--() {
		_pnode = _pnode->_prev;
		return *this;
	}
	_self operator--(int) {
		_pnode = _pnode->_prev;
		return _self(_pnode->_next);
	}
	const T& operator*() {
		return _pnode->_data;
	}
	const T* operator->() {
		return &_pnode->_data;
	}
	bool operator!=(const _self& it) {
		return this->_pnode != it._pnode;
	}

};

这样我们就能达到目的了。

别忘了再去写cons修饰的list对应的begin和end,这样返回的begin()和end()才都是不会被修改的。

            

注意此处的lt也必须是const list<int> lt ;(也就是链表也必须是被const修饰的)

       


读者朋友们是否会觉得这样的实现有点过于冗余呢?

因为ListIterator<T>和ListConstIterator<T>两者非常相像,只有少数接口不一样

能否不定义一个新的类呢:

     

此时编译器会分别拿T和const T 分别去实现两个迭代器

如果T是int

ListIterator中所有的T都变成了const int,但是class list中依然实例化的是int,   若调用:

              

此时初始化const_iterator就需要一个ListNode<const int>*  ,  而我们的list和ListNode都是用的int来初始化,上图中return处的构造就没法成功。

                      

_head->_next的类型取决于list处你用什么去生成模版类

按照我们之前的写法就一直都是一个int*

并且也没有人会要求初始化一个const_iterator是需要一个const T*的,T*就足够了。

但是按照之前的办法,实现两个完整的迭代器就不存在这个问题,我们可以在const_iterator的构造函数处将传入的参数设置成 ListNode<T>* 即可。

解决方案:

直接将引用和指针传入,一个不使用const , 一个使用const修饰

iterator:

对照着来看: 


5. list中的insert和erase

insert不会发生迭代器失效,但是erase还是有迭代器失效的。

首先看insert :

 按照合理的顺序更改各个指针的指向即可

再看返回值:

指向用户插入的第一个元素:

iterator insert(iterator pos, const T& x) {
	Node* tmp = new Node(x);

	tmp->_next = pos._pnode;
	tmp->_prev = pos._pnode->_prev;
	pos._pnode->_prev->_next = tmp;
	pos._pnode->_prev = tmp;

	return iterator(tmp);
}

返回临时变量(我们用tmp构造的临时变量)时就不要传引用返回了,返回一个tmp的拷贝更加合适。

           


类似于vector ,  erase返回的也是被删除的下一个位置的迭代器

同时还要注意断言,不要把哨兵位给删掉了

iterator erase(iterator pos) {
	assert(pos != end());//别把哨兵位给删了
	Node* cur = pos._pnode;
	Node* prev = pos._pnode->_prev;
	Node* next = pos._pnode->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值