运行在VS2022,x86,Debug下。
目录
1)VS检测内存泄漏,官方文档使用 CRT 库查找内存泄漏 | Microsoft Learn
代码1如下,在类中定义了数组指针,并且定义了两个接口 setName()设置姓名,动态分配内存并赋值;getName()获取姓名。
【内存泄漏1】当我们在setName()中,为类成员变量动态分配内存时,如果没有检查是否已有内存并释放旧内存,造成内存泄漏,分析如下。
【内存泄漏2】另外,如果在析构函数中,没有检查是否为类成员变量分配内存,并释放内存,造成内存泄漏。(即把代码1的析构函数注释掉的话)
【内存泄漏3】 对象数组指针,使用delete时,没有使用方括号,造成内存泄漏,分析如下。
【内存泄漏4】 对象指针数组,使用delete[],只释放了所有指针的内存空间,而没有释放对象的内存空间,造成内存泄漏,分析如下。
代码2如下,在代码1基础上,定义一个Student类,继承Person类。
【内存泄漏6】当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么只会调用基类的析构函数,由于子类析构函数不会被调用,子类的资源没有正确释放,造成内存泄漏。
【内存泄漏7】循环引用,导致引用计数混乱,堆内存无法正确释放,造成内存泄漏。
代码4如下,在类中定义了整型指针,并且在类的构造函数中,为它动态分配内存。
【内存泄漏8】在类的构造函数和析构函数中没有匹配的调用new和delete函数,造成内存泄漏。(即把代码4的析构函数注释掉的话)
【内存泄漏10】当函数返回动态分配的内存时,返回类型是引用,由于不再需要时无法使用delete释放内存,因为不能删除引用指向的内存地址,造成内存泄漏。
1)VS检测内存泄漏,官方文档使用 CRT 库查找内存泄漏 | Microsoft Learn
示例如下。
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include<iostream>
using namespace std;
int main()
{
int* x = new int(7);
char* s = new char[100];
_CrtDumpMemoryLeaks(); //检查内存泄漏
return 0;
}
分析:在输出的调试窗口,看到两处内存泄漏,如下图。
【注意】使用CRT库是检查main()运行结束前,动态分配的内存是否已经释放。但如果是在main()里创建类对象,那么在程序运行结束时会自动调用类对象的析构函数。这时候还需要通过反汇编,进一步分析析构函数是否正确释放对象内存空间,如果已释放,那么就没有内存泄漏,代码如下。
define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include<iostream>
using namespace std;
class MyClass {
private:
int* value;
public:
MyClass(int v) : value(new int(v)) {
printf("%s\n", "Constructor class");
}
~MyClass()
{
delete value;
printf("%s\n", "Delete class");
}
};
int main()
{
MyClass obj(42);
_CrtDumpMemoryLeaks();
return 0;
}
分析1:在输出的调试窗口,看到类对象动态分配内存4B,如下图。
在return 0 处设置断点,进行调试。
分析2,如下图。
- 打开寄存器窗口,ecx保存对象obj地址,即0x00B6FB70。
- 打开内存窗口1,查看对象obj保存的数据内容,即指针value的值,为0x00FA6A48。
- 打开内存窗口2,查看指针value保存的数据内容,即int类型数据,十六进制2a即十进制42。
分析3,执行完类对象析构函数后,指针value的内容被释放,即没有出现内存泄漏,如下图。
2)内存泄漏的几种情况。
代码1如下,在类中定义了数组指针,并且定义了两个接口 setName()设置姓名,动态分配内存并赋值;getName()获取姓名。
class Person
{
private:
char* name;
public:
Person() :name(nullptr) {}
~Person()
{
if (name != nullptr)
{
cout << "delete person:" << name << endl;
delete[] name;
name = nullptr;
}
}
public:
void setName(const char* name) //设置姓名
{
int len = strlen(name);
this->name = new char[len + 1];
if (this->name != nullptr)
strcpy(this->name, name);
}
const char* getName() { return name; } //获取姓名
};
【内存泄漏1】当我们在setName()中,为类成员变量动态分配内存时,如果没有检查是否已有内存并释放旧内存,造成内存泄漏,分析如下。
在main(),创建类对象p1,p1调用两次setName(),代码如下。
int main()
{
Person p1;
p1.setName("Tony");
p1.setName("Marry");
_CrtDumpMemoryLeaks(); //检查内存泄漏
return 0;
}
分析1:借助CRT库看类对象内存分配情况,分别为“Tony","Marry"分配了5B、6B,如下图。
分析2:在控制台输出类析构信息,只释放了“Marry”的内存,如下图。
总结:当p1指向新的内存时,p1曾指向的内存不会被释放,造成内存泄漏。
解决:在setName()添加name的内存检验,若已有内存,释放旧内存,从而避免内存泄漏,如下图。
【内存泄漏2】另外,如果在析构函数中,没有检查是否为类成员变量分配内存,并释放内存,造成内存泄漏。(即把代码1的析构函数注释掉的话)
【内存泄漏3】 对象数组指针,使用delete时,没有使用方括号,造成内存泄漏,分析如下。
在main(),创建对象数组指针p,它的两个对象都调用了setName(),使用delete释放内存,代码如下。
int main()
{
Person* p = new Person[2];
p[0].setName("Tony");
p[1].setName("Marryasdfgfghjjkkkll");
delete p;
_CrtDumpMemoryLeaks();
return 0;
}
在delete p 处设置断点,进行调试。
分析1,如下图。
- eax保存了对象数组的首地址,即0x0165166C。
- 内存窗口1,查看对象数组保存的数据内容,即指针value的值。
- 第一个对象地址为0x0165166C,它的value值为0x01651010。
- 第二个对象地址为0x01651670,它的value值为0x0164B4C8。
- 内存窗口2,查看第一个对象value保存的数据内容,即“Tony”。
- 内存窗口3,查看第二个对象value保存的数据内容,即“Marryasdfgfghjjkkkll”。
分析2:只调用一次析构函数,即只释放第一个对象的内存空间,如下图。
分析3:紧接着释放指针的内存空间,会触发中断,如下图。
总结:对象数组指针,使用delete时,只会调用一次析构函数释放第一个对象的内存空间,其余对象的内存空间不会被释放,造成内存泄漏。而且在释放指针的内存空间时也会触发中断。
解决:对象数组指针,使用delete[] 释放,如下图。
分析:它会循环调用析构函数,释放完所有对象的内存空间后,再释放指针的内存空间,从而避免内存泄漏,如下图。
【内存泄漏4】 对象指针数组,使用delete[],只释放了所有指针的内存空间,而没有释放对象的内存空间,造成内存泄漏,分析如下。
在main(),创建对象指针数组p,它的两个对象指针都调用了setName(),使用delete[]释放内存,代码如下。
int main()
{
Person* p[2] = {new Person(), new Person()};
p[0]->setName("Tony");
p[1]->setName("Marryasdfgfghjjkkkll");
delete[] p;
_CrtDumpMemoryLeaks();
return 0;
}
在delete[] p 处设置断点,进行调试。
分析:只释放了所有指针的内存空间,如下图。
解决:遍历对象指针数组,使用delete释放每个对象指针,如下图。
分析:使用delete释放每个对象指针时,会先调用析构函数释放这个对象的内存空间,再释放指向这个对象的指针的内存空间,从而避免内存泄漏,如下图。
【内存泄漏5】当为类成员变量动态分配内存时,如果将一个对象赋值给另一个对象,由于没有重新定义赋值运算符,使用默认赋值运算符进行浅拷贝。即a和b指向同一内存,但b曾指向的内存不会被删除,造成内存泄漏;若一方离开了它的生存空间,使用析构函数释放资源,另一方会变成悬空指针,指向已释放的内存,导致未定义行为;同时当另一方调用析构函数时,会因重复释放同一堆空间而触发中断。分析如下。
在main(),创建对象p1和p2,p1和p2分别调用一次setName(),之后p1赋值给p2,代码如下。
int main()
{
Person p1;
p1.setName("Tony");
Person p2;
p2.setName("Marry");
p2 = p1;
_CrtDumpMemoryLeaks();
return 0;
}
分析1:在p1和p2分别调用完setName()时,p1的name指向“Tony”的内存空间,p2的name指向“Marry”的内存空间。
分析2:当p1赋值给p2后,调用默认赋值运算符进行浅拷贝,p1和p2的name都指向“Tony”的内存空间,但p2的name曾指向“Marry”的内存空间不会被释放,造成内存泄漏。
分析3:程序执行结束时,先调用p2的析构函数,其析构函数会释放p1还指向的内存,此时p1还存在,它的name指针会变成悬空指针,指向已经被释放的内存,导致未定义行为。如下图。
分析4:紧接着调用p1的析构函数,会重复释放同一堆空间而触发中断,如下图。
解决:重新定义赋值运算符,进行深拷贝,添加代码如下。
Person& operator=(const Person& other)
{
int len = strlen(other.name);
if (this->name != nullptr) //同样要检验name的内存
delete[] this->name;
this->name = new char[len + 1];
if (this->name != nullptr)
strcpy(this->name, other.name);
return *this;
}
【没有内存泄漏,但会重复释放同一堆空间而触发中断】当为类成员变量动态分配内存时,如果将一个对象去初始化另一个对象(即拷贝),由于没有重新定义拷贝构造函数,使用默认拷贝构造函数进行浅拷贝。此时,由于另一对象初始时没有旧内存,不会造成内存泄漏;但若其中一方离开了它的生存空间,使用析构函数释放资源,另一方会变成悬空指针;当另一方调用析构函数时,会因重复释放同一堆空间而触发中断。分析如下。
在main(),创建对象p1,p1调用一次setName(),之后p1拷贝给p2,代码如下。
int main()
{
Person p1;
p1.setName("Tony");
Person p2(p1);
_CrtDumpMemoryLeaks();
return 0;
}
分析1:p1拷贝给p2后,调用默认拷贝构造函数进行浅拷贝,p1和p2指向同一内存,如下图。
分析2:程序执行结束时,先调用p2的析构函数,再调用p1的析构函数,此时会重复释放同一堆空间而触发中断,如下图。
解决:重新定义拷贝构造函数,进行深拷贝,添加代码如下。
Person(const Person& other)
{
int len = strlen(other.name);
this->name = new char[len + 1];
if (this->name != nullptr)
strcpy(this->name, other.name);
}
代码2如下,在代码1基础上,定义一个Student类,继承Person类。
class Student :public Person
{
private:
int* age;
public:
Student(const int& myAge, const char* myName) { age = new int(myAge); Person::setName(myName); }
~Student() { cout << "delete studen" << endl; delete age;}
};
【内存泄漏6】当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么只会调用基类的析构函数,由于子类析构函数不会被调用,子类的资源没有正确释放,造成内存泄漏。
在main(),定义基类指针p1指向子类对象,代码如下。
int main()
{
Person* p1 = new Student(18, "Tony");
delete p1;
_CrtDumpMemoryLeaks();
return 0;
}
在delete p1处设置断点,进行调试。
分析:只会调用基类析构函数,释放基类的内存空间,然后释放指针的内存空间,如下图。
解决:将Person类的析构函数定义为虚函数,如下图。
分析:调用子类析构函数,释放子类的内存空间。然后调用基类析构函数,释放基类的内存空间。最后释放指针的内存空间,从而避免内存泄漏,如下图。
代码3如下,使用shared_ptr。
class A;
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
};
class B {
public:
std::shared_ptr<A> a_ptr;
};
int main()
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 互相引用
a->b_ptr = b;
b->a_ptr = a;
_CrtDumpMemoryLeaks();
return 0;
}
【内存泄漏7】循环引用,导致引用计数混乱,堆内存无法正确释放,造成内存泄漏。
解决:使用weak_ptr打破循环引用,因为weak_ptr不会增加引用计数,如下图。
代码4如下,在类中定义了整型指针,并且在类的构造函数中,为它动态分配内存。
class MyClass {
private:
int* value;
public:
MyClass(int v) : value(new int(v)) {
printf("%s\n", "Constructor class");
}
~MyClass()
{
delete value;
printf("%s\n", "Delete class");
}
};
【内存泄漏8】在类的构造函数和析构函数中没有匹配的调用new和delete函数,造成内存泄漏。(即把代码4的析构函数注释掉的话)
【内存泄漏9】不使用new,而是使用malloc()为对象指针动态分配内存,然后通过对象指针显式调用构造函数进行初始化,当不再需要时使用free()释放内存,由于free()不会调用析构函数,对象的内存空间无法释放,造成内存泄漏,分析如下。
在main(),使用malloc()为对象指针obj分配内存,再显式调用构造函数进行初始化,最后使用free()释放内存,代码如下。
int main()
{
MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
new(obj) MyClass(44);
free(obj);
_CrtDumpMemoryLeaks();
return 0;
}
分析:只释放了指针的内存空间,如下图。
解决:使用new为对象指针动态分配内存,如下图。
分析:当使用delete释放内存时,会先调用析构函数,释放对象内存空间,然后再释放指针内存空间,从而避免内存泄漏,如下图。
代码5如下,函数返回动态分配的内存。
int& allocateInteger()
{
int* p = new int(42);
return *p;
}
【内存泄漏10】当函数返回动态分配的内存时,返回类型是引用,由于不再需要时无法使用delete释放内存,因为不能删除引用指向的内存地址,造成内存泄漏。
在main(),获取返回值的引用,然后使用delete&ref 释放内存,代码如下。
int main()
{
int ref = allocateInteger();
delete& ref;
_CrtDumpMemoryLeaks();
return 0;
}
分析:释放指针内存空间时,触发中断。
解决:使用指针返回动态分配的内存,可以在不需要时使用delete释放内存,从而避免内存泄漏,如下图。