和我一起学C++之list<一>

来源:互联网 发布:php全栈 编辑:程序博客网 时间:2024/05/02 17:38

list(链表)是一种非常重要的数据结构,在实际应用中到处可见,所以在笔试面试中都是考察的大热门。与vector类似,list也是线性结构,但不同的是list中的内存分布并不是连续的。

同时std::list作为C++中的一种容器(container),相信用过的朋友不在少数,但是真正读过源码了解底层实现的可能并不多。原因大致有二,首先list作为一种工具,会用就行,你也大抵没见过使用电视的人需要会造电视吧;其次,标准库中相应的模板的知识,增加了对list这种数据结构理解的难度。知乎上曾有这样的一个问题:c/c++语言应该在linux系统下面学习吗?我觉得不是的,我们要抓住事情的关键部分,这里的关键就是c/c++语言本身。在windows系统,你可以在”宇宙第一IDE”vs下安安心心的学C/C++语言,但是在linux下面呢,你得先学会linux的基本命令、makefile和系统相关知识等等。诚然,在linux下学习会对编译、链接过程更加了解,但学习曲线的陡然上升是不符合我们的预期的。

基于上面的想法,我们只需要研究包含了简单类型如int类型的std::list<int>即可,当然为了不与标准库冲突,我们的代码全在namespace yang中编写,也就是yang::list大致相当于std::list<int>的功能。(用自己名字,让我厚颜无耻一次吧,哈哈)

1、list的构成

list可以看作是由一系列的node来构成,其结构示意图如下:

这里写图片描述

而每一个node的组成如下:
这里写图片描述 这里prev指针指向上一个node节点,next指针指向下一个node节点

所以很自然我们的list_node应该如下:

struct list_node{    list_node* prev{ nullptr };    list_node* next{ nullptr };    int value;};

2、如何操作list中的node

最简单的方式莫过于定义一个list_node* 指针来操作,但是直接用指针的方式首先是不太安全,其次是写起代码来不够优雅美观。所幸,在STL中有一个很好的范本,迭代器iterator。将list_node pointer封装在iterator类中,不但较为安全,并且简洁美观。
list_iterator的实现如下:

struct list_iterator{    list_node* node_;    list_iterator(list_node* node) :node_(node){}// implicit constructor cast    list_iterator(const list_iterator& rhs) :node_(rhs.node_){}    bool operator==(const list_iterator &rhs) const {        return node_ == rhs.node_;    }    bool operator!=(const list_iterator &rhs) const {        return node_ != rhs.node_;    }    int& operator*() const{ return node_->value; }    list_iterator& operator++(){        node_ = node_->next;        return *this;    }    list_iterator operator++(int){        list_iterator tmp = *this;        ++*this;        return tmp;    }    list_iterator& operator--(){        node_ = node_->prev;        return *this;    }    list_iterator operator--(int){        list_iterator tmp = *this;        --*this;        return tmp;    }};

首先list_iterator内部定义了一个list_node* node_的成员变量,用于来操作list中的每一个node。构造和拷贝构造函数都很简单,都只是初始化了node_ 变量,并没有做其他事。重载操作符是实现迭代器的关键,我们来讲解下面五个函数。

2.1、迭代器的比较
首先我们需要比较两个迭代器是否相等或不等,以确定何时可以终止迭代。很简单,判断它们的成员变量node_相等即可。

bool operator==(const list_iterator &rhs) const {    return node_ == rhs.node_;}bool operator!=(const list_iterator &rhs) const {    return node_ != rhs.node_;}

2.2、迭代器的解引用

最重要的是要得到node节点中的值,这个值很容易获得(node_->value),关键这个函数怎么写,写成普通的成员函数可以么?

int getValue(){return node_->value}

错到是没错,但是未免麻烦。想想我们引入迭代器是用来代替裸指针的,那么在行为上更类似指针的行为就更好理解且美观了,所以我们可以重载* operator

int& operator*() const{ return node_->value; }

为什么返回值是引用呢?留给下文阐述。

2.3、 迭代器的移动

对于某一个节点list_node* node_来说,它的上一个节点可以用node_->prev 表示,它的下一个节点可以用node_->next 表示。所以很自然的,有:

list_iterator& operator++(){    node_ = node_->next;    return *this;}list_iterator& operator--(){    node_ = node_->prev;    return *this;}

对于一个指向某个节点的迭代器list_iterator curr(node_) 来说,--curr和++curr 就分别表示了该node_节点的上一个节点和下一个节点,简洁还明了。

好了,现在说说为什么有的重载操作符需要返回引用了,记得这个问题还在知乎上有个讨论,但我粗略地看过一遍,似乎没有特别有说服力的答案。实际上,很简单,因为左值表达式(lvalue expression)和右值表达式(rvalue expression)的关系。

那么有哪些表达式是左值表达式呢?(以下内容节选自cpp reference,原文为英文,感兴趣的可以自行去查阅)
1、函数或者操作符重载表达式的返回引用类型
2、内置类型的前++和前–表达式
3、*p,the bulit-in indirection expression 内置的解引用表达式

我们知道,C++的设计初衷是让用户定义的类型(class)能像内置类型(int)一样工作,具有同等的地位。

那么对于int a = 5; int *p = &a; 来说,--a,++a,*p 就全部是左值表达式了,所以对于用户自定义类型来说,如果要重载这几个操作符的话,就要保证该操作符重载表达式是左值表达式了。而操作符重载实际上就是一种较为特殊的函数而已,而从上述的第一条可以看到,只要函数返回引用类型,该函数调用表达式就是个左值表达式了。

事实上,所有的操作符重载都是符合这个规律的。

好了,本章到此就结束了,下一章我们就可以讲list本身了。

总结: list作为一种很重要的数据结局,是我们不得不掌握的知识点。本文重点介绍了list中的节点list_node和迭代器list_iterator 和其代码实现,对于迭代器如何操作list中的node有了初步的了解。

参考资料:
STL 源码剖析 侯捷著
数据结构(c++语言版)(第三版)邓俊辉编著
gcc 2.95 version source code

0 0
原创粉丝点击