浅谈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();

经典的错误,标准的零分。创建了一堆对象,结果只释放了一个!