在编写一个涉及到矢量图形操作系统和空间数据拓扑关系的系统的时候,我利用了CArray来存储空间数据。
在编程过程中,我发现了由于过分信任CArray的功能而引起的一个很不容易察觉的内存泄漏。让我们首先来看看下面的一个类定义:
Class CBreakPoint{
public:
……
double *xy; //the coordinate of these breakpoint,
int nID; //折点的全局唯一标识符
……
}
CBreakPoint::~CBreakPoint()
{
if(xy) delete[]xy;
xy = NULL;
}
CBreakPoint类定义了折线的折点,其中xy表示折点的数组,我们将在具体输入折点的时候对xy进行内存空间申请;nNum表示一条折线中折点的数目;nID表示的折点的全局唯一标识符。再让我们来看一看折线类的定义:
Class CBreakLine{
public:
……
CBreakPoint* m_pBreakPoint;
int nID; //折线的全局唯一标识符
int nVertexID1; //折线的起始端点的nID
int nVertexID2; //折线的结尾端点的nID
int nNum; //该折线中折点的数目
int state; //折线的状态,0表示孤立,1表示连通,在未开始遍历前状态值为0
……
}
类CBreakLine的构造函数和析构函数如下:
CBreakLine::CBreakLine()
{
……
m_pBreakPoint = new CBreakPoint;
}
CBreakLine::~CBreakLine()
{
……
if(m_pBreakPoint) delete m_pBreakPoint;
m_pBreakPoint = NULL;
……
}
CBreakLine类定义了一个基本的空间元素——一条包含若干个折点的折线,在CBreakLine类的构造函数中,我们对m_pBreakPoint进行了内存分配:
CBreakLine::CBreakLine()
{
……
m_pBreakPoint = new CBreakPoint;
……
}
有了折点和折线类的定义,我们可以开始管理我们的折线了。因为在一幅矢量图形中会存在不确定的n条折线,要把这些折线统一的管理起来,我们采用了CArray模板类。首先定义一个集合类CbreakLineSet,在CbreakLineSet类中,定义一个CArray数组成员变量m_breakLises,这个变量代表了空间中所有的折线。类的定义如下:
Class CBreakLineSet{
public:
……
CArray <CBreakLine, CBreakLine&> m_breakLines;
……
}
我们可以利用CArray的成员函数Add(ARG_TYPE newElement)在数组的末尾添加元素(如果必要,扩展数组),利用下标操作符[ ]获取数组项。其他的许多成员函数都使得我们操作m_BreakLines非常的容易,而且,由于CArray由CObject派生,因此本身支持串行化,这样我们可以把折线数据保存到磁盘上。
现在我们来看一下前面定义的三个类,第一层是CBreakPoint,它代表了线上的折点,包括折线的两个端点;第二层是CBreakLine,它代表了折线;第三层是CBreakLineSet,用它来统一管理系统中所有的空间中的折线。最后我们定义集合管理类CSetManage:
Class CSetManage{
public:
……
CbreakLineSet lines, tempLines;
CArray<int, int&> vertexID; //用作堆栈,在遍历过程中存放端点ID
……
public:
……
void Trvel(int nFirstBreakPointID, CbreakLineSet* pLines)
……
}
看起来一切都非常的顺利,现在我们可以利用CArray的成员函数自由的操作m_BreakLines数组。当文件打开时,所有的折线数据都读入到对象
breakLineSet中。
但是,问题出现了。
为了对空间数据进行统计分析,系统要求把所有不连通的折线(即端点和其他折线的端点重合)都删除掉。我们要根据折线的状态判定折线是否连通。因为在任意的时刻,折线的状态并不全部为0,这就要求要有一个临时的空间存放所有的折线并把它们的状态设为0,然后再开始遍历。我采用了如下算法:先把lines中所有的折线复制到tempLines中,删除lines中的所有折线。然后遍历tempLines中的折线,标记连通的折线的状态为1,在遍历结束后,把tempLines中所有状态为1的复制到lines中,然后删除tempLines中的所有数据。
我们采用了如下的语句,请注意:
①for(int i=0; i<lines.m_breakLines.GetSize(); i++)
{
tempLines.m_breakLines.Add(lines.m_breakLines[i]);//
tempLines.m_breakLines[i].state = 0;
}
lines.m_breakLines.RemoveAll(); //清空lines中的所有折线
根据系统的其他因素,我们可以选定tempLines中的一条折线的一个端点作为起始点,然后开始对tempLines中的整个折线网络进行遍历。遍历时采用了递归的思想,具体函数代码如下:
int g_nLines=tempLines.m_breakLines.GetSize(); //系统中折线的数目
void CSetManage::Travel(int nFirstPointID)
{
int nSecondPointID; //折线另一端的折点的nID
for(int i=0; i< g_nLines; i++){
if(tempLines.m_breakLines[i].state == 0
&&( tempLines.m_breakLines[i].nVertexID1 == nFirstPointID
|| tempLInes.m_breakLines[i].nVertexID2 == nFirstPointID))
{
tempLines.m_breakLines[i].state = 1;
nSecondPointID = tempLines.GetOtherVertexID(nFirstPointID);
//获得另一个端点的nID
vertexID.Add(nSecondPointID);
Travel(nSecondPointID);
}
if(i==g_nLines)
{
if(vertexID.GetSize()>0)
{
vertexID.RemoveAt(vertexID.GetSize()-1); //弹出堆栈顶元素
if(vertexID.GetSize()>0) Travel(vertexID[vertexID.GetSize()-1]);
else return;
}
else return;
}
else return;
}
}
②for(int i=0; i<g_nLines; i++)
{
if(tempLines.m_breakLines[i].state == 1)
lines.m_breakLines.Add(tempLines.m_breakLines[i]); //
}
g_nLines = lines.m_breakLines.GetSize();
tempLines.m_breakLines.RemoveAll(); //清空所有的临时分配的空间
到这里,我们开始编译程序,程序运行时一切都很正常,可是在退出程序时出现了错误。跟踪调试,发现问题出在CBreakLines的析构函数中,此时m_pBreakPoint指针所指向的内容已经面目全非。
问题出在那一步呢?肯定是在折线的反复复制、删除过程中。我们知道CArray在添加元素时,会为新的项分配空间,但是它不会为模板使用的类(但前程序中是CBreakLine)类中的指针分配空间,即不会为m_pBreakPoint->xy指针分配新的空间。因此,就是说,当调用
tempLines.m_breakLines.Add(lines.m_breakLines[i]);
时,tempLines中的m_pBreakPoint指向的xy是和lines中m_pBreakPoint指向的xy完全相同的,而在
lines.m_breakLines.RemoveAll(); //清空lines中的所有折线
语句后,函数层层调用各个类的析构函数(CBreakPoint->CBreakLine->CbreakLi
neSet),此时tempLines.m_breakLines[i].m_pBreakPoint指向的内容被清空掉了,但是lines.m_breakLines[i].m_pBreakPoint仍旧存在,其值(指针指向的地址)保留原来的地址(这很危险,指向了不再属于它的空间)。但是程序下面的操作仍旧能够正常执行,因为不涉及到xy的操作。
直到退出程序时,才又一次调用了lines.m_breakLines[i]的析构函数:
if(m_pBreakPoint) delete m_pBreakPoint;
这时,危险爆发了,删除不明的指针导致了内存泄漏。
找到了问题的根源,解决它就很简单了。我们可以重载操作符”=”,取代CArray的Add()函数,在重载操作符时为每一个m_pBreakPoint->xy分配内存空间,使它和原来的指针指向不同的地址,这样当调用RemoveAll时,就不会影响到复制的折线的数据了。重载的函数很简单,如下:
CBreakLine& CBreakLine::operator =(CBreakLine &p)
{
int i=0;
……
if(m_pBreakPoint->xy!=NULL)
{
delete[]m_pBreakPoint->xy;
m_pbreakPoint->xy=NULL;
};
breakPoint->xy = new double[nNum*2];
for(i=0; i<nNum*2; i++)
{
breakPoint->xy[i] = p.breakPoint->xy[i];
}
……
return *this;
}
接下来,添加
再把所有的红色标注的代码分别替换如下:
①tempLines.SetSize(g_nLines);
for(int i=0; i<lines.m_breakLines.GetSize(); i++)
{
tempLines.m_breakLines[i] = lines.m_breakLines[i];
tempLines.m_breakLines[i].state = 0;
}
添加
lines.SetSize(g_nLines);
替换原有代码如下:
int k = 0;
for(int i=0; i<g_nLines; i++)
{
if(tempLines.m_breakLines[i].state == 1)
lines.m_breakLines[k++] = tempLines.m_breakLines[i];②
}
g_nLines = k;
这样,因为我们为所有的新的折线以及折线的折点分配了新的空间,就不会出现内存空间在分配何时方式产生的不明显的冲突。