《C/C++ 面试 100 例》(二)内存泄漏

前言

  • 每次游戏上线前跑压力测试,总会发现一些内存泄漏,而且由于项目庞大,添加上检测工具以后,服务器运行就变得奇慢无比,非常耗时,所以有必要总结一下其中的一些原因和解决方案,方便日后做自动化。

一、准备工作

1、工具安装
  • 内存泄漏检测工具:Visual Leak Detector

  • 链接: https://pan.baidu.com/s/1fBPiQ-N5C0lLAl-y1MP43Q 提取码: a5bn

  • 附带调试代码:

  • 链接: https://pan.baidu.com/s/1a_zPcncK3gtrCIVB2Q3epw 提取码: ppk2

2、目录添加
  • a)vld 头文件目录添加到 vs(头文件目录在vld安装目录下)
右键工程Properties
C/C++
General
Additional_Include_Directories
  • b)vld 库文件目录添加到 vs(库文件目录在vld在安装目录下)
右键工程Properties
Linker
General
Additional_Library_Directories
3、信息配置
  • a)工程配置必须为 Debug 模式;
  • b)将 VLD 工具安装目录下的 vld.ini 文件拷贝到需要检测的程序的工程代码(或者可执行文件)目录下,然后对配置进行一定的修改,这样就能将最后的报告输出到文件中:
    ReportFile =.\memory_leak_report.txt 
    ReportTo = both
  • c)对需要检测的代码增加一行头文件包含,放在最前面即可;
    #include <vld.h>

二、基础测试

1、简单尝试
  • c++的内存泄漏最原生的原因就是分配的内存没有释放,即 new/malloc 出的内存没有与之配对的 delete/free,举个最简单的例子;
#include "vld.h"

int main() {
	int *p = new int;
	*p = 0x09ABCDEF;
	return 0;
}
  • 程序退出的时候会进行内存泄漏的检测,并且输出报告;
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x0000000013065440: 4 bytes ----------
  Leak Hash: 0xD58AB4EF, Count: 1, Total 4 bytes
  Call Stack (TID 17172):
    MSVCR120D.dll!operator new()
    e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (78): MemoryLeakTester.exe!main() + 0xA bytes
    f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c (466): MemoryLeakTester.exe!mainCRTStartup()
    KERNEL32.DLL!BaseThreadInitThunk() + 0x14 bytes
    ntdll.dll!RtlUserThreadStart() + 0x21 bytes
  Data:
    EF CD AB 09                                                  ........ ........
Visual Leak Detector detected 1 memory leak (56 bytes).
2、报告分析
  • 报告中包含了几个内容:
    1. 【Block X at …: Y bytes】第 X 个块(new)泄漏了 Y 个字节;
    1. 【Call Stack】泄漏内存的堆栈信息,双击对应的行可以跳到具体代码位置;
    1. 【Data】泄漏内存的数据信息;
    1. 【Visual Leak Detector detected X memory leak (Y bytes).】总共 X 处泄漏,共泄漏字节数 Y 字节;
3、某些疑惑
  • 观察代码只泄漏了 4 个字节(1个int),但是报告显示泄漏了 56 个字节,多了 52 个字节,所以再做一些尝试,尝试如下;
#include "vld.h"

int main() {
	char *p1 = new char;
	*p1 = 0xAB;

	short *p2 = new short;
	*p2 = 0xABCD;

	int *p3 = new int;
	*p3 = 0x09ABCDEF;

	long long *p4 = new long long;

	return 0;
}
  • 得到的报告如下:
WARNING: Visual Leak Detector detected memory leaks!

---------- Block 1 at 0x00000000E1167080: 1 bytes ----------
  Leak Hash: 0xE21F7247, Count: 1, Total 1 bytes
  Call Stack (TID 16576):
    ...
  Data:
    AB
    
---------- Block 2 at 0x00000000E1165BF0: 2 bytes ----------
  Leak Hash: 0xE58559DC, Count: 1, Total 2 bytes
  Call Stack (TID 16576):
    ...
  Data:
    CD AB

---------- Block 3 at 0x00000000E1165C60: 4 bytes ----------
  Leak Hash: 0xB0A3AA1A, Count: 1, Total 4 bytes
  Call Stack (TID 16576):
    ...
  Data:
    EF CD AB 09

---------- Block 4 at 0x00000000E1165440: 8 bytes ----------
  Leak Hash: 0x49D5C84C, Count: 1, Total 8 bytes
  Call Stack (TID 16576):
    ...
  Data:
    CD CD CD CD    CD CD CD CD

Visual Leak Detector detected 4 memory leaks (223 bytes).
  • i. char、short、int、long long 分别泄漏 1、2、4、8 个字节,但是最后的泄漏的字节数不是 15 而是 223 ???
  • ii. 观察发现:
    (223 - 15) / 4 = 52
  • iii. 所以大胆猜测,每个 new 会导致对应的内存块多泄漏 52 个字节,这个可能是 VLD 工具泄漏的,猜测是它要记录内存泄漏的信息,所以这些信息不能在程序退出的时候释放(有兴趣可以看下 VLD 源码),但是这个不影响我们查内存泄漏的问题,只要没有泄漏,这 52 个字节也不会泄漏;
4、解决泄漏
  • 解决内存泄漏,就是让所有的 new 匹配 delete;
#include "vld.h"

int main() {
	int *p = new int;
	delete p;
	return 0;
}
    No memory leaks detected.
    Visual Leak Detector is now exiting.

三、常见内存泄漏

1、虚析构
  • 面试 c++ 的时候总会听到一些熟悉的问题:
    i. 为什么基类对象的析构函数一般都要声明 virtual 关键字?
    ii. 虚析构的作用和原理是什么?
  • 来看一段代码:
#include "vld.h"

using namespace std;

// 基类
class LeakBaseObject {
public:
	LeakBaseObject() {
		printf("LeakBaseObject Construction!\n");
		
	}
	~LeakBaseObject() {
		printf("LeakBaseObject Destruction!\n");
	}
};

// 派生类
class LeakObject : public LeakBaseObject {
public:
	LeakObject() {
		printf("LeakObject Construction!\n");
		p = new int[100];
	}
	~LeakObject() {
		printf("LeakObject Destruction!\n");
		if (p) {
			delete[] p;
			p = nullptr;
		}
	}
private:
	int *p;
};

int main() {
    // 基类指针指向派生类
	LeakBaseObject *pLBObj = new LeakObject();
	delete pLBObj;
}
  • 来看下控制台输出:
LeakBaseObject Construction!
LeakObject Construction!
LeakBaseObject Destruction!
  • 派生类对象在 new 的时候,会调用基类的构造函数,再调用自身的构造函数;
  • 当基类指针指向派生类对象,并且进行 delete 的时候,只会调用基类的析构函数,不会调用派生类的析构函数,原因是因为基类的析构函数不是虚的,所以这里只要派生类的成员变量中有进行 new 操作申请内存,并且在析构函数里面 delete 的,这里就有可能产生内存泄漏;
  • 泄漏字节数 = 100 * 4 + 52 = 452 字节;
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 2 at 0x000000004671BB20: 400 bytes ----------
  Leak Hash: 0x1B693661, Count: 1, Total 400 bytes
  Call Stack (TID 8688):
    ... ...
  Data:
    ... ...

Visual Leak Detector detected 1 memory leak (452 bytes).
  • 解决方案只要在基类的 ~LeakBaseObject() 函数前加上 virtual 关键字即可;
class LeakBaseObject {
public:
	LeakBaseObject() {
		printf("LeakBaseObject Construction!\n");
	}
	virtual ~LeakBaseObject() {
		printf("LeakBaseObject Destruction!\n");
	}
};
2、STL容器泄漏
  • 所有STL容器的内存分配都是动态分配的,也就是在声明的时候,其实已经做了内存分配,还是拿虚析构来举例;
  • 以下这段代码和上面那段代码的不同之处只有派生类的成员变量这里,从 int 指针变成了 vector;
#include "vld.h"

using namespace std;

// 基类
class LeakBaseObject {
public:
	LeakBaseObject() {
		printf("LeakBaseObject Construction!\n");
		
	}
	~LeakBaseObject() {
		printf("LeakBaseObject Destruction!\n");
	}
};

// 派生类
class LeakObject : public LeakBaseObject {
public:
	LeakObject() {
		printf("LeakObject Construction!\n");
	}
	~LeakObject() {
		printf("LeakObject Destruction!\n");
	}
private:
	vector<int>    m_kVector;
};

int main() {
    // 基类指针指向派生类
	LeakBaseObject *pLBObj = new LeakObject();
	delete pLBObj;
}
  • 运行结果如下:
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 2 at 0x00000000781758C0: 16 bytes ----------
  Leak Hash: 0x211451AF, Count: 1, Total 16 bytes
  Call Stack (TID 20216):
    MSVCR120D.dll!operator new()
    d:\microsoft visual studio 12.0\vc\include\xmemory0 (848): MemoryLeakTester.exe!std::_Wrap_alloc<std::allocator<std::_Container_proxy> >::allocate()
    d:\microsoft visual studio 12.0\vc\include\vector (624): MemoryLeakTester.exe!std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >::_Alloc_proxy() + 0xF bytes
    d:\microsoft visual studio 12.0\vc\include\vector (603): MemoryLeakTester.exe!std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > >() + 0xA bytes
    d:\microsoft visual studio 12.0\vc\include\vector (681): MemoryLeakTester.exe!std::vector<int,std::allocator<int> >::vector<int,std::allocator<int> >()
    e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (25): MemoryLeakTester.exe!LeakObject::LeakObject()
    e:\dreamrivakes\trunk\program\other\memoryleaktester\memoryleaktester\memoryleaktester\main.cpp (64): MemoryLeakTester.exe!main() + 0x30 bytes
    f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c (466): MemoryLeakTester.exe!mainCRTStartup()
    KERNEL32.DLL!BaseThreadInitThunk() + 0x14 bytes
    ntdll.dll!RtlUserThreadStart() + 0x21 bytes
  Data:
    ...

Visual Leak Detector detected 1 memory leak (68 bytes).
  • 不出所料,泄漏字节数为 68 = 16 + 52;
  • vector 也是一个类,也有它的构造函数,构造过程中也会进行对应的内存分配,具体堆栈如下:
vector构造函数
_Vector_alloc构造函数
_Alloc_proxy
_Alproxy.allocate
_Alloc::allocate
_Allocate
  • 跟进到 STL 底层源码,_Allocate 的函数实现如下:
template<class _Ty> inline                                            //i
_Ty *_Allocate(size_t _Count, _Ty *)
{	
    // allocate storage for _Count elements of type _Ty
	void *_Ptr = 0;
	if (_Count == 0)
		;
	else if (((size_t)(-1) / sizeof (_Ty) < _Count)                  // ii
		|| (_Ptr = ::operator new(_Count * sizeof (_Ty))) == 0)      // iii
		_Xbad_alloc();	// report no memory
	return ((_Ty *)_Ptr);
}
  • i. 这是一个模板函数,_Ty 代表具体的类型;
  • ii. 64位程序的 size_t 是 unsigned long long,所以 -1 转换成无符号整数,是 64位无符号整数的最大值,这步判断主要时对最大内存申请进行限制;
  • iii. ::operator new 就是 new 关键字,构造函数里 vector 会初始化 _Count 个元素数据;
  • 再去看 vector 的析构函数,会对这部分 new 出来的内存进行释放,所以如果派生类的析构函数没有调用到的话, vector 的析构函数也不会调用,就会产生内存泄漏了;
3、Protobuf的内存泄漏
  • 注意进程退出前,需要调用一下 pb 的静态函数 :
	google::protobuf::ShutdownProtobufLibrary();
  • 否则,会产生内存泄漏,一般不调用也没事,进程关闭自动会释放,只是要查问题,所以还是调用一下,这里释放的是 Protobuf 内部的反射对象;
  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
c++面试题53个问题 1.C++的三大特性 2.C和C++的区别 3.全局变量和局部变量在内存分配上有何不同 4.static的作用 5.const解释其作用 6.指针和引用的区别 7.智能指针 8.简述深拷贝和浅拷贝的区别 9.编写my_strcpy函数,实现与库函数strcpy类似的功能,不能使用任何库函数 10.请讲述堆和栈的区别 11.全局变量和局部变量有什么区别?实怎么实现的?操作系统和编译器是怎么知道的 12.new、delete、malloc、free之间的关系 13. 头文件种的ifndef/define/endif 是干什么用的 14.TCP和UDP有什么区别 15.STL库用过吗?常见的STL容器有哪些?算法用过哪几个 16.同步IO和异步IO的区别 17.说下你对内存的了解 18.C++文件编译与执行的四个阶段 19.extern关键字的作用 20.#define和const的区别 21.结构体struct和共同体union(联合)的区别 22.C++中vector和list的区别 23.结构体和类的区别 24.STL中map和set的原理(关联式容器) 25.MFC的消息机制 26.消息映射 27.列举几种进程的同步机制,并比较其优缺点 28.数组和链表的区别 29.MFC主要要用到哪几个类?及其各个类的作用 30.MFC六大核心机制 31.OnDraw和OnPaint 32.win32程序的消息响应机制是如何实现的 33.MFC中的消息响应机制是如何实现的 34.WM_COMMAND命令消息处理顺序 35.MFC序列化的概念 36.PeekMessage和GetMessage的主要区别 37.WIN32创建窗口程序基本函数 38.Windows中的系统消息循环占用CPU的疑问 39.队列消息与非队列消息 40.GDI对象绘图步骤 41.设备上下文DC 42.GDI位图绘制步骤 43.当模态对话框点开后,主窗口还能响应处理消息吗 44.MFC的消息分类 45.CListCtrl 虚拟列表技术 46.虚函数是怎么实现的 47.什么是内存泄漏?面对内存泄漏指针越界,你有哪些方法? 48.变量的声明和定义有什么区别 49.sizeof是一个操作符,strlen是库函数 50.写一个“标准”宏MIN 51.简述strcpy sprintf与mencpy的区别 52.链表与数组的区别 53.简述队列和栈的异同

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值