浅谈C++的内存泄露问题
作者[@Cyilin]
2022 年 7 月 28 日
在C++项目中,最让程序员头疼的问题中,内存泄露定是名列前茅,而C++也是在进行着更新换代,利用一些新的机制来解决困扰程序员的问题。
首先来谈谈内存泄露的定义,以及如何检测与避免?
概念很简单,在堆上动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。
内存泄露的原因
造成内存泄漏的几种原因:
1)类的构造函数和析构函数中==new和delete没有配套==
2)在释放对象数组时没有使用==delete[]==,使用了delete
3)==没有将基类的析构函数定义为虚函数==,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露
class X {
private:
int x;
};
class Y: public X {
private:
int y;
};
int main(){
X* x = new Y;
delete x;
}
程序员没有为基类 X 和派生类 Y 定义析构函数,当在主函数内 delete 基类指针 x 的时候,需要调用基类的析构函数。于是,编译器会隐式自动的为类 X 生成一个析构函数,从而可以成功的销毁 x 指向的派生类对象中的基类子对象(即 int 型成员变量 x)。
小提示:
在C++中,构造函数的顺序是基类到子类;但是析构的时候,方向是反的,从子类到基类。因此在上面的例子中,没有指明虚函数的前提下,delete x ,所调用的析构的函数是父类的,以至于子类的资源没有被析构掉。
但是,这段代码存在内存泄露的问题,当利用 delete 语句删除指向派生类对象的指针 x 时,系统调用的是基类的析构函数,而非派生类 Y 类的析构函数,因此,编译器无法析构派生类的 int 型成员变量 y。
因此,一般情况下我们需要将基类的析构函数定义为虚函数,当利用 delete 语句删除指向派生类对象的基类指针时,系统会调用相应的派生类的析构函数(实现多态性),从而避免内存泄露。但是编译器隐式自动生成的析构函数都是非虚函数,这就需要由程序员手动的为基类 X 定义虚析构函数
再啰嗦一下,我们在使用virtual 修饰基类的析构函数时,可以再锦上添花地加上 = default 关键字,提高点性能。
例如
class X{
public:
virtual ~X() = default;
}
......
避免方法
a. new 和 delete 要配套;
b. 利用C++的RAII(资源获取即初始化)机制;
- 智能指针
- std::move
- 等等...
c. 将基类的析构函数设为虚函数;
OpenCV的内存泄露问题
这个问题之前是算法人员提出的,稍加深入了解才发现OpenCV库中的蹊跷。
其实解决内存泄露,最好的是心里得有杆秤,创建了一个对象应该时应该要outline它的生命轨迹了。
不过OpenCV后来好像也支持了类似智能指针的东西,名称是Ptr。
注意点1
OpenCV中很多数据结构与对象都有一个release方法,记得用完这些对象跟数据容器之后调用release/destory方法。最典型的就是Mat对象的release方法,调用release并不会重根本上保证立刻回收内存,==它只是让对象的引用计数减一==(引用计数这个思想,共享智能指针使用过,操作系统的软链接也使用过),只有当对象的引用计数为0的时候,才会回收内存。此外release方法还是一个原子操作,也可以线程中被调用。
注意点2
Mat m1;
for (int i = 0; i < 100; i++) {
m1 = Mat::zeros(Size(512, 512), CV_8UC3);
}
imshow("input-m1", m1);
m1.release();
经典的错误,标准的零分。创建了一堆对象,结果只释放了一个!