OpenCV程序内存泄露的预防与检测

I. 内存泄露

    首先我们要搞清楚什么是内存泄露。一个应用程序在运行时占用内存区域可以分为五个:栈区、堆区、自由存储区、静态区、常量区(不讨论代码区)。栈(Stack)区存放局部变量,也就是在一个函数或代码段中定义的变量,局部变量由系统自动进行分配和释放;堆(Heap)区和自由存储区存放我们程序运行过程中动态分配的内存,主要由C语言的malloc函数和C++的new运算符进行分配(OpenCV中的cvCreateImage等函数正是调用了它们才能给图像分配一块存储区域),堆区中的内存空间不会被系统自动释放,使用完后需要由程序进行清理,如果堆区中的空间没有被程序释放,应用程序结束后系统才会回收这片区域;静态区存放全局变量和静态变量,静态区在程序开始时由系统自动分配,在程序运行过程中不改变,直到应用程序结束;常量区存放程序中定义的常量。

    栈区的大小是有上限的,所以我们不能在一个函数中定义过多的局部变量或过大的数组,如果一个函数中定义的局部变量所占用的内存之和超过一个阈值(这个阈值在不同系统和编译器中是不一样的,有些编译器如VS可以调整这个阈值),当程序执行到这个函数时就会提醒栈溢出(Stack Overflow,最著名的编程问答网站也叫这个名字,这个网站每月有几千万的活动用户帮你解答各种程序问题)。我们都知道平面图像其实就是一个二维数组,稍微大一点的图像就不可能作为局部变量存放在栈区,所以cvCreateImage等函数就在堆区中分配了一段内存空间,将图像的实际内容存储在里面,只把这段内存的首地址返回供我们使用。当我们分配了堆区空间,使用完后却忘记释放时,这段没有价值内存在应用程序运行过程中就会一直被占用,这就是内存泄露。随着应用程序的运行,内存泄露会越来越严重,直到耗尽全部物理内存或超过系统限制,程序就会报错退出。即使内存泄露没有严重到耗尽内存,也会拖慢程序的运行速度。由于图像处理程序会在内存堆区中多次分配大量的内存,稍微一不注意就可能造成严重的内存泄露,是内存泄露的重灾区。

II. 运算符new和delete的使用

    C语言中,使用malloc/free函数进行内存的分配和释放,C++中增加了new/delete运算符来完成这两项功能。new和delete的使用非常简单,只要别忘了delete一般就不会造成内存泄露。需要提示一下的是如果需要创建一个数组,不要搞错语法:

int* ptArray = new int[ARRAY_SIZE];
delete[] ptArray;

    注意不要忘记delete后面的中括号,中括号是在delete后和变量指针前,不是在后面;而且new变量后面的中括号里面有数组的大小,而delete后的中括号里面是空的。如果delete后的中括号里面有数值,VS自带的编译器不会提示错误,G++这种严格的编译器会直接报错。

    下面再给一个定义动态二维数组并释放的例子,用这种方法定义的动态二维数组可以用两个中括号方便地访问:

int** Array2D = new int*[ARRAY_HEIGHT];     //Array2D[i]中存储的实际是每行的头指针
for(int i=0;i

III. OpenCV 1.X数据类型防内存泄露

    OpenCV 1.X中存储图像用的数据类型是IplImage(矩阵CvMat的使用与IplImage十分相似),IplImage的空间分配与释放都需要由程序完成。由于大部分人没有读过OpenCV的源代码,不知道究竟哪些函数分配了内存空间,所以经常会出这种错误:

IplImage* img = cvCreateImage(cvSize(512,512),8,1);
img = cvCloneImage(srcImg);
cvReleaseImage(&img);

    这段程序看似已经释放了img所指图像的内存空间,实际上已经造成了内存泄露。cvCreateImage的作用是分配一段内存空间,cvCloneImage的作用是先分配一段内存空间然后将源图片的内容拷贝到该处。我们执行完第一行代码后,img里存储的是cvCreateImage创建的内存空间首地址,执行完第二行代码后img中的内容就变成了cvCloneImage函数所创建空间的首地址,这样cvCreateImage函数所创建的内存空间就消失在了茫茫的内存海中,没有一个指针能“找到”它,我们既不能使用也不能释放这块区域,直到应用程序关闭。

    所以我们首先要知道哪些函数创建了内存空间,哪些函数使用的是已有内存空间的图像。对于IplImage, 请记住有cvLoadImage、cvCreateImage、cvCloneImage这三个函数进行了内存的分配,其他所有函数如cvCopyImage使用的都是他们三个已经分配好的内存,这三个函数每调用一次就必须对应一个cvReleaseImage,否则就会造成内存泄露。

    除了IplImage和CvMat,其他一些需要分配内存的OpenCV的数据结构也需要释放,如CvMemStorage、IplConvKernel、CvHistogram等,一般来说只要有cvCreateXXX就会有相应的cvReleaseXXX。

    另外,我们检查程序是否有内存泄露时还要注意以下几点:

    1、当函数返回一个图像指针或函数参数中有指针引用时,注意这个指针指向的图像是不是新创建的,比如:

IplImage* CreateAndCloneImage(IplImage* &Img)
{
     IplImage* img = cvCreateImage(cvSize(512,512),8,1);
     paraImg = cvCloneImage(img);   //不知道为什么是指针引用请Google
     return img;
}

    函数返回值是一个新创建图像的指针,我们使用完成后要记得释放;函数参数是一个指针引用,而且给这个指针赋了一个新创建图像的地址,所以我们传入这个函数的指针一定是一个空指针,否则就会因重复创建造成内存泄露,用完后也要释放。

    2、要在所有可能的分支中释放图像。比如我们在函数开始处创建了一个新图像,而函数中间有if+return组合的话我们就不能只在函数最后加释放语句,否则当if的条件成立时程序就会造成内存泄露。同样,我们在某个循环开始处新建了一个图像,我们要在循环正常结束前、每个break和每个continue前加释放语句。如果程序中有goto语句的话更要仔细检查。

    3、一个图像指针定义时如果没有创建图像,最好将指针赋为NULL,尤其是全局变量或类成员变量。释放图像后也尽量将指针赋为NULL。因为VisualStudio生成的程序不管是在Debug模式下或是在Release模式下,新定义的指针默认都不是NULL,如果我们手动将指针赋为NULL,我们就可以通过判断是不是空指针来判断图像是不是被创建过,防止因重复创建图像造成的内存泄露,如下面这段程序一样:

If(m_img != NULL)
     cvReleaseImage(&m_img);
m_img = cvCreateImage(cvSize(512,512),8,1);

IV. OpenCV 2.X的数据类型

    OpenCV从2.0开始使用C++进行实现。借助于RAII,OpenCV 2.X的新数据类型Mat几乎能避免全部的内存泄露,而且不需要我们再耗费大量精力去检查是否有图像未被释放。Mat类的的析构函数会自动调用Mat::Release,除非我们有特殊需要就不用再调用它了。而且Mat::Create和Mat::Clone等函数会自动检测图像是否已经被创建过,如果图像已经被创建的话在本次创建前自动释放掉,这样就避免了重复创建造成的内存泄露。因此如果程序用的是C++的话,强烈推荐使用OpenCV2.X的数据类型和函数! IplImage和Mat相互之间的转换非常方便,不必担心新老程序间的相互调用。查询新版本函数的话可以下载一个最新的PDF说明文档,搜索老版函数就能找到对应的新版函数。

V. 内存泄露检测工具

    检测内存泄露是件非常枯燥的工作,而且难免有疏漏。这里推荐一个用于Visual Studio的免费内存泄露检测工具Visual Leak Detect. 下载地址可以从Google搜索,最新版本是2.3版。VLD的配置非常方便,先将下载下来的exe文件安装到某个目录,再将该目录下的include文件夹加入到工程的头文件目录中,将lib/Win32或lib/Win64加入到工程的库目录中(方法跟OpenCV配置差不多)。最后在任意一个源文件中加入#include “vld.h”就好了,不需要内存检测或发布程序的时候只要注释掉这一句就可以了。

    另外为了防止同一个内存泄露信息重复出现,我们还需要进行一些配置。到VLD的安装目录下找到vld.ini文件,打开(如果用记事本打开换行出问题的话就用写字板或Vim吧)后找到AggregateDuplicates项,默认为no,改为yes,否则同一个内存泄露的位置可能会重复出现很多次。这个配置文件还有一些其他功能,比如将内存泄露信息导出到文本文件中,如果需要的话可以进行更改。

    配置好vld之后,我们每次调试运行程序时vld就会自动记录哪个位置出现了内存泄露,结束调试(指正常关闭程序,用shift+F5就不会显示)后Viusal Studio的“输出”窗口就会列出所有造成内存泄露的代码位置,如下图所示:

OpenCV程序内存泄露的预防与检测_第1张图片

    VLD中一条内存泄露的位置信息是所调用的全部堆栈帧的信息,里面不仅有我们自己写的函数还有系统函数或OpenCV函数。那么怎么找到是哪一个文件中的函数造成了内存泄露?这里有个技巧,就是从上往下看文件名,直到找到第一个自己写的源文件,因为一般来说系统函数或OpenCV函数不会造成内存泄露。双击那一行我们就能跟踪到造成泄露那行代码的位置,一般是分配内存的语句。如果没有内存泄露,VLD就会输出如下图所示的信息:

OpenCV程序内存泄露的预防与检测_第2张图片

    注意,如果一次运行没有内存泄露也不要高兴太早,上面我们说过内存泄露需要考虑到所有的分支,这次运行没有检测到泄露只能说明本次运行的分支和功能是没有内存泄露的。所以我们要尽可能多的去跑遍所有分支和功能,把内存泄露的可能性降到最低。

你可能感兴趣的:(CV,OpenCV,C++,内存泄露,图像处理)