为何要编写有迭代器的链表
在编写链表时可以这么做:
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_iterator
和iterator
。这里的技巧在于让iterator
继承const_iterator
。这是因为这两种迭代器的不同之处只在operator*
时才体现出来————一个返回普通引用,而另一个返回指向常量的引用,其他的都一样。所以我们可以使用继承机制来实现。接下来对继承关系进行分析:iterator
可以当作const_iterator
来使用,而反之不然。也就是说iterator
is-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;
}
似乎没什么,但问题是node
是List
的封装成员,如果将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_iterator
和iterator
存在一些细微差别,需要重写。
- 前置++
与后置++
返回不同。
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
{
private:
void 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->next
或cur->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);
}
至此,一个带有迭代器的且较为健壮的链表就实现成功了!