内存泄露详解
内存泄露的含义
内存泄漏(Memory Leak)是指程序在运行过程中,动态分配的内存(比如用 new/malloc 分配的内存)不再被使用,但没有被释放,导致这部分内存一直被占用,直到程序结束才会被操作系统回收。
在短时间运行的小程序中,内存泄露可能看不出影响,但长期运行的程序(比如服务器、后台服务)会持续占用更多内存,最终导致程序卡顿、崩溃,甚至耗尽系统内存。
C++ 内存泄露指动态分配内存未释放。常见原因包括只分配不释放、指针覆盖、类中未写析构函数及异常导致释放代码跳过。预防方法包括配对使用 new/delete、手动定义析构函数及使用 RAII 思想。RAII(资源获取即初始化)通过对象生命周期绑定资源管理,利用构造函数获取资源、析构函数释放资源,实现自动管理,避免泄漏。C++11 智能指针是 RAII 的典型应用,此外还可用于文件句柄、锁等资源管理。
内存泄漏(Memory Leak)是指程序在运行过程中,动态分配的内存(比如用 new/malloc 分配的内存)不再被使用,但没有被释放,导致这部分内存一直被占用,直到程序结束才会被操作系统回收。
在短时间运行的小程序中,内存泄露可能看不出影响,但长期运行的程序(比如服务器、后台服务)会持续占用更多内存,最终导致程序卡顿、崩溃,甚至耗尽系统内存。
内存泄漏的本质是:动态分配的内存的'所有权'丢失——程序再也找不到这块内存的指针,无法调用 delete/free 释放它。常见场景有:
这是新手最容易犯的错误,用 new 分配内存后,没有对应的 delete。
#include <iostream>
using namespace std;
void func() {
// 动态分配 int 类型内存,指针 p 是局部变量
int* p = new int(10);
// 用完后没有 delete,函数结束后 p 被销毁,再也找不到这块内存
}
int main() {
// 多次调用 func,会泄漏多块内存
for (int i = 0; i < 1000; i++) {
func();
}
return 0;
}
每次调用 func,都会分配 4 字节(int 大小)内存,但没有释放。循环 1000 次后,就泄漏了 4000 字节内存,且程序运行期间无法回收。
指针指向动态内存后,被重新赋值,原内存地址丢失,无法释放。
void func() {
int* p = new int(20); // 分配内存 A
p = new int(30); // 分配内存 B,覆盖 p 的地址
// 此时内存 A 的地址丢失,再也无法 delete,导致泄漏
delete p; // 只释放了内存 B,内存 A 永远泄漏
}
如果类的成员变量是动态分配的,但没有定义析构函数释放,对象销毁时就会泄漏。
class BadString {
public:
BadString(const char* str) {
m_str = new char[strlen(str) + 1]; // 分配内存
strcpy(m_str, str);
}
// 没有定义析构函数!编译器生成的默认析构不会 delete m_str
private:
char* m_str;
};
int main() {
BadString s("Leak Memory");
return 0; // 对象 s 销毁,m_str 指向的内存泄漏
}
如果 new 后,释放代码(delete)前抛出异常,且没有捕获,会跳过 delete 导致泄漏。
void riskyFunc() {
int* p = new int(40);
// 假设这里抛出异常,后续的 delete 不会执行
throw runtime_error("Something wrong");
delete p; // 永远执行不到,内存泄漏
}
new ↔ delete、new[] ↔ delete[]、malloc ↔ free 在编程时配对使用,有始有终;new/文件句柄等),必须手动定义析构函数释放;使用智能指针:C++11 及以上推荐用 std::unique_ptr/std::shared_ptr,它们会自动释放内存,无需手动 delete;
#include <memory>
void safeFunc() {
// unique_ptr 自动管理内存,函数结束时自动释放
unique_ptr<int> p(new int(50));
}
Linux/macOS:使用 valgrind 工具,终端命令:
valgrind --leak-check=full ./你的程序名
它会详细列出泄漏的内存地址、大小、所在代码行;
参考微软官方文档:析构函数 (C++)
析构函数是 C++ 类中一种特殊的成员函数,专门用于清理对象生命周期结束时的资源(比如动态分配的内存、打开的文件句柄、网络连接等),它的作用和构造函数正好相反:构造函数的作用是在对象创建时初始化、分配资源;而析构函数的作用是在对象销毁时释放资源、做收尾工作。
在对象超出范围或通过调用 delete 或 delete[] 显式销毁对象时,会自动调用析构函数。析构函数与类同名,前面带有波形符 ( ~ )。例如,声明 String 类的析构函数:~String()。
如果你未定义析构函数,编译器会提供一个默认的析构函数;对于某些类来说,这就足够了。但是,默认析构函数只会释放对象本身占用的内存,不会清理动态分配的资源。当类维护必须显式释放的资源(例如系统资源的句柄,或指向在类的实例被销毁时应释放的内存的指针)时,你需要定义一个自定义的析构函数。
比方说,如果类中使用了 new 分配内存,默认析构函数不会释放这部分内存,会导致内存泄漏,此时必须手动定义析构函数释放资源。
如果类中没有手动定义析构函数,编译器会自动生成一个默认析构函数,但默认析构函数只会释放对象本身占用的内存,不会清理动态分配的资源。
析构函数是由编译器自动调用的。下面的代码中手动实现了析构函数,运行一下,可以看到析构函数在实例被销毁时自动被调用。
#include <iostream>
using namespace std;
class Person {
public:
// 构造函数
Person(string name) : m_name(name) {
cout << "Person " << m_name << " 被创建" << endl;
}
// 析构函数(手动定义)
~Person() {
cout << "Person " << m_name << " 被销毁" << endl;
}
private:
string m_name;
};
int main() {
// 栈上创建对象,函数结束时自动销毁
Person p1("张三");
{
// 局部作用域,离开作用域时销毁
Person p2("李四");
}
// 此处 p2 的析构函数被调用
return 0;
}
// 此处 p1 的析构函数被调用
输出结果:
Person 张三 被创建 Person 李四 被创建 Person 李四 被销毁 Person 张三 被销毁
如果类中使用了 new 分配内存,默认析构函数不会释放这部分内存,会导致内存泄漏,此时必须手动定义析构函数释放资源。
下面的 MyString 类中,在构造时使用了 new 动态分配内存,此时就必须手动实现析构函数,在析构函数中,释放动态分配的内存,以避免内存泄露。
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
public:
// 构造函数:动态分配内存
MyString(const char* str) {
if (str == nullptr) {
m_str = new char[1];
*m_str = '\0';
} else {
int len = strlen(str);
m_str = new char[len + 1]; // 分配内存
strcpy(m_str, str); // 拷贝字符串
}
cout << "MyString 构造:" << m_str << endl;
}
// 析构函数:释放动态分配的内存
~MyString() {
delete[] m_str; // 释放数组内存
cout << "MyString 析构:内存已释放" << endl;
}
// 打印字符串
void print() {
cout << "字符串:" << m_str << endl;
}
private:
char* m_str; // 动态分配的字符数组
};
int main() {
MyString s("Hello C++");
s.print();
return 0; // 程序结束时,s 销毁,析构函数自动调用
}
输出结果:
MyString 构造:Hello C++ 字符串:Hello C++ MyString 析构:内存已释放
RAII 是 Resource Acquisition Is Initialization 的缩写,中文译作'资源获取即初始化',是 C++ 特有的一种编程思想与设计范式,其原理是让资源的生命周期和对象的生命周期绑定,也就是通过 class 实例的创建与销毁,实现资源的自动管理,从根本上避免内存泄漏、文件句柄未关闭等资源管理问题。
可以把 RAII 理解为:程序需要使用一个资源(比如内存、文件、锁),就委托一个 C++ 对象来管理它:
new 内存、fopen 打开文件)delete 内存、fclose 关闭文件)因为 C++ 对象的析构是编译器自动触发的(离开作用域必调用),所以资源一定会被释放,不会遗漏。
这是最基础的 RAII 应用,也是 C++ 智能指针(unique_ptr/shared_ptr)的底层原理:
#include <iostream>
using namespace std;
// 自定义 RAII 类管理 int 类型的动态内存
class RAIIInt {
public:
// 构造函数:获取资源(分配内存)
RAIIInt(int value) : m_ptr(new int(value)) {
cout << "资源已获取:分配内存,值为" << value << endl;
}
// 析构函数:释放资源(自动调用)
~RAIIInt() {
delete m_ptr; // 无论如何,析构时必释放
cout << "资源已释放:内存被 delete" << endl;
}
// 提供资源访问接口
int& get() { return *m_ptr; }
void set(int value) { *m_ptr = value; }
private:
int* m_ptr; // 封装需要管理的资源(动态内存)
// 禁用拷贝(避免浅拷贝导致重复释放,新手暂时记住即可)
RAIIInt(const RAIIInt&) = delete;
RAIIInt& operator=(const RAIIInt&) = delete;
};
// 测试:资源自动释放
void testRAII() {
RAIIInt raii_obj(100); // 构造:获取内存
raii_obj.set(200); // 访问资源
cout << "当前值:" << raii_obj.get() << endl;
// 函数结束,raii_obj 离开作用域,析构函数自动调用,内存释放
}
int main() {
testRAII();
cout << "函数执行完毕,资源已安全释放" << endl;
return 0;
}
输出结果:
资源已获取:分配内存,值为 100 当前值:200 资源已释放:内存被 delete 函数执行完毕,资源已安全释放
除了内存,RAII 还能管理文件、锁、网络连接等所有需要'获取 - 释放'的资源:
#include <iostream>
#include <cstdio>
using namespace std;
// RAII 类管理文件句柄
class RAIIFile {
public:
// 构造:打开文件(获取资源)
RAIIFile(const char* filename, const char* 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;
}
}
// 提供文件操作接口
void write(const char* content) { fputs(content, m_file); }
private:
FILE* m_file; // 封装文件句柄
// 禁用拷贝
RAIIFile(const RAIIFile&) = delete;
RAIIFile& operator=(const RAIIFile&) = delete;
};
void testFileRAII() {
RAIIFile file("test.txt", "w"); // 构造:打开文件
file.write("Hello RAII!"); // 写入内容
// 函数结束,file 析构,文件自动关闭(即使中途抛异常也会关闭)
}
int main() {
testFileRAII();
return 0;
}
C++11 提供的 std::unique_ptr/std::shared_ptr 是 RAII 思想的现成实现,无需自己写 RAII 类:
#include <iostream>
#include <memory>
// 智能指针头文件
using namespace std;
void testSmartPtr() {
// unique_ptr 是 RAII 类,构造时获取内存,析构时自动释放
unique_ptr<int> ptr(new int(300));
cout << "智能指针管理的值:" << *ptr << endl;
// 函数结束,ptr 析构,内存自动 delete,无泄漏
}
int main() {
testSmartPtr();
return 0;
}
delete 可能因异常跳过)。new/delete);std::lock_guard,构造加锁,析构解锁);
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online