编写有迭代器的链表(C++专用)

来源:互联网 发布:网络吃鸡是什么意思啊 编辑:程序博客网 时间:2024/06/08 12:05

为何要编写有迭代器的链表

 在编写链表时可以这么做:

template <typename T>class List{public:    /****/    typedef List* iterator;};int main(){    List<int>::iterator it;}

 注意一定要写在public中,否则无法使用。
 但是这样有一个很严重的问题:那就是由于iterator的本质仍然是指针,它将无法对越界进行检验

编写分析

方法

 使用嵌套类可以做到。

附注:C++中的嵌套类和外层类没有任何关系,相互之间没有访问权限,这点要注意!

大致结构

template <typename T>class List{private:    struct Node    {        T val;        Node *next;        Node *prev;        Node(T va = 0, Node *pre = nullptr, Node *ne = nullptr) : val(va), prev(pre), next(ne) {}    };public:    class const_iterator    {        /**/    };    class iterator : public const_iterator    {        /**/    };public:    /*-------------Constuctor & BigThree-------------*/    List()    {        /**/    }    //注意本函数的实现技巧    List(const List &list)    {        /**/    }    //注意返回值    const List &operator=(const List &list)    {        /**/    }    ~List()    {        /**/    }    /*-------------Manipulate-------------*/    void Clear()    {        while (!IsEmpty())        {            Pop_back();        }    }    iterator Insert(const iterator &iter, const T &val)    {        /**/    }    iterator Erase(const iterator &iter)    {       /**/    }    iterator Erase(const iterator &beg, const iterator &end)    {        /**/    }    void Push_back(const T &val)    {        Insert(end(), val);    }    void Pop_back()    {        Erase(--end(), val);    }    /*-------------Iterator-------------*/    iterator begin()    {        /**/    }    //尾后的const一定要写,因为它也是签名之一,写上去才能视作有效的重载    const_iterator begin() const    {        /**/    }    iterator end()    {        /**/    }    const_iterator end() const    {        /**/    }    /*-------------Info-------------*/    void Print()    {        Node *p = head->next;        cout << "[" << p->val;        p = p->next;        while (p != tail)        {            cout << ", " << p->val;            p = p->next;        }        cout << "]" << endl;    }    bool IsEmpty() const    {        return size == 0;    }    size_t Size() const    {        return size;    }    T & front()    {        return *begin();    }    const T & front() const    {        return *begin();    }    T & back()    {        return *--end();    }    const T & back() const    {        return *--end();    }private:    void Init()    {        size = 0;        head = new Node;        tail = new Node;        tail->prev = head;        head->next = tail;    }    Node *head;    Node *tail;    size_t size;};

 注意到这里我们同时实现了两个迭代器const_iteratoriterator。这里的技巧在于让iterator继承const_iterator。这是因为这两种迭代器的不同之处只在operator*时才体现出来————一个返回普通引用,而另一个返回指向常量的引用,其他的都一样。所以我们可以使用继承机制来实现。接下来对继承关系进行分析:iterator可以当作const_iterator来使用,而反之不然。也就是说iteratoris-aconst_iterator,所以我们令const_iterator为父类。

异常处理

 我们编写迭代器的目的就在于要处理异常情况,所以我们需要编写一个异常类来处理相关问题:

class IteratorMisMatchException : public exception{public:    IteratorMisMatchException(const char * inf) : info(inf) {}    string what()    {        return info;    }private:    string info;};

iterator编写

 先考虑数据域的问题,这里很简单我们可以用一个Node *cur,来表示迭代器现在指向的位置(我们待会会说明这是不够的)。唯一要注意的是,不要写成private,不然iterator将无法访问数据域

构造问题

 再考虑迭代器应该给外界留下那些接口:
- 用于构造的接口
- 各种operator

 对于后者我们没有什么问题,这是给用户的接口,让我们的迭代器能像指针一样使用,因此编写的运算符重载要包括: ++, --, +, -, *, ==, !=。理所当然,访问权限要是public
 关键在于前者,我们要仔细考虑一下要有那些构造方法。首先,一个默认构造函数是必不可少的,这样就可以声明一个不指向任何结点的迭代器。

class const_iterator{public:    const_iterator() : cur(nullptr) {}//注意这个protectedprotected:    Node *cur;};

 重点来了,我们接下来会很理所当然的写出这样的构造函数:

class const_iterator{public:    const_iterator() : cur(nullptr) {}    //顺便说一下,可以写成Node *p, 但不要写成const Node *p; 否则会出现底层const不相容问题    const_iterator(Node * const p) : cur(p), theList(list) {}protected:    Node *cur;};

 表面上这没什么问题,但是这样的话就会出现这种情况:

int main(){    const_iterator it(node);    return 0;}

 似乎没什么,但问题是nodeList的封装成员,如果将const_iterator(Node * const p)作为对外的构造接口的话,就势必意味这封装性被破坏。所以const_iterator(Node * const p)不能设为public而应该是protected
 那么外部应该怎样获取一个有效的迭代器呢?我们可以联想到标准库的做法:begin()end()。所以这样一来我们的对外构造模型就成立了:外界通过调用list.begin()方法,获取一个迭代器对象。而begin()内部则会构造一个迭代器对象return出去

iterator begin(){        /**/}//尾后的const一定要写,因为它也是签名之一,写上去才能视作有效的重载const_iterator begin() const{    /**/}iterator end(){    /**/}const_iterator end() const{    /**/}

这里我们有两个版本的的begin(),尾后的const不能省略,它属于方法签名的一员,不加的话会无法重载函数
 现在还有一个问题,虽然我们通过将方法声明为protected避免了破坏封装,但是现在外部类List<T>也无法使用该构造方法了。解决该问题的途径用到一个小技巧:List<T>声明为友元类

class const_iterator{public:    const_iterator() : cur(nullptr) {}protected:    //注意!    friend class List<T>;    Node *cur;    const_iterator(Node * const p) : cur(p), theList(list) {}};

opeartor

 编写operator总体来讲较为简单,要注意的有两点:
- const_iteratoriterator存在一些细微差别,需要重写。
- 前置++与后置++返回不同。

const_iterator:

const T &operator*() const{    return cur->val;}//注意前置++返回引用const_iterator &operator++(){    cur = cur->next;    return *this;}//后置++不可返回引用const_iterator operator++(int){    const_iterator old(theList, this->cur);    //调用前置++    ++(*this);    return old;}bool operator==(const const_iterator &iter) const{    return cur == iter.cur;}bool operator!=(const const_iterator &iter) const{    //调用operator==    return !(*this == iter);}

iterator:

T &operator*(){    return cur->val;}//注意const T &operator*() const{    return const_iterator::operator*();}//重写iterator &operator++(){    cur = cur->next;    return *this;}

 注意这里iterator中保留了const T &operator*() const。这是因为要考虑到const List<T>::iterator it这样的迭代器,显然它不能调用非const方法

List<T>编写

构造函数和BigThree

 在这里我们主要展示一下代码重用的思想,其实这几个方法本身并没什么特殊的:

class List{public:    List()    {        Init();    }    //注意本函数的实现技巧    List(const List &list)    {        Init();        operator=(list);    }    //注意返回值    const List &operator=(const List &list)    {        if (this == &list)        {            return *this;        }        Clear();        for (const_iterator it = list.begin(); it != list.end(); ++it)        {            Push_back(*it);        }        return *this;    }    ~List()    {        Clear();        delete head;        delete tail;    }private:    void Init()    {        size = 0;        head = new Node;        tail = new Node;        tail->prev = head;        head->next = tail;    }};

迭代器函数

 较为简单,不做赘述。(不过注意这里写的函数并不完美,因为关于迭代器类我们稍后还要做些优化)

iterator begin(){    return iterator(head->next);}//尾后的const一定要写,因为它也是签名之一,写上去才能视作有效的重载const_iterator begin() const{    return const_iterator(head->next);}iterator end(){    return iterator(tail);}const_iterator end() const{    return const_iterator(tail);}

 对于const list而言我们返回常迭代器,这一点是符合标准的。

插入与删除

iterator Insert(const iterator &iter, const T &val){    Node *p = iter.cur;    ++size;    //注意技巧    return iterator(this,            p->prev = p->prev->next = new Node(val, p->prev, p));}iterator Erase(const iterator &iter){    Node *p = iter.cur;    //erase的返回约定    iterator save(this, p->next);    p->prev->next = p->next;    p->next->prev = p->prev;    delete p;    --size;    return save;}iterator Erase(const iterator &beg, const iterator &end){    //不要++it!    for (iterator it = beg; it != end; )    {        it = Erase(it);    }    return end;}

 要注意的是参数一定要写成const iterator &,而非iterator &。这是因为在我们的程序中会出现这种调用形式:

Erase(--end());

如果写成后者的形式,就会出现左值引用尝试去绑定右值的情形,所以一定要写做const iterator &

以上程序存在的问题

 到目前为止我们已经把大体的框架打好了,但是,以上程序的健壮性是极差的

越界检查

 这是很理所当然的,毕竟我们构建迭代器类的目的就在于此。我们可以在类中定义一个会抛出异常的函数:

class const_iterator{privatevoid AssertRange() const    {        if (this->cur == nullptr)        {            throw IteratorMisMatchException("Iterator went out of the range!");        }    }};

 在涉及移动迭代器的操作中就可以这样:

iterator &operator++(){    this->cur = this->cur->next;    this->AssertRange();    return *this;}

迭代器有效问题

 我们在使用迭代器之前,首先得确定它是一个有效的迭代器。比如Erase(it),如果it无效则很显然会有问题。
 我们先讲解前两种情况,也是较为简单的情况
- 迭代器未初始化
- 迭代器指向了尾端
 这两种较为简单,如果未初始化,则cur一定为nullptr;若在尾端,则cur->nextcur->prev一定为nullptr

void AssertValidity() const{    if (theList == nullptr || cur == nullptr ||         cur->next == nullptr || cur->prev == nullptr)    {        throw IteratorMisMatchException("The iterator is invalid!");    }}

 难点在于第三种情况:
- 迭代器有效但不匹配

 来考虑这样一个情形:

List<int> l1;List<int> l2;/**添加元素**/l1.Insert(l2.end(), 5);l2.Erase(l1.begin(), l2.end());

 可以看到,虽然我们传入的是有效迭代器,但却和链表根本不匹配。尤其是第二个实例,会陷入死循环的境地。
 为了改变这一情形,我们这里使用一个技巧。我们在迭代器类中额外增加一个记录了它指向的链表的指针,用作标识。

class const_iterator{    /**/    friend class List<T>;    const_iterator(const List<T> *list, Node * const p) : cur(p), theList(list) {}    //Attention!!!    const List<T> *theList;};

 然后我们就可以修改之前的方法,让它们更加健壮:

iterator Insert(const iterator &iter, const T &val){    //检查迭代器是否有效,因为在end()前插入是允许的,所以排除该特殊情况    if (iter != end())    {        iter.AssertValidity();    }    //检查是否是本链表上的迭代器    if (iter.theList != this)    {        throw IteratorMisMatchException("Iterator dose not refer to this list");    }    Node *p = iter.cur;    ++size;    return iterator(this,p->prev = p->prev->next = new Node(val, p->prev, p));}iterator Erase(const iterator &iter){    iter.AssertValidity();    if (iter.theList != this)    {        throw IteratorMisMatchException("Iterator dose not refer to this list");    }    Node *p = iter.cur;    //erase的返回约定    iterator save(this, p->next);    p->prev->next = p->next;    p->next->prev = p->prev;    delete p;    --size;    return save;}iterator Erase(const iterator &beg, const iterator &end){    beg.AssertValidity();    if (end != this->end())    {        end.AssertValidity();    }    //检查是否指向一个表,以及是否指向本表    if (beg.theList != end.theList)    {        throw IteratorMisMatchException("Beg and end refer to different list!");    }    else if (beg.theList != this)    {        throw IteratorMisMatchException("Iterator dose not refer to this list");    }    //不要++it!    for (iterator it = beg; it != end; )    {        it = Erase(it);    }    return end;}

 然后在修改一下其他函数中的构造函数参数:

iterator begin(){    return iterator(this, head->next);}

 至此,一个带有迭代器的且较为健壮的链表就实现成功了!

原创粉丝点击