【解决内存泄漏的问题】 Qt 框架中的父子对象关系会自动管理内存,父对象会在其销毁时自动销毁所有子对象。

news2024/11/13 18:05:03

修改前的代码

在这里插入图片描述
这段代码可能会出现内存泄漏问题,主要原因是构造函数中创建的 LoginDialogRegisterDialog 对象未在合适的地方被正确释放。具体分析如下:

1. 构造函数中的问题

_login_dlg = new LoginDialog();
setCentralWidget(_login_dlg);
_login_dlg->show();

connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
_reg_dlg = new RegisterDialog();
  • MainWindow 的构造函数中,使用了 new 操作符创建了 _login_dlg_reg_dlg 对象。这两个对象在堆上分配了内存,但是后面代码并没有显示地管理它们的生命周期。
  • 这里的 _login_dlg 被设置为 setCentralWidget,这意味着 MainWindow 将会管理这个 widget 的生命周期。通常,当 MainWindow 被销毁时,作为中央窗口的小部件也会被自动销毁。

2. 析构函数中的问题

MainWindow::~MainWindow()
{
    delete ui;
    if (_login_dlg) {
        delete _login_dlg;
        _login_dlg = nullptr;
    }

    if (_reg_dlg) {
        delete _reg_dlg;
        _reg_dlg = nullptr;
    }
}
  • 析构函数尝试使用 delete 来释放 _login_dlg_reg_dlg。这里存在两个问题:
    • 如果 _login_dlg 已经通过 setCentralWidget 交由 MainWindow 管理,它可能会在 MainWindow 销毁时自动释放。如果你再次 delete 它,可能会导致重复释放,进而产生未定义行为。
    • _reg_dlg 并没有添加到窗口层次结构中,只有通过 connect 和信号槽进行通信。这意味着你需要手动管理它的内存,但如果某些情况下没有执行 delete _reg_dlg,则可能会产生内存泄漏。

3. 可能的内存泄漏原因

  • 重复释放_login_dlg 作为 setCentralWidget 的参数,可能已经被 MainWindow 析构函数释放了。如果你在 MainWindow 的析构函数中再次释放它,就会造成重复释放的问题,虽然这不一定导致内存泄漏,但会造成崩溃。
  • 未释放的内存:虽然你已经试图在析构函数中释放 _reg_dlg,但如果由于某些原因在程序执行过程中,_reg_dlg 没有被正确创建或被多次创建,仍然有可能造成内存泄漏。

4. 解决方案建议

  • 移除重复删除:如果 setCentralWidget 已经管理了 _login_dlg 的生命周期,你不需要在析构函数中手动删除它。可以将以下代码移除:

    if (_login_dlg) {
        delete _login_dlg;
        _login_dlg = nullptr;
    }
    
  • 手动管理其他对象的生命周期:对于没有交由 Qt 管理的对象(如 _reg_dlg),确保它在使用后被正确删除。如果你不确定某个对象是否已经被删除,可以使用智能指针(如 std::unique_ptr)来自动管理内存。

修改后的代码,解决内存泄漏问题

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    _login_dlg = new LoginDialog(this);
    setCentralWidget(_login_dlg);
    //_login_dlg->show();

    // 创建和注册消息的链接
    connect(_login_dlg,&LoginDialog::switchRegister,this,&MainWindow::SlotSwitchReg);
    _reg_dlg = new RegisterDialog(this);

    _login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
    _reg_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
    _reg_dlg->hide();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::SlotSwitchReg()
{
    setCentralWidget(_reg_dlg);
    _login_dlg->hide();
    _reg_dlg->show();
}


通过修改代码,将 LoginDialogRegisterDialog 的父对象设置为 MainWindow,有效解决了内存泄漏问题。这是因为 Qt 框架中的父子对象关系会自动管理内存,父对象会在其销毁时自动销毁所有子对象。让我们逐步分析为什么这些改动解决了内存泄漏的问题:

1. 父子对象机制

在 Qt 中,当一个对象(如 QWidget)被创建时,如果为它指定了一个父对象(parent),则这个父对象会负责管理子对象的生命周期。这意味着当父对象被销毁时,它会自动销毁其所有的子对象,无需手动调用 delete

在原代码中,LoginDialogRegisterDialog 被手动创建,并没有指定父对象,因此它们必须手动释放。如果忘记释放它们,或者发生异常退出,会导致内存泄漏。

2. LoginDialogRegisterDialog 作为子对象

在修改后的代码中:

_login_dlg = new LoginDialog(this);
_reg_dlg = new RegisterDialog(this);
  • this(即 MainWindow)作为 LoginDialogRegisterDialog 的父对象被传入构造函数。这样,MainWindow 将自动负责这两个对话框的生命周期管理。
  • MainWindow 析构时,Qt 会自动调用 delete 来释放它的所有子对象,因此不需要手动释放这两个对话框。

3. 避免手动 delete

原代码中,析构函数手动释放了 _login_dlg_reg_dlg,但这容易导致两类问题:

  • 忘记释放:如果忘记调用 delete 或在某些异常情况下没有释放这些对象,会导致内存泄漏。
  • 重复释放:如果对象已经由其他地方(如 setCentralWidget)管理,再次手动释放可能导致崩溃。

修改后的代码不再手动 delete 这些对话框,而是依靠父子关系自动释放,避免了重复释放和内存泄漏的问题。

4. Qt 父对象的自动销毁机制

MainWindow 被销毁时,Qt 会遍历它的所有子对象并自动销毁它们。具体过程如下:

  • MainWindow 被销毁时,它的子对象 _login_dlg_reg_dlg 会自动被销毁。
  • 每个子对象会调用它自己的析构函数,释放分配的资源。

通过这种机制,程序中动态分配的内存被有效管理,避免了手动 delete 带来的内存管理复杂性。

5. 设置 setCentralWidget

setCentralWidget(_login_dlg);
  • setCentralWidget 中设置的控件通常会由 MainWindow 负责销毁。如果你不将 LoginDialog 设置为 MainWindow 的子对象,可能需要手动管理这个控件的内存。
  • 现在由于它有了父对象,内存管理由 Qt 自动完成。

6. 总结

通过将 LoginDialogRegisterDialog 的父对象设置为 MainWindow,你利用了 Qt 的内存管理机制来自动释放内存,解决了内存泄漏问题:

  • 父子对象关系:父对象会自动管理子对象的内存。
  • 无需手动释放内存:不再需要在析构函数中手动 delete 对象,避免了内存泄漏和潜在的重复释放问题。

这使得代码更加简洁、安全,并且更符合 Qt 的内存管理方式。

堆和栈的主要区别

堆(Heap)和栈(Stack)是两种用于存储数据的内存区域,它们在计算机内存管理和程序执行中扮演着不同的角色。以下是堆和栈的主要区别:

1. 内存分配方式

    • 分配方式:栈内存是系统自动分配和释放的,通常用于存储局部变量、函数参数、返回地址等。内存的分配和释放遵循后进先出(LIFO,Last In First Out)的规则。
    • 管理方式:栈由操作系统自动管理,程序不需要手动控制栈内存的分配和释放。
    • 速度:由于栈是由系统自动分配和释放的,因此栈的操作非常快。
    • 分配方式:堆内存是程序员手动分配和释放的,通常通过 newmalloc 分配内存,通过 deletefree 释放内存。
    • 管理方式:堆内存的管理由程序员负责,错误的管理可能会导致内存泄漏或程序崩溃。
    • 速度:堆的分配和释放比栈慢,因为它涉及更多的内存管理操作。

2. 内存的使用模式

    • 栈的内存是按顺序分配的,每次分配时内存块紧跟在上一个内存块后面。当函数结束时,栈指针会自动回退,释放局部变量占用的内存。
    • 典型用于存储局部变量、函数调用栈帧等。
    • 堆内存是动态分配的,内存块可以在程序的不同位置请求分配,大小可以不固定。需要程序员手动管理内存的释放,否则可能导致内存泄漏。
    • 常用于存储在程序中生命周期不确定的对象或数据,比如对象实例、数据结构(链表、树等)。

3. 生命周期

    • 栈上的变量生命周期是确定的,即局部变量在函数执行期间存在,当函数返回时,栈上的内存会自动释放,局部变量随之消失。
    • 堆上的变量生命周期不确定,内存的释放由程序员决定。如果程序员不释放,内存会一直存在,直到程序结束。

4. 内存大小限制

    • 栈的大小是有限的,通常由系统决定。如果递归太深或者局部变量占用内存过多,可能会导致栈溢出(stack overflow)。
    • 堆的大小取决于系统的可用内存,通常比栈大得多。但是由于需要手动管理,使用堆内存时需要特别注意内存泄漏问题。

5. 使用场景

    • 栈适合存储短生命周期的变量和小数据结构,尤其是局部变量、函数调用参数和返回值。
    • 栈的分配速度快,但空间有限。
    • 堆适合存储需要动态分配的大数据结构或需要灵活生命周期管理的对象,比如链表、树、动态数组等。
    • 堆的分配较慢,但空间更大且可以按需分配。

6. 内存管理的复杂性

    • 内存管理简单,系统自动处理内存的分配和释放。
    • 不需要程序员关注内存释放的问题,但栈空间有限,容易产生栈溢出。
    • 内存管理复杂,程序员需要手动控制分配和释放内存。如果忘记释放,可能会造成内存泄漏;如果多次释放,会导致程序崩溃。

7. 内存布局

    • 栈是从高地址向低地址分配的,栈顶向下增长。
    • 堆是从低地址向高地址分配的,堆顶向上增长。

总结

特性栈(Stack)堆(Heap)
分配方式由系统自动分配和释放由程序员手动分配和释放
管理方式操作系统自动管理程序员手动管理
分配速度
生命周期随函数执行完毕后自动释放由程序员控制,直到手动释放或程序结束
内存大小较小,有系统设定的固定大小较大,由系统内存大小决定
使用场景局部变量、函数调用栈帧动态分配的数据结构(如对象、数组)
管理难度简单复杂,容易出现内存泄漏或崩溃

栈适合于局部变量的快速分配和释放,而堆适合更灵活的动态内存管理,但需要程序员小心管理内存的分配和释放。

智能指针(Smart Pointers) 是 C++ 中的一类指针对象,用于自动管理动态分配的内存,避免手动管理内存带来的内存泄漏或重复释放问题。

智能指针主要通过 RAII(Resource Acquisition Is Initialization)原则管理资源,即在智能指针的生命周期内自动管理其指向的资源,当智能指针超出作用域时,自动释放资源。

std::unique_ptr 是 C++11 引入的最常用的智能指针之一,它提供了独占式的所有权语义,表示该指针所指向的对象只能由一个 unique_ptr 拥有,不能被复制。它确保在 unique_ptr 失效或超出作用域时,自动释放所占用的内存,从而避免了手动管理动态分配的内存。

std::unique_ptr 的特点

  1. 独占所有权

    • std::unique_ptr 是独占的,表示它所指向的资源只能有一个 unique_ptr 拥有,不能被复制。
    • 如果需要将所有权转移到另一个 unique_ptr,必须使用 移动语义,即通过 std::move 转移资源所有权。
  2. 自动释放内存

    • unique_ptr 超出其作用域或被显式销毁时,它会自动调用 delete 来释放内存,不再需要手动 delete,避免了内存泄漏问题。
  3. 不能复制

    • std::unique_ptr 禁止拷贝,任何拷贝行为都会被编译器阻止。因此,std::unique_ptr 可以防止资源重复释放或内存泄漏。
  4. 支持自定义删除器

    • std::unique_ptr 允许用户指定自定义的删除器(deleter),可以用来释放特殊类型的资源,比如文件句柄、数据库连接等。

示例代码

#include <iostream>
#include <memory>  // 包含智能指针的头文件

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
    void display() { std::cout << "Hello from MyClass" << std::endl; }
};

int main() {
    // 创建一个 std::unique_ptr 对象,管理动态分配的 MyClass 实例
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();

    // 访问智能指针指向的对象
    ptr->display();

    // 自动释放 MyClass 实例,无需手动 delete
    return 0;
}
输出:
MyClass Constructor
Hello from MyClass
MyClass Destructor

在这个例子中:

  • std::make_unique<MyClass>() 动态分配了一个 MyClass 对象,并返回了一个 std::unique_ptr,该指针自动管理这个对象的生命周期。
  • ptr 超出作用域时,它会自动调用 MyClass 的析构函数,并释放内存。

std::unique_ptr 的操作

1. 创建 unique_ptr
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
  • std::make_unique 是推荐的创建 unique_ptr 的方式,它提供了更安全和高效的内存分配。
2. 访问对象
ptr->display();  // 通过智能指针访问对象的成员
3. 转移所有权
std::unique_ptr<MyClass> new_ptr = std::move(ptr);  // 将所有权从 ptr 转移到 new_ptr
  • std::move 用于将 ptr 的所有权转移给 new_ptr,此时 ptr 变为空(nullptr)。
4. 自定义删除器

如果需要自定义资源释放逻辑,可以为 unique_ptr 指定一个自定义删除器:

std::unique_ptr<MyClass, void(*)(MyClass*)> ptr(new MyClass, [](MyClass* p) {
    std::cout << "Custom Deleter" << std::endl;
    delete p;
});
  • 在这个例子中,当 ptr 被销毁时,调用自定义的 lambda 函数删除器。

std::unique_ptr 的优点

  1. 内存安全std::unique_ptr 自动管理内存,避免了手动 newdelete 带来的内存泄漏问题。
  2. 资源独占:资源只能由一个 unique_ptr 拥有,防止了多次释放相同资源的问题。
  3. 移动语义std::unique_ptr 支持移动语义,允许所有权的安全转移。
  4. 性能优良std::unique_ptr 是轻量级的智能指针,没有额外的性能开销。

std::unique_ptrstd::shared_ptr 的区别

  • std::unique_ptr:独占所有权,不能复制,只能通过移动转移所有权。
  • std::shared_ptr:共享所有权,允许多个指针共享同一块资源,当最后一个 shared_ptr 被销毁时,资源才会被释放。

总结

std::unique_ptr 是一个强大而轻量级的智能指针,它通过独占所有权自动管理内存,确保资源能够在生命周期结束时被正确释放。它可以有效防止内存泄漏和重复释放问题,在现代 C++ 中,std::unique_ptr 是替代裸指针(raw pointers)管理动态内存的首选。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2118972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【北京迅为】《STM32MP157开发板使用手册》- 第十二章 编译Linux内核

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

运算放大器中的反馈

运算放大器中的反馈&#xff1a;原理、类型与应用 运算放大器&#xff08;Operational Amplifier, 简称Op-Amp&#xff09;是现代电子电路中的重要组成部分&#xff0c;被广泛应用于信号处理、放大、滤波等场合。而反馈技术则是运算放大器电路的核心之一&#xff0c;直接影响其…

代码随想录算法训练营第二十二天| 491. 递增子序列、46. 全排列、47. 全排列Ⅱ

今日内容 Leetcode. 491 递增子序列Leetcode. 46 全排列Leetcode. 47 全排列Ⅱ Leetcode. 491 递增子序列 文章链接&#xff1a;代码随想录 (programmercarl.com) 题目链接&#xff1a;491. 非递减子序列 - 力扣&#xff08;LeetCode&#xff09; 本题也是一个子集问题&#…

【AI绘画】Midjourney后置指令--seed、--tile、--q、--chaos、--w、--no详解

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI绘画 | Midjourney 文章目录 &#x1f4af;前言&#x1f4af;Midjourney后置指令--seed测试1测试2如何获取未指定种子图片的随机种子注意点 &#x1f4af;Midjourney后置指令--tile测试 &#x1f4af;Midjourney后置指令--q(or-…

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI&#xff08;简称 RVC&#xff09;模型是一个基于 VITS&#xff08;Variational Inference with adversarial learning for end-to-end Text-to-Speech&#xff09;的简单易用的语音转换框架。 具有以下特点 简单易用&a…

chrome浏览器如何设置自动播放音视频

使用场景&#xff1a; 有些场景需要打开页面后&#xff0c;自动播放视频或者视频流&#xff0c;这时候发现无法播放&#xff0c;打开浏览器控制台发现有下面的错误提示&#xff1a;NotAllowedError: play() failed because the user didnt interact with the document first 。…

顶级出图效果!免费在线使用FLux.1 模型,5s出图无限制!

最近发现一个可以在线免费使用 FLux.1 模型 生成图片的AI工具。 先看效果图&#xff1a; 工具不需要登录即可使用&#xff0c;目前还是完全免费的&#xff0c;国内可以直接使用。 在提示词输入框直接输入提示词即可&#xff0c;选择图片比例之后&#xff0c;直接生图。 出图的…

安全运营之浅谈SIEM告警疲劳

闲谈&#xff1a; 刚开始学习SIEM、态势感知这类产品的时&#xff0c;翻阅老外们的文章总是谈什么真阳性&#xff0c;假阳性告警、告警疲劳&#xff0c;当时在国内资料中没找到很合理的解释&#xff0c;慢慢就淡忘这件事了。随着慢慢深入工作&#xff0c;感觉大概理解了这些概念…

‌技术人必看!如何科学规划,从需求出发打造完美技术方案

引言 在互联网架构师的角色中&#xff0c;我们面临的挑战不仅仅是编写代码&#xff0c;更重要的是深入理解需求、设计系统&#xff0c;并确保我们的解决方案能够稳定、高效地运行。本文将详细介绍从新需求提出到技术方案发布的全过程。 1. 理解现有需求和场景 在开始一个新的…

信息学奥赛初赛天天练-87-NOIP2014普及组-完善程序-矩阵、子矩阵、最大子矩阵和、前缀和、打擂台求最大值

1 完善程序 最大子矩阵和 给出 m行 n列的整数矩阵&#xff0c;求最大的子矩阵和(子矩阵不能为空)。 输入第一行包含两个整数 m和 n&#xff0c;即矩阵的行数和列数。之后 m行&#xff0c;每行 n个整数&#xff0c;描述整个矩阵。程序最终输出最大的子矩阵和。 &#xff08;最…

SAP中mmpv自动过账—附带源码

想省事儿的直接拖到后面查看代码 思路分析 实现逻辑:初版 前台测试:选择屏幕确认公司代码。必要情况手动开账勾选前台执行按钮 1.1去marv表找公司代码的当前账期,简单运算获取下一个账期。1.2执行bdc,模拟前台手动开账期1.3执行的必要信息存日志表。例:修改人(开账期的人…

FastAPI 进阶:使用 BackgroundTasks 处理长时间运行的任务

在 FastAPI 中&#xff0c;BackgroundTasks 是一个功能&#xff0c;它允许你在发送响应给客户端之后执行后台任务。这些任务对于不需要客户端等待的操作非常有用&#xff0c;比如发送电子邮件通知或处理数据。然而&#xff0c;当服务器重启时&#xff0c;由于 BackgroundTasks …

C++: set与map容器的介绍与使用

本文索引 前言1. 二叉搜索树1.1 概念1.2 二叉搜索树操作1.2.1 查找与插入1.2.2 删除1.2.3 二叉搜索树实现代码 2. 树形结构的关联式容器2.1 set的介绍与使用2.1.1 set的构造函数2.1.2 set的迭代器2.1.3 set的容量2.1.4 set的修改操作 2.2 map的介绍与使用2.2.1 map的构造函数2.…

基于python的mediapipe姿态识别 动作识别 人体关健点 实现跳绳状态判别 计数功能

基于Python的MediaPipe姿态识别实现跳绳状态判别与计数功能 项目概述 本项目旨在利用Google的MediaPipe库&#xff0c;结合姿态识别技术&#xff0c;实现对跳绳动作的实时检测与计数功能。通过识别人体关键点&#xff0c;系统能够准确判断跳绳动作的状态&#xff0c;并实时统…

Java入门:07.Java中的面向对象03

11 this关键字 this关键字有两个作用 第一个作用&#xff0c;用来调用重载的构造方法 public class Test3{public static void main(String[] args){new User();new User("ls");new User("ls","女");} } ​ class User{String name ;String sex…

Autosar工程师必读:ETAS工具链自动化实战指南<三>

----自动化不仅是一种技术&#xff0c;更是一种思维方式&#xff0c;它将帮助我们在快节奏的工作环境中保持领先&#xff01; 目录 往期推荐 自动化命令--generate 命令语法 参数说明 命令使用前提 场景1&#xff1a;BSW代码生成 场景2&#xff1a;RTE代码生成 场景3&a…

对非洲33国免关税!非洲市场不容错过

2024年9月5日中非合作论坛峰会在北京隆重召开&#xff0c;会议后宣布对非洲33个国家实行0关税的优惠政策&#xff0c;并且在未来三年&#xff0c;推动中国企业对非投资不少于700亿元人民币。 自然而然的&#xff0c;中非友好关系必然会带动中国对非洲市场的出口&#xff0c;近…

云计算实训44——K8S及pod相关介绍

一、K8S基本概念 1、k8s是什么 K8S是Kubernetes的 缩写&#xff0c;由于k 和 s 之间有⼋个字符&#xff0c;所以因此得名。 Kubernetes 是⼀个可移植的、可扩展的开源平台&#xff0c;⽤于管理容器化 的⼯作负载和服务&#xff0c;可促进声 明式配置和⾃动化。 2、k8s的功能…

STM32F1+HAL库+FreeTOTS学习10——任务相关API函数使用

STM32F1HAL库FreeTOTS学习10——任务相关API函数使用 任务相关API函数1. uxTaskPriorityGet()2. vTaskPrioritySet()3. uxTaskGetNumberOfTasks()4. uxTaskGetSystemState()5. vTaskGetInfo()6. xTaskGetCurrentHandle()7. xTaskGetHandle()8. xTaskGetIdleTaskHandle()9. uxTa…

你需要掌握的算法:快慢指针

文章目录 前言龟兔赛跑乌龟能否追上兔子乌龟与兔子在何处相遇龟兔问题的推论 快慢指针基础概念发展历史 快慢指针的应用检测链表是否有环找到链表的中间节点计算链表的环长度找到链表环的入口节点 小结 前言 在处理链表数据结构时&#xff0c;快慢指针是一种非常高效的算法技巧…