C++反汇编——内存泄漏的十种情况

运行在VS2022,x86,Debug下。

目录

1)VS检测内存泄漏,官方文档使用 CRT 库查找内存泄漏 | Microsoft Learn

示例如下。

【注意】使用CRT库是检查main()运行结束前,动态分配的内存是否已经释放。但如果是在main()里创建类对象,那么在程序运行结束时会自动调用类对象的析构函数。这时候还需要通过反汇编,进一步分析析构函数是否正确释放对象内存空间,如果已释放,那么就没有内存泄漏,代码如下。

​编辑

2)内存泄漏的几种情况。

代码1如下,在类中定义了数组指针,并且定义了两个接口 setName()设置姓名,动态分配内存并赋值;getName()获取姓名。

【内存泄漏1】当我们在setName()中,为类成员变量动态分配内存时,如果没有检查是否已有内存并释放旧内存,造成内存泄漏,分析如下。

【内存泄漏2】另外,如果在析构函数中,没有检查是否为类成员变量分配内存,并释放内存,造成内存泄漏。(即把代码1的析构函数注释掉的话)

【内存泄漏3】 对象数组指针,使用delete时,没有使用方括号,造成内存泄漏,分析如下。

【内存泄漏4】 对象指针数组,使用delete[],只释放了所有指针的内存空间,而没有释放对象的内存空间,造成内存泄漏,分析如下。

【内存泄漏5】当为类成员变量动态分配内存时,如果将一个对象赋值给另一个对象,由于没有重新定义赋值运算符,使用默认赋值运算符进行浅拷贝。即a和b指向同一内存,但b曾指向的内存不会被删除,造成内存泄漏;若一方离开了它的作用域,使用析构函数释放资源,另一方会变成悬空指针,指向已释放的内存,导致未定义行为;同时当另一方调用析构函数时,会因重复释放同一堆空间而触发中断。分析如下。

【没有内存泄漏,但会重复释放同一堆空间而触发中断】当为类成员变量动态分配内存时,如果将一个对象去初始化另一个对象(即拷贝),由于没有重新定义拷贝构造函数,使用默认拷贝构造函数进行浅拷贝。此时,由于另一对象初始时没有旧内存,不会造成内存泄漏;但若其中一方离开了它的作用域,使用析构函数释放资源,另一方会变成悬空指针;当另一方调用析构函数时,会因重复释放同一堆空间而触发中断。分析如下。

代码2如下,在代码1基础上,定义一个Student类,继承Person类。

【内存泄漏6】当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么只会调用基类的析构函数,由于子类析构函数不会被调用,子类的资源没有正确释放,造成内存泄漏。

代码3如下,使用shared_ptr。

【内存泄漏7】循环引用,导致引用计数混乱,堆内存无法正确释放,造成内存泄漏。

代码4如下,在类中定义了整型指针,并且在类的构造函数中,为它动态分配内存。

【内存泄漏8】在类的构造函数和析构函数中没有匹配的调用new和delete函数,造成内存泄漏。(即把代码4的析构函数注释掉的话)

【内存泄漏9】不使用new,而是使用malloc()为对象指针动态分配内存,然后通过对象指针显式调用构造函数进行初始化,当不再需要时使用free()释放内存,由于free()不会调用析构函数,对象的内存空间无法释放,造成内存泄漏,分析如下。

代码5如下,函数返回动态分配的内存。

【内存泄漏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释放内存,从而避免内存泄漏,如下图。

  • 31
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值