内存泄漏
什么是内存泄漏?
内存泄漏(Memory Leak)指的是程序在动态分配内存后未能正确释放已分配的内存,导致这些内存块无法被再次使用或回收。内存泄漏的发生主要是在使用堆内存(通过new
或malloc
分配的内存)时没有调用相应的delete
或free
来释放内存。
由于Qt是一个c++的框架,谈论Qt内存泄漏本质上就是在说c++内存泄漏
c++内存分配
谈内存泄露之前,需要先知道c++的内存分配情况
(1)栈区:由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似,栈区有以下特点:
1)由系统自动分配。比如在函数运行中声明一个局部变量int b = 10;,系统自动在栈中为b开辟空间;
2)只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
(2)堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收;分配方式类似于链表,堆区有以下特点:
1)需要程序员自己申请,并指明大小,在C中是有malloc函数,在C++中多使用new运算符(从C++角度上说,使用new分配堆空间可以调用类的构造函数,而malloc()函数仅仅是一个函数调用,它不会调用构造函数,它所接受的参数是一个unsigned long类型。同样,delete在释放堆空间之前会调用析构函数,而free函数则不会)。
2)在操作系统中有一个记录空闲内存地址的表,这是一种链式结构。它记录了有哪些还未使用的内存空间。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
(3)全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放。
(4)文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放。“常量”是指它的值是不可变的,同时,虽然常量也是存储在内存的某个地方,但是无法访问常量的地址的。
(5)程序代码区:存放函数体的二进制代码。但是代码段中也分为代码段和数据段。
关于C++delete于new
在C++中,我们经常使用new操作符来进行内存分配,其内部主要做了两件事: 通过operator new从堆上申请内存(glibc下,operator new底层调用的是malloc) 调用构造函数(如果操作对象是一个class的话) 对应的,使用delete操作符来释放内存,其顺序正好与new相反: 调用对象的析构函数(如果操作对象是一个class的话) 通过operator delete释放内存
Qt内存管理
Qt 的内存管理机制相较于纯 C++ 更为高级和自动化。主要通过父子关系和智能指针来管理对象的生命周期,减少内存泄漏的风险。
1. 父子关系机制
Qt 提供了一种父子关系机制,通过设置父对象,自动管理子对象的生命周期。这种机制主要依靠 QObject 类及其派生类来实现。如果其 parent 非 nullptr,那么其 parent析构时会自动析构该对象。Qt 使用父子关系来管理对象的生命周期,确保子对象在父对象销毁时自动被销毁,从而避免内存泄漏。
- 通过父对象的 QObject构造函数传递父对象指针,或者使用setParent()方法来设置父对象。
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget window;
// 使用父子关系,按钮的内存由窗口管理
QPushButton *button = new QPushButton("Click me", &window);
window.resize(200, 100);
window.show();
return app.exec();
}
在上述代码中按钮 button的父对象是窗口 window。当 window 被销毁时button也会自动被销毁。
2.QWidget 及其派生类的对象
可以设置 Qt::WA_DeleteOnClose 标志位。当窗口关闭时,该对象会被自动析构。这对于临时窗口或对话框非常有用。
QWidget *window = new QWidget;
window->setAttribute(Qt::WA_DeleteOnClose);
window->show();
3. QAbstractAnimation 派生类的对象
可以设置 QAbstractAnimation::DeleteWhenStopped属性。当动画停止时,该对象会自动删除。
QPropertyAnimation *animation = new QPropertyAnimation(object, "geometry");
animation->setDuration(1000);
animation->setStartValue(QRect(0, 0, 100, 30));
animation->setEndValue(QRect(250, 250, 100, 30));
animation->setEasingCurve(QEasingCurve::OutBounce);
animation->setDeleteWhenStopped(true);
animation->start();
4. QRunnable::setAutoDelete() 和 MediaSource::setAutoDelete()
QRunnable对象可以使用 QRunnable::setAutoDelete(true) 设置为自动删除。当 QRunnable 对象完成执行时,会自动删除。
类似地,MediaSource对象也可以通过 MediaSource::setAutoDelete(true)设置为自动删除
5. 智能指针
C++11 引入了智能指针,Qt 也提供了一些智能指针类,如 `QScopedPointer`、`QSharedPointer` 和 `QPointer`。
QScopedPointer
QScopedPointer 是一个简单的智能指针类,用于管理动态分配的对象。当 QScopedPointer 超出作用域时,自动删除其管理的对象。
#include <QScopedPointer>
#include <QPushButton>
void createButton() {
QScopedPointer<QPushButton> button(new QPushButton("Click me"));
// button 在函数结束时自动删除
}
QSharedPointer
QSharedPointer 是一个引用计数智能指针类,用于共享所有权。当最后一个 QSharedPointer被销毁时,删除其管理的对象。
#include <QSharedPointer>
#include <QPushButton>
void sharedButton() {
QSharedPointer<QPushButton> button1(new QPushButton("Click me"));
{
QSharedPointer<QPushButton> button2 = button1;
// button1 和 button2 共享同一个 QPushButton 对象
}
// button2 超出作用域,引用计数减少但对象不删除
// button1 超出作用域,引用计数为零,对象删除
}
QPointer
QPointer是一个弱指针,用于监视 QObject 派生类对象。当被监视对象被销毁时,QPointer 自动变为 nullptr,避免空指针。
#include <QPointer>
#include <QPushButton>
void weakPointer() {
QPointer<QPushButton> button(new QPushButton("Click me"));
delete button;
// button 现在为 nullptr,避免悬空指针
}
6. QObjects 和子对象的自动销毁
除了通过父子关系和智能指针管理内存,Qt 中所有继承自 QObject的对象也可以利用其内置的子对象管理功能。在对象的析构函数中,会自动销毁所有子对象。
class MyWidget : public QWidget {
public:
MyWidget(QWidget* parent = nullptr) : QWidget(parent) {
button = new QPushButton("Click me", this); // this 是 button 的父对象
}
~MyWidget() {
// 不需要显式删除 button,父对象会自动销毁所有子对象
}
private:
QPushButton* button;
};
引起内存泄漏常见的原因以及解决办法
(1)在类的构造函数和析构函数中没有匹配的调用new和delete函数
两种情况下会出现这种内存泄露:
1)在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;
2)在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存。
例子
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel *label = new QLabel("Hello Qt!");
label->show();
return a.exec();
}
分析:label 既没有指定 parent,也没有对其调用 delete,所以会造成内存泄漏
解决方法:
#include <QApplication>
#include <QLabel>
//方法一:对象创建到栈上而不是堆上
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel label("Hello Qt!");
label.show();
return a.exec();
}
//方法二:设置标志位,close后会delete label(注意只有分配到栈上才能这样用,要不然就delete栈空间了)
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QLabel *label = new QLabel("Hello Qt!");
label->setAttribute(Qt::WA_DeleteOnClose);
label->show();
return a.exec();
}
//方法三:手动释放
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
int ret = 0;
QLabel *label = new QLabel("Hello Qt!");
label->show();
ret = a.exec();
delete label;
return ret;
}
(2)delete栈空间
例子
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello Qt!");
label.show();
label.setAttribute(Qt::WA_DeleteOnClose);
return app.exec();
}
分析
程序崩溃,因为 label 被 close 时,delete &label; 但 label 对象是在栈上分配的内存空间,delete 栈上的地址会出错。
有些朋友理解为 label 被 delete 两次而错误,可以测试QLabel label("Hello Qt!"); label.show();delete &label;
第一次 delete 就会出错。
解决方法
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Hello Qt!");
label.show();
//注释掉
//label.setAttribute(Qt::WA_DeleteOnClose);
return app.exec();
}
(3)子对象先于父对象创建
例子
#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QLabel label("Hello Qt!");
QWidget w;
label.setParent(&w);
w.show();
return app.exec();
}
分析
w 比 label 先被析构,当 w 被析构时,会删除 chilren 列表中的对象 label,但 label 是分配到栈上的,因 delete 栈上的对象而出错。
解决方法
#include <QApplication>
#include <QLabel>
//方法一:交换父子对象创建顺序
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget w;
QLabel label("Hello Qt!");
label.setParent(&w);
w.show();
return app.exec();
}
//方法二:将子对象创建到堆上
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QWidget w;
QLabel *label = new QLabel("Hello Qt!");
label->setParent(&w);
w.show();
return app.exec();
}
(4)没有正确地清除嵌套的对象指针
例子
#include <QWidget>
#include <QPushButton>
class MyWidget : public QWidget {
public:
MyWidget() {
button = new QPushButton("Click Me", this);
// 错误:没有删除 button
}
~MyWidget() {
// 忘记删除 button
}
private:
QPushButton* button;
};
分析
虽然 QPushButton
由 MyWidget
作为父对象管理,但在 MyWidget
中声明 QPushButton* button
并在构造函数中使用 new
分配内存时,应该在析构函数中显式地释放 button
,以避免潜在的资源泄漏。
解决方法
#include <QWidget>
#include <QPushButton>
//方法一:在析构函数中添加delete
class MyWidget : public QWidget {
public:
MyWidget() {
button = new QPushButton("Click Me", this);
}
~MyWidget() {
// 正确:删除 button
delete button;
}
private:
QPushButton* button;
};
(3)在释放对象数组时在delete中没有使用方括号
方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。
例子
#include <QWidget>
class MyWidget : public QWidget {
public:
MyWidget() {
dataArray = new int[10];
}
~MyWidget() {
delete dataArray; // 错误:没有使用方括号
}
private:
int* dataArray;
};
分析
只会释放第一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露
解决方法
#include <QWidget>
class MyWidget : public QWidget {
public:
MyWidget() {
dataArray = new int[10];
}
~MyWidget() {
delete[] dataArray; // 正确:使用方括号
}
private:
int* dataArray;
};
(4)指向对象的指针数组不等同于对象数组
对象数组是指:数组中存放的是对象,只需要delete [ ] p,即可调用对象数组中的每个对象的析构函数释放空间
指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete [ ] p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。
例子
#include <QWidget>
class MyObject {
public:
MyObject() {}
~MyObject() {}
};
int main() {
MyObject* objectArray[10];
for (int i = 0; i < 10; ++i) {
objectArray[i] = new MyObject();
}
// 错误:只释放了指针数组的内存,没有释放对象
delete[] objectArray;
}
分析
delete [ ] objectArray只是释放了每个指针,但是并没有释放对象的空间
解决方法
#include <QWidget>
class MyObject {
public:
MyObject() {}
~MyObject() {}
};
int main() {
MyObject* objectArray[10];
for (int i = 0; i < 10; ++i) {
objectArray[i] = new MyObject();
}
// 正确:先删除每个对象,然后删除指针数组
for (int i = 0; i < 10; ++i) {
delete objectArray[i];
}
}
(5)缺少拷贝构造函数
两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。按值传递会调用(拷贝)构造函数,引用传递不会调用。
在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数(值拷贝,浅拷贝),复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。
所以,如果一个类里面有指针成员变量,要么重写写拷贝构造函数(深拷贝)和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符。
例子
#include <QWidget>
class MyObject {
public:
MyObject() {
data = new int[100];
}
~MyObject() {
delete[] data;
}
private:
int* data;
};
int main() {
MyObject obj1;
MyObject obj2 = obj1; // 错误:隐式拷贝构造函数导致两个对象指向同一块内存
}
分析
隐式拷贝构造函数导致两个对象指向同一块内存,main函数结束时,对象会释放两次
解决方法
#include <QWidget>
class MyObject {
public:
MyObject() {
data = new int[100];
}
~MyObject() {
delete[] data;
}
//重写拷贝构造函数,使其为深拷贝
MyObject(const MyObject& other) {
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
delete[] data;
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
return *this;
}
private:
int* data;
};
(6)缺少重载赋值运算符
这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露.
例子
#include <QWidget>
class MyObject {
public:
MyObject() {
data = new int[100];
}
~MyObject() {
delete[] data;
}
private:
int* data;
};
int main() {
MyObject obj1;
MyObject obj2;
obj2 = obj1; // 错误:隐式赋值运算符导致两对象共享相同内存
}
分析
隐式拷贝构造函数导致两个对象指向同一块内存,main函数结束时,对象会释放两次
解决方法
#include <QWidget>
class MyObject {
public:
MyObject() {
data = new int[100];
}
~MyObject() {
delete[] data;
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
delete[] data;
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
return *this;
}
private:
int* data;
};
(7)关于nonmodifying运算符重载的常见错误
1)返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针(指向被释放的或者访问受限内存的指针);
2)返回内部静态对象的引用;
3)返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收。
解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,而应该是类型的返回值,即不是 int&而是int。
例子
class MyObject {
public:
int value;
MyObject(int v) : value(v) {}
int& operator++() {
return value; // 错误:返回栈上对象的引用
}
};
分析
因为栈上的对象自己会释放,当你访问栈上的对象或者数据时可能访问的不是你所需要的数据
解决方法
class MyObject {
public:
int value;
MyObject(int v) : value(v) {}
int operator++() {
return value;
}
};
(8)没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是虚函数,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
造成野指针的原因:
1)指针变量没有被初始化(如果值不定,可以初始化为NULL);
2)指针被free或者delete后,没有置为NULL, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL;
3)指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针;
4)shared_ptr循环引用。
例子
class Base {
public:
Base() {}
~Base() {} // 错误:未定义为虚函数
};
class Derived : public Base {
public:
Derived() {}
~Derived() {} // 子类的析构函数不会被调用
};
分析
解决方法
class Base {
public:
Base() {}
virtual ~Base() {} // 正确:基类的析构函数为虚函数
};
class Derived : public Base {
public:
Derived() {}
~Derived() {}
};
(9)析构的时候使用void*
delete掉一个void*类型的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露。
例子
#include <iostream>
class MyClass {
public:
MyClass() {
data = new int[10]; // 分配内存
}
~MyClass() {
delete[] data; // 释放内存
}
private:
int* data;
};
int main() {
MyClass* obj = new MyClass(); // 创建对象
void* ptr = static_cast<void*>(obj); // 错误:将 MyClass 对象转换为 void*
delete static_cast<MyClass*>(ptr); // 错误:使用 void* 删除,析构函数不会被调用
return 0;
}
分析
创建了一个 MyClass
对象,并将其指针转换为 void*
。使用 void*
指针来执行 delete
操作。由于 void*
不包含类型信息,编译器无法调用正确的析构函数,MyClass
对象的析构函数不会被调用,从而导致内存泄漏。
解决方法
程序异常崩溃(持续更新)
参考:
Qt浅谈之一:内存泄露(总结)_qprocess内存泄露-CSDN博客
C++ 内存管理中内存泄漏问题产生原因以及解决方法 - Jcpeng_std - 博客园 (cnblogs.com)
Qt 难找的意料之外原因、崩溃原因及错误原因(持续更新)_qt designer中总是闪退该怎么办-CSDN博客