C++内存泄露、析构函数与RAII编程思想详解

内存泄露详解

内存泄露的含义

内存泄漏(Memory Leak)是指程序在运行过程中,动态分配的内存(比如用 new/malloc 分配的内存)不再被使用,但没有被释放,导致这部分内存一直被占用,直到程序结束才会被操作系统回收。

在短时间运行的小程序中,内存泄露可能看不出影响,但长期运行的程序(比如服务器、后台服务)会持续占用更多内存,最终导致程序卡顿、崩溃,甚至耗尽系统内存。

内存泄漏的常见原因

内存泄漏的本质是:动态分配的内存的“所有权”丢失——程序再也找不到这块内存的指针,无法调用 delete/free 释放它。常见场景有:

1. 只分配不释放

这是新手最容易犯的错误,用 new 分配内存后,没有对应的 delete

#include<iostream>usingnamespace std;voidfunc(){// 动态分配int类型内存,指针p是局部变量int* p =newint(10);// 用完后没有delete,函数结束后p被销毁,再也找不到这块内存}intmain(){// 多次调用func,会泄漏多块内存for(int i =0; i <1000; i++){func();}return0;}

每次调用 func,都会分配4字节(int大小)内存,但没有释放。循环1000次后,就泄漏了4000字节内存,且程序运行期间无法回收。

2. 指针被覆盖(所有权丢失)

指针指向动态内存后,被重新赋值,原内存地址丢失,无法释放。

voidfunc(){int* p =newint(20);// 分配内存A p =newint(30);// 分配内存B,覆盖p的地址// 此时内存A的地址丢失,再也无法delete,导致泄漏delete p;// 只释放了内存B,内存A永远泄漏}
3. 类中动态资源未写析构函数

如果类的成员变量是动态分配的,但没有定义析构函数释放,对象销毁时就会泄漏。

classBadString{public:BadString(constchar* str){ m_str =newchar[strlen(str)+1];// 分配内存strcpy(m_str, str);}// 没有定义析构函数!编译器生成的默认析构不会delete m_strprivate:char* m_str;};intmain(){ BadString s("Leak Memory");return0;// 对象s销毁,m_str指向的内存泄漏}
4. 异常导致释放代码未执行

如果 new 后,释放代码(delete)前抛出异常,且没有捕获,会跳过 delete 导致泄漏。

voidriskyFunc(){int* p =newint(40);// 假设这里抛出异常,后续的delete不会执行throwruntime_error("Something wrong");delete p;// 永远执行不到,内存泄漏}

如何预防和检测内存泄漏

1. 从代码层面预防
  • 配对使用newdeletenew[]delete[]mallocfree 在编程时配对使用,有始有终;
  • 类中必写析构:只要类中有动态资源(new/文件句柄等),必须手动定义析构函数释放;
  • 异常安全:用RAII(资源获取即初始化)思想管理资源。
  • AI辅助:claude code等AI辅助工具都可以有效检测代码中的内存泄露。

使用智能指针:C++11及以上推荐用 std::unique_ptr/std::shared_ptr,它们会自动释放内存,无需手动 delete

#include<memory>voidsafeFunc(){// unique_ptr自动管理内存,函数结束时自动释放 unique_ptr<int>p(newint(50));}
2. 检测方法
  • Windows:使用 Visual Studio 的 “内存诊断工具”(Memory Diagnostic),调试时可实时检测内存泄漏;

Linux/macOS:使用 valgrind 工具,终端命令:

valgrind --leak-check=full ./你的程序名 

它会详细列出泄漏的内存地址、大小、所在代码行;

析构函数详解

析构函数的概念

microsoft文档:析构函数 (C++)

析构函数是 C++ 类中一种特殊的成员函数,专门用于清理对象生命周期结束时的资源(比如动态分配的内存、打开的文件句柄、网络连接等),它的作用和构造函数正好相反:构造函数的作用是在对象创建时初始化、分配资源;而析构函数的作用是在对象销毁时释放资源、做收尾工作。

在对象超出范围或通过调用 delete 或 delete[] 显式销毁对象时,会自动调用析构函数。 析构函数与类同名,前面带有波形符 ( ~ )。 例如,声明 String 类的析构函数:~String()

如果你未定义析构函数,编译器会提供一个默认的析构函数;对于某些类来说,这就足够了。但是,默认析构函数只会释放对象本身占用的内存,不会清理动态分配的资源。当类维护必须显式释放的资源(例如系统资源的句柄,或指向在类的实例被销毁时应释放的内存的指针)时,你需要定义一个自定义的析构函数。

比方说,如果类中使用了 new 分配内存,默认析构函数不会释放这部分内存,会导致内存泄漏,此时必须手动定义析构函数释放资源。

析构函数的使用场景与示例

如果类中没有手动定义析构函数,编译器会自动生成一个默认析构函数,但默认析构函数只会释放对象本身占用的内存,不会清理动态分配的资源。

析构函数是由编译器自动调用的。下面的代码中手动实现了析构函数,运行一下,可以看到析构函数在实例被销毁时自动被调用。

#include<iostream>usingnamespace std;classPerson{public:// 构造函数Person(string name):m_name(name){ cout <<"Person "<< m_name <<" 被创建"<< endl;}// 析构函数(手动定义)~Person(){ cout <<"Person "<< m_name <<" 被销毁"<< endl;}private: string m_name;};intmain(){// 栈上创建对象,函数结束时自动销毁 Person p1("张三");{// 局部作用域,离开作用域时销毁 Person p2("李四");}// 此处p2的析构函数被调用return0;}// 此处p1的析构函数被调用

输出结果:

Person 张三 被创建 Person 李四 被创建 Person 李四 被销毁 Person 张三 被销毁 

如果类中使用了 new 分配内存,默认析构函数不会释放这部分内存,会导致内存泄漏,此时必须手动定义析构函数释放资源。

下面的MyString类中,在构造时使用了 new 动态分配内存,此时就必须手动实现析构函数,在析构函数中,释放动态分配的内存,以避免内存泄露。

#include<iostream>#include<cstring>usingnamespace std;classMyString{public:// 构造函数:动态分配内存MyString(constchar* str){if(str ==nullptr){ m_str =newchar[1];*m_str ='\0';}else{int len =strlen(str); m_str =newchar[len +1];// 分配内存strcpy(m_str, str);// 拷贝字符串} cout <<"MyString 构造:"<< m_str << endl;}// 析构函数:释放动态分配的内存~MyString(){delete[] m_str;// 释放数组内存 cout <<"MyString 析构:内存已释放"<< endl;}// 打印字符串voidprint(){ cout <<"字符串:"<< m_str << endl;}private:char* m_str;// 动态分配的字符数组};intmain(){ MyString s("Hello C++"); s.print();return0;// 程序结束时,s销毁,析构函数自动调用}

输出结果:

MyString 构造:Hello C++ 字符串:Hello C++ MyString 析构:内存已释放 

注意事项

  • 不要手动调用析构函数:编译器会自动调用,手动调用会导致同一对象的析构函数被执行多次,引发崩溃;
  • 继承中的析构:如果是多态场景(基类指针指向派生类对象),基类析构函数必须声明为 virtual(虚析构),否则派生类的析构函数不会被调用,导致资源泄漏;
  • 默认析构的局限性:仅清理对象本身,不清理动态资源(如 new、fopen 等),这类场景必须手动写析构。

RAII编程思想

RAII的含义

RAII 是 Resource Acquisition Is Initialization 的缩写,中文译作“资源获取即初始化”,是C++特有的一种编程思想与设计范式,其原理是让资源的生命周期和对象的生命周期绑定,也就是通过class实例的创建与销毁,实现资源的自动管理,从根本上避免内存泄漏、文件句柄未关闭等资源管理问题。

可以把 RAII 理解为:程序需要使用一个资源(比如内存、文件、锁),就委托一个C++对象来管理它:

  • 这个对象在构造时获取资源(比如 new 内存、fopen 打开文件)
  • 这个对象在析构时自动释放资源(比如 delete 内存、fclose 关闭文件)

因为C++对象的析构是编译器自动触发的(离开作用域必调用),所以资源一定会被释放,不会遗漏。

RAII的核心原则

  1. 封装资源:把需要管理的资源(如指针、文件句柄)封装到一个类中
  2. 获取资源:在类的构造函数中获取/分配资源
  3. 提供访问:类中提供成员函数,让外部能访问资源(比如解引用指针、读写文件)
  4. 释放资源:在类的析构函数中释放/清理资源。

三、RAII的实战示例

示例1:用RAII管理动态内存(替代手动new/delete)

这是最基础的RAII应用,也是C++智能指针(unique_ptr/shared_ptr)的底层原理:

#include<iostream>usingnamespace std;// 自定义RAII类管理int类型的动态内存classRAIIInt{public:// 构造函数:获取资源(分配内存)RAIIInt(int value):m_ptr(newint(value)){ cout <<"资源已获取:分配内存,值为"<< value << endl;}// 析构函数:释放资源(自动调用)~RAIIInt(){delete m_ptr;// 无论如何,析构时必释放 cout <<"资源已释放:内存被delete"<< endl;}// 提供资源访问接口int&get(){return*m_ptr;}voidset(int value){*m_ptr = value;}private:int* m_ptr;// 封装需要管理的资源(动态内存)// 禁用拷贝(避免浅拷贝导致重复释放,新手暂时记住即可)RAIIInt(const RAIIInt&)=delete; RAIIInt&operator=(const RAIIInt&)=delete;};// 测试:资源自动释放voidtestRAII(){ RAIIInt raii_obj(100);// 构造:获取内存 raii_obj.set(200);// 访问资源 cout <<"当前值:"<< raii_obj.get()<< endl;// 函数结束,raii_obj离开作用域,析构函数自动调用,内存释放}intmain(){testRAII(); cout <<"函数执行完毕,资源已安全释放"<< endl;return0;}

输出结果

资源已获取:分配内存,值为100 当前值:200 资源已释放:内存被delete 函数执行完毕,资源已安全释放 
示例2:用RAII管理文件句柄(避免文件未关闭)

除了内存,RAII还能管理文件、锁、网络连接等所有需要“获取-释放”的资源:

#include<iostream>#include<cstdio>usingnamespace std;// RAII类管理文件句柄classRAIIFile{public:// 构造:打开文件(获取资源)RAIIFile(constchar* filename,constchar* mode):m_file(fopen(filename, mode)){if(m_file ==nullptr){perror("文件打开失败");exit(1);} cout <<"文件已打开:"<< filename << endl;}// 析构:关闭文件(释放资源)~RAIIFile(){if(m_file !=nullptr){fclose(m_file); cout <<"文件已关闭"<< endl;}}// 提供文件操作接口voidwrite(constchar* content){fputs(content, m_file);}private: FILE* m_file;// 封装文件句柄// 禁用拷贝RAIIFile(const RAIIFile&)=delete; RAIIFile&operator=(const RAIIFile&)=delete;};voidtestFileRAII(){ RAIIFile file("test.txt","w");// 构造:打开文件 file.write("Hello RAII!");// 写入内容// 函数结束,file析构,文件自动关闭(即使中途抛异常也会关闭)}intmain(){testFileRAII();return0;}
示例3:C++标准库中的RAII智能指针

C++11提供的 std::unique_ptr/std::shared_ptr 是RAII思想的现成实现,无需自己写RAII类:

#include<iostream>#include<memory>// 智能指针头文件usingnamespace std;voidtestSmartPtr(){// unique_ptr是RAII类,构造时获取内存,析构时自动释放 unique_ptr<int>ptr(newint(300)); cout <<"智能指针管理的值:"<<*ptr << endl;// 函数结束,ptr析构,内存自动delete,无泄漏}intmain(){testSmartPtr();return0;}

RAII的优势

  1. 异常安全:即使代码中抛出异常,对象的析构函数仍会被调用,资源一定会释放(手动 delete 可能因异常跳过)。
  2. 无需手动管理:不用记“分配后要释放”,编译器自动保证资源释放,从根源避免泄漏。
  3. 通用性:可管理任何资源(内存、文件、锁、网络连接等),只要资源有“获取-释放”的成对操作。

RAII的常见使用场景

  • 动态内存管理(替代手动 new/delete);
  • 文件/套接字/管道等句柄管理;
  • 多线程中的锁管理(如 std::lock_guard,构造加锁,析构解锁);
  • 数据库连接、网络连接等需要手动关闭的资源。

Read more

使用 VS Code 连接 MySQL 数据库

使用 VS Code 连接 MySQL 数据库

文章目录 * 前言 * VS Code下载安装 * 如何在VS Code上连接MySQL数据库 * 1、打开扩展 * 2、安装MySQL插件 * 3、连接 * 导入和导出表结构和数据 前言 提示:这里可以添加本文要记录的大概内容: 听说VS Code不要钱,功能还和 Navicat 差不多,还能在上面打游戏 但是没安装插件是不行的 发现一个非常牛的博主 还有一个非常牛的大佬 提示:以下是本篇文章正文内容,下面案例可供参考 VS Code下载安装 VS Code下载安装 如何在VS Code上连接MySQL数据库 本篇分享是在已有VS Code这个软件的基础上,数据库举的例子是MySQL 1、打开扩展 2、安装MySQL插件 在搜索框搜索 MySQL和 MySQL Syntax,下载这三个插件 点击下面的插件,选择【install】安装

By
RustFS 保姆级上手指南:国产开源高性能对象存储

RustFS 保姆级上手指南:国产开源高性能对象存储

最近在给项目选型对象存储的时候,发现一个挺有意思的现象:一边是MinIO社区版功能逐渐“躺平”,另一边是大家对存储性能和安全性的要求越来越高。就在这时,一个叫 RustFS 的国产开源项目闯入了我的视野。 折腾了一阵子后,我感觉这玩意儿确实有点东西。它用Rust语言写,天生就带着高性能和内存安全的基因,性能号称比MinIO快一大截,而且用的是对商业友好的Apache 2.0协议。今天,我就手把手带大家从零开始,搭建一个属于自己的RustFS服务,体验一下国产存储的威力。 一、 RustFS是什么?为什么值得你关注? 简单说,RustFS是一个 分布式对象存储系统 。你可以把它理解成一个你自己搭建的、功能跟阿里云OSS、亚马逊S3几乎一样的“私有云盘”。 但它有几个非常突出的亮点,让我觉得必须试试: * 性能猛兽 :基于Rust语言开发,没有GC(垃圾回收)带来的性能抖动,官方数据显示在4K随机读场景下,性能比MinIO高出40%以上,内存占用还不到100MB,简直是“小钢炮”。 * 100%S3兼容 :这意味着你现有的所有使用S3 API的代码、工具(比如AWS

By