CList,MFC中的常用链表,其本质是一个双向链表。操作包括基本的插入(头插,尾插等),查找等常用操作。
本文代码均截取自Afxtempl.h
先看类的声明:
template
class CList : public CObject
{
...
}
与CArray一样,是模板类型,第一个参数为链表中保存的对象类型,第二个参数则决定了后续的插入等操作传入的是对象还是对象的引用,使用者可选。
以下截取部分成员变量声明
节点定义
protected:
struct CNode
{
CNode* pNext;
CNode* pPrev;
TYPE data;
};
显而易见,CList是一个双向链表,有两个指针域(一个指前驱节点,一个指向后继节点),一个数据域(用来保存对象)
再看其他成员变量
protected:
//头节点指针,链表的第一个节点
CNode* m_pNodeHead;
//尾节点指针,指向链表的最后一个节点
CNode* m_pNodeTail;
//链表中的节点个数
INT_PTR m_nCount;
CNode* m_pNodeFree;
struct CPlex* m_pBlocks;
INT_PTR m_nBlockSize;
template<class TYPE, class ARG_TYPE>
CList<TYPE, ARG_TYPE>::~CList()
{
RemoveAll();
ASSERT(m_nCount == 0);
}
template<class TYPE, class ARG_TYPE>
CList<TYPE, ARG_TYPE>::CList(INT_PTR nBlockSize)
{
ASSERT(nBlockSize > 0);
m_nCount = 0;
m_pNodeHead = m_pNodeTail = m_pNodeFree = NULL;
m_pBlocks = NULL;
m_nBlockSize = nBlockSize;
}
通过上面可以看出,构造函数构造了一个空链表,构造之后,链表的节点个数是0,头指针和尾指针都为NULL。析构函数通过调用RemoveAll来移除所有节点,从而清空整个链表。
下面来看几个主要函数
AddHead(头插),AddTail(尾插),InsertBefore(前端插入),InsertAfter(后端插入)
关键代码如下(其中涉及到的NewNode,freeNode稍后讨论,与CPlex内存管理方法有关,在此知识先看到NewNode功能,是创造一个节点,传入的参数是此节点的头指针和尾指针,分别指向此节点的前驱节点和后继节点)
template<class TYPE, class ARG_TYPE>
POSITION CList<TYPE, ARG_TYPE>::AddHead(ARG_TYPE newElement)
{
//通过NewNode造出一个新的节点(此节点的头指针是NULL,尾指针是旧的头指针(pNodeHead))
CNode* pNewNode = NewNode(NULL, m_pNodeHead);
//节点数据域进行赋值,如果数据保存的是对象,那么会调用拷贝构造函数
pNewNode->data = newElement;
//把新建的节点插在头部,更新头节点
if (m_pNodeHead != NULL)
m_pNodeHead->pPrev = pNewNode;
else
m_pNodeTail = pNewNode;
m_pNodeHead = pNewNode;
return (POSITION) pNewNode;
}
此函数返回新插入的节点的指针,逻辑常规,双向链表头部插入。示意图如下
1.新建节点(头指针指向NULL,尾指针指向1号节点)
2.新节点拼接到旧的第一个节点之前
3.指向头结点的成员变量m_pNodeHead指向新节点
关键代码如下
template<class TYPE, class ARG_TYPE>
POSITION CList<TYPE, ARG_TYPE>::AddTail(ARG_TYPE newElement)
{
//新建节点,头指针指向旧的尾节点,尾指针指向NULL
CNode* pNewNode = NewNode(m_pNodeTail, NULL);
//节点数据域进行赋值,如果数据保存的是对象,那么会调用拷贝构造函数
pNewNode->data = newElement;
//把新建的节点插在尾部,更新尾节点
if (m_pNodeTail != NULL)
m_pNodeTail->pNext = pNewNode;
else
m_pNodeHead = pNewNode;
m_pNodeTail = pNewNode;
return (POSITION) pNewNode;
}
此函数返回新插入的节点的指针,逻辑常规,双向链表尾部插入。示意图如下
1.新建尾(头指针指向旧的尾节点m_pNodeTail,尾指针指向NULL)
2.新节点拼接到旧的最后一个节点之后
3.指向尾结点的成员变量m_pNodeHead指向新节点(更新尾节点成员变量)
InsertBefore插入,把新的节点插入到某一个节点之前,其传入的第一个参数代表链表中的一个节点,新插入的节点在这个节点之前
before插入与after插入原理相同,所以后面只看after插入,before列出函数声明
template<class TYPE, class ARG_TYPE>
POSITION CList<TYPE, ARG_TYPE>::InsertBefore(POSITION position, ARG_TYPE newElement);
position指向插入的位置(实现中强转成了Node*),newElement为要插入的元素,可以是对象
afterBefore插入,把新的节点插入到某一个节点之后,其传入的第一个参数代表链表中的一个节点,新插入的节点在这个节点之后
关键代码如下
position指向插入的位置(实现中强转成了Node*),newElement为要插入的元素,可以是对象
template<class TYPE, class ARG_TYPE>
POSITION CList<TYPE, ARG_TYPE>::InsertAfter(POSITION position, ARG_TYPE newElement)
{
// Insert it before position
CNode* pOldNode = (CNode*) position;
//新建节点,节点的头指针指向pOldNode ,尾指针指向pOldNode的下一个节点
CNode* pNewNode = NewNode(pOldNode, pOldNode->pNext);
//节点数据域进行赋值,如果数据保存的是对象,那么会调用拷贝构造函数
pNewNode->data = newElement;
//以下代码吧新建节点插入到目标位置pOldNode之后(pOldNode下一个节点之前)
if (pOldNode->pNext != NULL)
{
ASSERT(AfxIsValidAddress(pOldNode->pNext, sizeof(CNode)));
pOldNode->pNext->pPrev = pNewNode;
}
else
{
ASSERT(pOldNode == m_pNodeTail);
m_pNodeTail = pNewNode;
}
pOldNode->pNext = pNewNode;
//返回新插入的节点
return (POSITION) pNewNode;
}
示意图如下:
1.新建节点
2.插入新建节点(这里假定插入到1号节点之后)
template<class TYPE, class ARG_TYPE>
void CList<TYPE, ARG_TYPE>::RemoveAt(POSITION position)
{
ASSERT_VALID(this);
CNode* pOldNode = (CNode*) position;
ASSERT(AfxIsValidAddress(pOldNode, sizeof(CNode)));
// remove pOldNode from list
if (pOldNode == m_pNodeHead)
{
m_pNodeHead = pOldNode->pNext;
}
else
{
ASSERT(AfxIsValidAddress(pOldNode->pPrev, sizeof(CNode)));
pOldNode->pPrev->pNext = pOldNode->pNext;
}
if (pOldNode == m_pNodeTail)
{
m_pNodeTail = pOldNode->pPrev;
}
else
{
ASSERT(AfxIsValidAddress(pOldNode->pNext, sizeof(CNode)));
pOldNode->pNext->pPrev = pOldNode->pPrev;
}
FreeNode(pOldNode);
}
pos为要修改节点的地址,关键代码如下(要修改哪一个节点)
template<class TYPE, class ARG_TYPE>
AFX_INLINE void CList<TYPE, ARG_TYPE>::SetAt(POSITION pos, ARG_TYPE newElement)
{ CNode* pNode = (CNode*) pos;
ASSERT(AfxIsValidAddress(pNode, sizeof(CNode)));
//修改节点,要调用拷贝构造函数
pNode->data = newElement; }
所谓查找,就是传入一个整形的索引,返回”索引所在位置的Node”,也就是”获取第Index个(Index从0开始计数,0表示head节点)Node”。
template<class TYPE, class ARG_TYPE>
POSITION CList::FindIndex(INT_PTR nIndex) const
{
ASSERT_VALID(this);
if (nIndex >= m_nCount || nIndex < 0)
return NULL; // went too far
//从头结点开始查找
CNode* pNode = m_pNodeHead;
//循环遍历节点,遍历nIndex次
while (nIndex--)
{
ASSERT(AfxIsValidAddress(pNode, sizeof(CNode)));
//pNode依次往后移
pNode = pNode->pNext;
}
//循环结束后,返回Index号节点Node指针
return (POSITION) pNode;
}
//真正的”查找节点”
下面这个函数从某一个位置开始向后查找,与希望查找的数据(可能是对象)进行”数据比较”,会调用对象的比较函数,返回符合比较条件的值,比较函数由使用者自己定义。
template<class TYPE, class ARG_TYPE>
POSITION CList<TYPE, ARG_TYPE>::Find(ARG_TYPE searchValue, POSITION startAfter) const
{
ASSERT_VALID(this);
CNode* pNode = (CNode*) startAfter;
if (pNode == NULL)
{
pNode = m_pNodeHead; // start at head
}
else
{
ASSERT(AfxIsValidAddress(pNode, sizeof(CNode)));
pNode = pNode->pNext; // start after the one specified
}
for (; pNode != NULL; pNode = pNode->pNext)
if (CompareElements<TYPE>(&pNode->data, &searchValue))
return (POSITION)pNode;
return NULL;
}
再看CList的NewNode之前,先看看MFC中简单粗暴的CPlex内存管理方法
通过afxplex_.h和Plex.cpp来看CPlex类是如何管理内存的
struct CPlex
{
CPlex* pNext;
// BYTE data[maxNum*elementSize];
void* data() { return this+1; }
static CPlex* PASCAL Create(CPlex*& head, UINT_PTR nMax, UINT_PTR cbElement);
// like 'calloc' but no zero fill
// may throw memory exceptions
void FreeDataChain(); // free this one and links
};
首先看Create
CPlex* PASCAL CPlex::Create(CPlex*& pHead, UINT_PTR nMax, UINT_PTR cbElement)
{
ASSERT(nMax > 0 && cbElement > 0);
if (nMax == 0 || cbElement == 0)
{
AfxThrowInvalidArgException();
}
//重点关注下面这里
CPlex* p = (CPlex*) new BYTE[sizeof(CPlex) + nMax * cbElement];
// may throw exception
p->pNext = pHead;
pHead = p; // change head (adds in reverse order for simplicity)
return p;
}
由着几行来看他的结构
CPlex* p = (CPlex*) new BYTE[sizeof(CPlex) + nMax * cbElement];
// may throw exception
p->pNext = pHead;
pHead = p; // change head (adds in reverse order
每次在分配内存的时候,函数的后面两个参数是用户分配的内存,而实际多分配了一个CPlex内存来保存指针,以便让这个”链表链起来”,传入的Head表示链表的头节点,从类的声明来看,并没有成员变量保存这个头结点,这个头结点由使用者自己保存,首次Create内存时,头结点置NULL传入即可。
void* data() { return this+1; }
通过CPlex*类型的指针来获取内存块,this+1也就是上图中的Block位置,this也就是上图中的内存块的首地址。
void CPlex::FreeDataChain() // free this one and links
{
CPlex* p = this;
while (p != NULL)
{
BYTE* bytes = (BYTE*) p;
CPlex* pNext = p->pNext;
delete[] bytes;
p = pNext;
}
}
使用者通过链表的首址Head来调用这个方法即可。
用这种方式管理内存,每次多分配一个sizeof(CPlex)
具体使用,看看后面CList是如何来使用这个CPlex类的
上面函数一直在用NewNode来创建节点,那么他是否每次都需要重新”new”一个Node?首先,构造函数中已经把m_pNodeFree初始化为了NULL,m_nBlockSize初始化为了10(通过默认参数,使用者可选)
template<class TYPE, class ARG_TYPE>
typename CList<TYPE, ARG_TYPE>::CNode*
CList<TYPE, ARG_TYPE>::NewNode(CNode* pPrev, CNode* pNext)
{
if (m_pNodeFree == NULL)
{
//如果备用的Node没有了(Null==m_pNodeFree),就要新建内存来存储备用的Node
CPlex* pNewBlock = CPlex::Create(m_pBlocks, m_nBlockSize,
sizeof(CNode));
// chain them into free list
CNode* pNode = (CNode*) pNewBlock->data();
// free in reverse order to make it easier to debug
pNode += m_nBlockSize - 1;
for (INT_PTR i = m_nBlockSize-1; i >= 0; i--, pNode--)
{
pNode->pNext = m_pNodeFree;
m_pNodeFree = pNode;
}
}
ENSURE(m_pNodeFree != NULL); // we must have something
//把备用Node中取出一个Node出来给使用者,此时备用的Node就减少一个
CList::CNode* pNode = m_pNodeFree;
m_pNodeFree = m_pNodeFree->pNext;
pNode->pPrev = pPrev;
pNode->pNext = pNext;
m_nCount++;
::new( (void*)( &pNode->data ) ) TYPE;
return pNode;
}
从上面可以看出,实际上并不是每次都要NewNode,CList根据m_nBlockSize大小已经一次性的New出了m_nBlockSize个Node,当需要Node时,就在已经new的Node中取出一个给使用者。
每一个保存Node的Block内存如下
CList正是这样来预存备用的Node的,这里的Node是备用的
主要代码如下
template<class TYPE, class ARG_TYPE>
void CList<TYPE, ARG_TYPE>::FreeNode(CNode* pNode)
{
//析构Node保存的对象
pNode->data.~TYPE();
//之后把Node重新放入储备的Node当中,以便下次NewNode
pNode->pNext = m_pNodeFree;
m_pNodeFree = pNode;
m_nCount--;
// if no more elements, cleanup completely
if (m_nCount == 0)
RemoveAll();
}
FreeNode就是”释放”一个Node,这里的释放实质上是把Node又放回备用Node的储备中,由已”分配状态“变为”待分配状态“。直观的做法是m_pNodeFree前移,m_pNodeFree前移就表示可分的Node多了一个
主要代码如下
template<class TYPE, class ARG_TYPE>
void CList<TYPE, ARG_TYPE>::RemoveAll()
{
//释放链表(这个链表是使用者直接使用的链表)
CNode* pNode;
for (pNode = m_pNodeHead; pNode != NULL; pNode = pNode->pNext)
pNode->data.~TYPE();
m_nCount = 0;
m_pNodeHead = m_pNodeTail = m_pNodeFree = NULL;
//释放存储"备用Node"的CPlex
m_pBlocks->FreeDataChain();
m_pBlocks = NULL;
}
释放整个CList保存的链表空间,其中分两步
1.释放链表(这个链表是使用者直接使用的链表)
2.释放存储”备用Node”的CPlex
注意:和CArray一样,妥善处理拷贝构造函数和析构函数,CList操作中多次涉及到拷贝构造函数。小心使用防止内存泄漏