C++软件开发之内存泄漏闭坑方法

(一)引言

C++语言没有自动垃圾回收机制,程序员进行软件开发时,需要时刻考虑内存的分配和释放,必须通过代码显式进行对象的创建和销毁,所以这也是有些人感觉C++难学的原因之一。但作为熟练的C++程序员必须掌握内存的管理方法,对自己所设计的程序逻辑要了然于胸,做到对系统内存的"有借有还"。

(二)程序的内存空间简介

程序内存空间的管理非常重要,涉及到程序运行效率和稳定性;C/C++程序运行时,主要涉及以下几种内存区域:代码区、全局数据区、堆区、栈区、常量数据区。

  • 代码区:存放编译后程序的机器指令(代码),该区域只读不允许修改,例如函数的定义放在代码区。
  • 全局数据区:存储全局变量和静态变量,该区域在程序启动时分配,直到退出结束时释放,该区域又分为全局初始化数据库和未初始化数据区。
  • 栈区:用于存储局部变量和函数调用的数据(如返回地址、参数、局部变量等),该区域由编译器自动管理,遵循后进先出原则。函数调用时分配,退出时释放。本文所讲的内存泄漏问题就是出现在该区域,没有对动态分配的内存进行正确管理导致的。
  • 堆区:用于动态内存分配,如使用malloc、new等函数分配的内存。一般由开发者控制内存分配和释放,需要人工设计机制对分配内存进行管理以避免内存泄漏。
  • 常量区:用于存放常量值,例如字符串常量、整型常量等,该区域的数据只读不允许修改,编译器将程序的常量数据存放在该区域。

(三)常见的内存泄漏问题

1、未成对使用new和delete

C++标准中new和delete是两个非常重要的操作符,它们用于动态内存管理,并且是C++语言的核心组成部分。开发者使用new分配内存并赋值给某指针变量之后,不再使用时必须调用delete进行显式释放先前分配的内存;此外内存分配和释放操作符成对出现还涵盖使用new [xx](xx代表数组的长度)分配的内存数组,也必须使用delete [] 进行释放。如下是代码示例:

cpp 复制代码
void memleak_func1(void)
{
	//分配字符指针
	char* pstrChar = new char;
	*pstrChar = 'b';

	std::cout << "---------------------\n";
	printf("   字符的ASCII码=%d \n\r", *pstrChar);
	std::cout << "---------------------\n\n\n\n";

	delete pstrChar;

	//分配1024的字符串指针/字符数组
	int len(1024);
	char* strName = new char[len];
	strcpy_s(strName, len, "TOM");

	std::cout << "---------------------\n";
	std::cout << "  Name  is  " << strName << std::endl;
	std::cout << "---------------------\n\n\n\n";

	delete [] strName;
}

2、未成对使用malloc()和free()

不同于C++标准中的new操作符,C标准提供了malloc()库函数用于动态内存分配,采用mallco分配出来的指针不再使用时,必须使用另一个库函数free()释放该指针所指向的内存区域,否则就会带来内存泄漏。

使用void * malloc(size_t size)函数分配内存时,其形参size为准备分配内存区域的字节数,例如需要分配一个字符类型指针则需要char * pstrData = (char*)malloc(sizeo(char))或char * pstrData = (char*)malloc(1),而分配一个长度为32个字符的字符串指针时需要char * pstrBuffer = (char*)malloc(32)。使用malloc分配的指针变量不再使用时,必须使用malloc释放其指向的内存区域。

示例代码如下:

cpp 复制代码
void memleak_func2(void)
{
	//分配字符指针
	char* pstrChar = (char*)malloc(sizeof(char));
	*pstrChar = 'f';
	std::cout << "---------------------\n";
	printf("   字符的ASCII码=%d \n\r", *pstrChar);
	std::cout << "---------------------\n\n\n\n";
	free(pstrChar);

	//分配1024的字符串指针/字符数组
	int len(1024);
	char* strName = (char*)malloc(1024);
	strcpy_s(strName, len, "Mike");

	std::cout << "---------------------\n";
	std::cout << "  Name  is  " << strName << std::endl;
	std::cout << "---------------------\n\n\n\n";

	free(strName);
}

3、混合使用new和free()

首先,new是C++标准的操作符,free(void * ptr)是C标准库的函数,两者不可混合使用。C++是面向对象的程序设计语言,new操作符会触发类(class)的构造函数构建出对象,而调用delete操作符销毁对象指针时则会触发相应类(class)的析构函数,如果new出来的对象指针调用free()来释放则只会释放指针指向的内存区域而不会触发类(class)的析构函数,如果类成员比较复杂包含其他类对象的指针,析构函数不被调用则会导致内存泄漏。

代码示例如下,使用free销毁new出来的CStudent对象指针pStudent,其析构函数~CStudent()并未被调用,而使用delete pStudent则会输出"The Class ~CStudent func is Called!"。

cpp 复制代码
//学生类
class CStudent
{
public:
	CStudent(int id, const char* pstr)
		: m_id(id)
	{
		assert(NULL != pstr);
		size_t len = strlen(pstr);
		m_name = new char[len + 1];
		strcpy_s(m_name, len + 1, pstr);
	}

	virtual~CStudent(void)
	{
		assert(NULL != m_name);
		delete[] m_name;
		std::cout << "The Class  ~CStudent func  is  Called!" << std::endl;
	}

	void printf_info(void)
	{
		std::cout << "---------------------\n";
		std::cout << "The student'id    is  " << m_id << std::endl;
		std::cout << "The student'name  is  " << m_name << std::endl;
		std::cout << "---------------------\n\n\n\n";
	}

private:
	int		m_id;
	char *	m_name;
};

void memleak_func3(void)
{
	CStudent* pStudent = new CStudent(16, "John");
	pStudent->printf_info();

	//delete pStudent;
	free(pStudent); //pStudent为new出来的,现在使用free销毁,不会调用析构函数
}

4、混合使用malloc()和delete

前面内容已介绍过,malloc()和free()是C标准库函数,new和delete是C++标准操作符,后者会触发C++类的构造和析构函数,而前者是C库函数前者则不会触发构造和析构函数。

对于CStudent类,使用malloc()函数时不会触发类的构造函数,所以其成员CStudent::m_name不会被构建出来,会导致后续程序执行错误。示例代码如下:

cpp 复制代码
void memleak_func3(void)
{
	CStudent* pStudent = new CStudent(16, "John");
	pStudent->printf_info();
	//delete pStudent;
	free(pStudent); //pStudent为new出来的,现在使用free销毁,不会调用析构函数
}

前面介绍了malloc()和delete针对复杂类对象混合使用的情况,对于基本数据类型(char、int等),虽然malloc()和delete、new和free()混合使用不涉及构造函数和析构函数的调用,暂不会导致程序崩溃,但是不同平台、不同编译环境中程序的行为是不确定的,出现问题也难以调试和定位,所以一定不要混合使用两种方式分配释放内存。

(四)C库其它内存动态管理函数

C标准库中还有calloc、realloc函数,与malloc一样,调用这两个函数所分配的内存都需要调用free进行释放。此外,还有alloca函数,它与前面几个函数不同。下面详细介绍:

1、 calloc函数

calloc是C标准库的内存分配函数,与malloc不同之处在于需要输入元素数量和单个元素的字节数,其函数定义为void *calloc(size_t num, size_t size)。其中num就是要分配的元素个数,size为每个元素的大小(以字节为单位)。

函数执行成功则返回void*类型的指针,否则返回NULL。与malloc不同的是,使用calloc可以将分配的内存自动初始化为0,而malloc则不进行所分配内存的初始化。

2、realloc函数

realloc是C标准库另一个动态内存管理函数,主要用于调整已分配内存块的大小,其函数定义为void * realloc(void * ptr, size_t size)。其中ptr为已分配好内存块的指针,size为准备新分配内存块的字节数。

****(1)执行结果:****函数执行成功则返回void*类型的指针,否则返回NULL。

(2)‌ ****内存扩展‌:****如果原内存块之后有足够空间,realloc 可能会直接在原地扩展内存,无需移动数据。

(3) ****‌内存迁移‌:****如果原内存块后没有足够空间,则会分配一块新的内存,并将旧数据复制到新内存中,然后释放旧内存。

(4)输入形参:形参ptr为NULL则该函数类似malloc,分配一块全新内存块;形参size为0‌则该函数会释放该输入形参ptr所指向的内存块并返回NULL。

****(5)注意问题:****调用realloc函数后,原指针ptr可能失效,应将返回值赋给新指针或更新原指针;如果realloc函数执行失败,原指针ptr指向的内存块不会被释放,须检查返回值;此外,ptr指向的内存成功进行扩展并迁移后,原内存中的数据在重新分配后将会被保留,但新扩展分配的内存不会初始化。

3、 alloca 函数

****(1)使用及注意事项:****alloca函数用于在函数栈区动态分配内存,通常用于需要临时分配大量内存的场景。与malloc不同,alloca分配的内存会在函数返回时自动释放,无需手动调用free。alloca是在当前程序所调用函数的栈区进行内存分配,其分配和释放速度非常快,且所调用函数返回时会自动释放,所以返回的指针不能被其它程序模块调用(已成野指针)。需要注意的是alloca函数如果分配栈区内存失败,不是返回NULL指针而是导致栈溢出程序会异常。

****(2)不能跨平台使用:****alloca不是C标准库的函数,不同平台或编译器可能不支持,可移植性差必须慎用。

(五)结语

本文介绍了C++软件开发之内存泄漏常见的问题,并且从C标准库提供的动态分配内存的函数、C++语言的操作符new和delete的使用方面进行了基础性介绍和知识普及。其实在软件开发过程中,此类应用属于最简单的闭坑方法,为防止内存泄漏或者避免内存对象频繁分配和释放,开发人员应当根据业务需要自行设计专用数据结构来统一管控对象的创建和销毁,例如设计一些队列、栈等模板或者直接使用STL标准模板库,后面找机会再进行这方面的介绍。

相关推荐
Ethan-D1 小时前
#每日一题19 回溯 + 全排列思想
java·开发语言·python·算法·leetcode
Benny_Tang1 小时前
题解:CF2164C Dungeon
c++·算法
满栀5852 小时前
分页插件制作
开发语言·前端·javascript·jquery
froginwe112 小时前
C 标准库 - <stdio.h>
开发语言
zwtahql2 小时前
php源码级别调试
开发语言·php
qq_406176142 小时前
深入剖析JavaScript原型与原型链:从底层机制到实战应用
开发语言·前端·javascript·原型模式
青小莫2 小时前
C语言vsC++中的动态内存管理(内含底层实现讲解!)
java·c语言·c++
持梦远方3 小时前
算法剖析1:摩尔投票算法 ——寻找出现次数超过一半的数
c++·算法·摩尔投票算法
{Hello World}3 小时前
Java抽象类与接口深度解析
java·开发语言