上一篇讲到了链式存储结构的基本操作,本篇就对具体操作在代码层上进行实现。
我们需要设计一个单链表的类LinkList:
使用类模板实现。
继承自List类。
通过头结点访问后继结点。
定义内部结点类型Node,用于描述数据域和指针域。
类里实现链表的关键操作(增删待查等等)。
看一个LinkList类的声明:
template <typename T>
class LinkList : public List<T>
{
protected:
struct Node : public Object//顶层父类实现一些操作符重载如new、delete、[]、==、!=等等
{
T value;
Node* next;
};
mutable Node m_header;
int m_length;
public:
LinkList();
bool insert(int i,const T& e);
bool insert(const T& e);
bool remove(int i);
bool set(int i,const T& e);
bool get(int i,T& e )const;
T get(int i)const;
int length()const;
void clear();
~LinkList();
};
定义了一个Node类,继承自顶层父类Object,包含数据变量和Node类型指针。
定义一个Node类型的头结点用于辅助获取元素位置。
定义一个表示链表长度的变量m_length。
类成员函数包括构造函数和析构函数、插入及其重载、删除、获取元素、设置元素值、获取链表长度、清空链表等操作。
下面看功能具体实现:
一、构造函数
构造函数用于设置头结点的指向(NULL)和初始化链表长度。
二、插入
链表插入元素上一篇已经讲过,操作步骤是先定位到待插入位置的前一个位置,再进行插入操作。
bool insert(int i,const T& e)
{
bool ret = (i >= 0) && (i <= m_length);
if(ret)
{
Node* node = new Node();
if(node != NULL)
{
Node* current = reinterpret_cast<Node*>(&m_header);
for(int p = 0; p < i; p++)
{
current = current->next;
}
node->value = e;
node->next = current->next;
current->next = node;
m_length++;
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException,"No memory to new ");
}
}
return ret;
}
bool insert(const T& e)//每次插入到最后一个位置
{
return insert(m_length,e);
}
执行顺序也是先定位后删除。
bool remove(int i)
{
bool ret = (i >= 0) && (i < m_length);
if(ret)
{
Node* current = reinterpret_cast<Node*>(&m_header);//出过错
for(int p = 0; p < i; p++)
{
current = current->next;
}
Node* toDel = current->next;
current->next = toDel->next;
delete toDel;
m_length--;
}
return ret;
}
先定位后操作
bool set(int i,const T& e)
{
bool ret = (i >= 0) && (i < m_length);
if(ret)
{
Node* current = reinterpret_cast<Node*>(&m_header);
for(int p = 0; p < i; p++)
{
current = current->next;
}
current->next->value = e;
}
return ret;
}
bool get(int i,T& e )const
{
bool ret = (i >= 0) && (i < m_length);
if(ret)
{
Node* current = reinterpret_cast<Node*>(&m_header);
for(int p = 0; p < i; p++)
{
current = current->next;
}
e = current->next->value;
}
return ret;
}
T get(int i)const//根据索引直接获取元素值
{
T ret;
if(get(i,ret))
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException,"Invalid parameter i to get ...");
}
return ret;
}
返回m_length即可。
六、析构函数
执行清空线性表操作。
七、头结点的隐患
前面设计中我们将头结点定义为Node类型,这本身是没有什么问题的,但是当我们在使用过程中要是自定义类类型时就可能出错,为什么呢?
定义一个在构造函数中抛出异常的类:
class Test
{
public:
Test()
{
throw 0;
}
};
执行:
LinkList<Test> list;//Test为类名
当执行这条语句时就会创建一个Test类型的头结点,在头结点中就会执行 T value;但是T在此时为类类型,所以就会调用构造函数,所以就会抛出异常打断程序执行流,这显然不是我们期望的,因为我们就没有创建Test对象为什么会执行构造函数呢?这时我们就需要想办法阻止T value;在T为类类型时调用构造函数。
由于在头结点中value并没有实际作用,所以我们就想办法避开,定义一个匿名结构,包含一个字符数组,长度为T的大小,这样就能使字符数组在内存排布上与value相同,这样就避开了构造函数的调用,具体实现如下:
mutable struct : public Object//如果未继承自Object可能导致内存布局和Node m_header内存布局不同。
{
char reserved[sizeof(T)];
Node* next;
}m_header;
为什么加上mutable关键字?因为在类的实现中编译器以为我们修改了成员变量(const函数内不允许修改成员变量)。
最后我们将new和delete封装起来:
virtual Node* create()
{
return new Node();
}
virtual void destroy(Node* pn)
{
delete pn;
}
为什么这么做?请看我下下篇里的描述。提醒一下,虚函数用于重写函数实现多态特性。
总结;
通过类模板实现链表,包含头结点成员和长度成员。
定义结点类型,并通过堆中的结点对象构成链式存数。
为了避免构造错误的隐患,头结点类型需要重定义。