C/C++内存管理


1 C/C++内存分布

在我们编写的程序通常需要有不同性质或者不同类型的数据,而不同的数据是存储在不同的区域中的(如堆、栈等),将数据分类存储可以更好的管理数据。接下来,我们来看看下面一段代码中的各个类型的数据的内存分布情况:

int globalVar = 1; //全局变量,存储在静态区(数据段)
static int staticGlobalVar = 1; //静态全局变量,存储在静态区(数据段)
void Test(){
	static int staticVar = 1; //静态局部变量,存储在静态区(数据段)

	int localVar = 1; //局部变量,存储在栈区

	int num1[10] = { 1, 2, 3, 4 }; //num1(表示数组首元素地址),存储在栈区,sizeof(num1) = 40

	//char2(表示数组首元素地址),char2 及 *char2 都存储在栈区;
	//该代码表示在栈区开辟一块字符数组的空间,并将字符串常量拷贝到空间中
	char char2[] = "abcd"; //sizeof(char2) = 5 ; strlen(char2) = 4

	//指针pChar3作为局部变量存储在栈区,指向常量池中的字符串常量,因此*pChar3存储在常量池(代码段)中
	const char* pChar3 = "abcd"; //sizeof(pChar3) = 4/8 取决于运行机器是32位还是64位;strlen(pChar3) = 4
	
	//以下代码均表示在堆区开辟一块空间,并用存储在栈上的指针ptr指向开辟的空间
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

说明:

  • 栈又叫堆栈 – 非静态局部变量、函数参数、函数返回值等等都存在栈区,栈是向下增长的。
  • 内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
  • 堆用于程序运行时的动态内存分配,堆是可以向上增长的。
  • 数据段 – 存储全局数据和静态数据。
  • 代码段 – 可执行的代码、只读常量。

2 C语言中动态内存管理方式(malloc/calloc/realloc/free)

示例代码:

void Test(){
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	free(p3);
}

关于C语言中动态内存开辟方式的区别:

  • malloc: 在堆上动态开辟一块内存空间,不会初始化。
  • calloc: 在堆上开辟一块内存空间,并将每个字节的空间都初始化为0。
  • realloc: 对已动态开辟的内存大小进行调整。如果原有空间之后还有足够大的连续空间可以调整,此时要扩展内存就可以直接在原有内存之后追加空间,原来空间的数据不发生变化,原有空间的起始地址与新空间的起始地址相同;如果原有空间之后没有足够大的连续空间可以调整,此时内存扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,此时函数返回的是一个新的内存地址,而原来开辟的内存由编译器自动释放,因此在上述代码中的 p2 指针指向的内存空间不需要我们再进行释放;此外,realloc函数也可以用来缩小开辟内存空间,此时,若原有空间存有数据,缩小空间后将会导致数据截断,因为缩小后的空间不再足以存放原有空间中的所有数据。

关于C语言中动态内存开辟方式的一些具体描述还可参见之前的博客:C语言动态内存分配


3 C++内存管理方式

C语言中的内存管理方式在C++中可以继续使用,但对于存储如自定义类型数据的内存的开辟就不是那么方便了,而且其使用起来也比较麻烦,因此C++又提出了自己的内存管理方式:通过 newdelete 操作符进行动态内存管理。

3.1 new和delete操作内置类型

示例代码:

void Test(){
	// 动态申请一个int类型的空间
	int* ptr1 = new int;

	// 动态申请一个int类型的空间并初始化为10
	int* ptr2 = new int(10);

	// 动态申请5个int类型的空间
	int* ptr3 = new int[5];

	// 动态申请5个int类型的空间,并初始化为{1, 2, 0, 0, 0}
	int* ptr4 = new int[5] {1, 2};

	delete ptr1;
	delete ptr2;
	delete[] ptr3;
	delete[] ptr4;
}

如下为调试运行代码时监视窗口中查看到了内存开辟情况:


new 和 delete 操作符使用说明示意图如下:

注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用 new[] 和 delete[] 。两对必须匹配使用。


3.2 new和delete操作自定义类型

示例代码:

class A{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	
	~A(){
		cout << "~A():" << this << endl;
	}
	
private:
	int _a;
};

int main(){
	// new/delete 和 malloc/free 最大区别是:
	//new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;
	// 内置类型是几乎是一样的
	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	free(p3);
	delete p4;
	A* p5 = (A*)malloc(sizeof(A) * 10);
	A* p6 = new A[10];
	free(p5);
	delete[] p6;
	return 0;
}

程序运行输出结果:


说明:在申请自定义类型的空间时,new 会调用其构造函数,delete 会调用其析构函数,而 malloc 和 free不会。


4 operator new与operator delete函数

newdelete 是用户进行动态内存申请和释放的操作符,operator newoperator delete 函数是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间,也就是说 new 和 delete 是对 operator new 和 operator delete 的封装。

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;
申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK);  /* block other threads */
	__TRY
		        /* get a pointer to memory block header */
		pHead = pHdr(pUserData);
	         /* verify block type */
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg(pUserData, pHead->nBlockUse);
	__FINALLY
		_munlock(_HEAP_LOCK);  /* release other threads */
	__END_TRY_FINALLY
		return;
}
/*
free的实现
*/
#define   free(p)    _free_dbg(p, _NORMAL_BLOCK) //宏函数

通过上述两个全局函数的实现知道,operator new 实际也是通过 malloc 来申请空间,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足的应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的注意:operator new 和 operator delete 不是对操作符 new 和 delete 的运算符重载,而是普通的函数名,是先有的函数底层实现,才有的 new 和 delete 操作符的上层封装使用。


5 new 和 delete 的实现原理

5.1 内置类型

如果申请的是内置类型的空间, new 和 malloc ,delete 和 free 基本类似,不同的地方在于:new 和 delete 申请和释放的是单个元素的空间,new[] 和 delete[]申请的是连续空间,而且 new 在申请空间失败时会抛出异常,malloc 则会返回 NULL。

5.2 自定义类型

  • new 的原理

    • 调用 operator new 函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造
  • delete 的原理

    • 在空间上执行析构函数,完成对象中资源的清理工作
    • 调用 operator delete 函数释放对象的空间
  • new T[N] 的原理

    • 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请
    • 在申请的空间上执行 N 次构造函数
  • delete[] 的原理

    • 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理
    • 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间

关于 delete 的实现机制:

我们可以看到,在使用 new 操作符开辟空间时,如果要开辟一块连续的内存空间,通常会指明相应的个数,而当我们要释放空间时,却不需要在 delete[] 中指明个数,这是由于编译器在我们使用 new[] 开辟内存空间时会在头上多开辟 4 个字节的空间用来存储空间对象个数以便知道之后该调用多少次析构函数来进行空间对象中资源的清理,同时返回指向待使用空间的指针,而 delete[] 在释放空间时会根据空间指针向前寻找前面空间所存储的空间对象个数,再根据此个数来判断需要调用相应对象类型的几次析构函数;但如果开辟空间时编译器判断对于该类型对象的释放不需要调用析构函数时,则不会再多开辟那 4 个字节的空间。因此,再次强调,new 和 delete ,new[] 和 delete[] ,malloc/calloc/realloc 和 free 必须匹配使用。


6 定位new 表达式(replacement new)

定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

  • 使用格式:
    new(place_address)typenew(place_address)type(initializer-list)
    place_address 必须是一个指针,initializer-list 是类型的初始化列表

  • 使用场景:
    定位new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式显示调用构造函数进行初始化。

    示例代码:

class A{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}

	~A(){
		cout << "~A():" << this << endl;
	}

private:
	int _a;
};

// 定位new/replacement new
int main(){

	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
	p1->~A();
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();
	operator delete(p2);

	return 0;
}

7 关于C/C++内存管理的常见问题

7.1 malloc/free 和 new/delete 的区别

  • malloc/free 和 new/delete 的共同点: 都是从堆上申请空间,并且需要用户手动释放。

  • malloc/free 和 new/delete 的不同点:

    • malloc 和 free 是函数,new 和 delete 是操作符
    • malloc 申请的空间不会初始化,new 可以初始化
    • malloc 申请空间时,需要手动计算空间大小并传递,new 只需要在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象的个数即可
    • malloc 的返回值类型为 void* ,在使用时必须强转,new 不需要,因为 new 后跟的就是空间的类型
    • malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空,new 不需要,但 new 需要捕获异常
    • 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数和析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理

7.2 内存泄漏

7.2.1 内存泄漏的概念及危害

内存泄漏: 内存泄漏是因为疏忽或错误造成程序未能释放已经不再使用的的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害: 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

示例代码:

void MemoryLeaks(){
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;

	// 2.异常安全问题
	int* p3 = new int[10];

	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.

	delete[] p3;
}

7.2.2 内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap Leak)

    堆内存指的是程序执行中需要通过malloc / calloc / realloc / new 等等从堆中分配的一块内存,用完后必须调用相应的 free 或 delete 释放。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak

  • 系统资源泄漏

    指程序使用系统分配的资源,比方套接字、文件描述符、管道等却没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。


7.2.3 内存泄漏检测

在vs下,可以使用Windows操作系统提供的 _CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其它更准确的位置信息。

示例代码:

int main(){
	int* p = new int[10];
	// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
	_CrtDumpMemoryLeaks();
	return 0;
}
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置

输出窗口:


强调: 在写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的还可以采用上述方式定位。如果工程比较大,内存泄漏位置比较多,不太好查找时,一般都是借助第三方内存泄漏检测工具处理的。


7.2.4 如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记得匹配释放。注意:如果碰上异常时,就算释放了,可能还是会出问题,此时需要使用智能指针来管理才有保证。
  • 采用RALL思想或者智能指针来管理资源。
  • 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  • 使用内存泄漏工具检测。(但可能有些工具不够靠谱,或者收费昂贵)

总结:内存泄漏非常常见,其解决方案分为两种:事前预防型(智能指针等);事后查错型(内存泄漏检测工具)。


以上是我对C/C++中内存管理相关知识的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大米饭_Mirai

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

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

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

打赏作者

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

抵扣说明:

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

余额充值