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

本文介绍如何在C++中实现带迭代器的链表,包括迭代器的设计、异常处理及构造方法等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

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) {}
//注意这个protected
protected:
    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);
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值