C/C++内存管理

1. C/C++内存分析

先通过下面一段代码,来讲解相关问题:

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	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);
}

判断以下变量的位置:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar____ staticGlobalVar____ staticVar____ localVar____ num1 ____

char2____ *char2___ pChar3____ *pChar3____ ptr1____ *ptr1____

  1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。

  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。

  4. 数据段--存储全局数据和静态数据。

  5. 代码段--可执行的代码/只读常量。

可以看到全局变量,和static静态修饰的变量存放在静态区,所以globalVar, staticGlobalVar,staticVar都是在静态区,函数内部的局部变量,在函数栈帧上开辟,所以localVar,num1,char2,pChar3,ptr1都是在栈上,*char2即char首元素也是在栈上,注意*pChar3用const修饰,所以pChar解引用后表示的首元素,是在常量区上的,而ptr1指向的是动态开辟的空间,解引用后当然是在堆上。

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

malloc

cpp 复制代码
int* p1 = (int*)malloc(sizeof(int) * 10);

malloc传参时,是传要开辟空间的字节数,最后还要手动free(p1)释放空间避免内存泄漏。

calloc

cpp 复制代码
int* p2 = (int*)calloc(4, sizeof(int));

calloc传参时,传的第一个参数是要开辟数据的个数,第二个参数是数据类型,但calloc会进行初始化,全部为零,而malloc全为随机值,最后还是要手动free(p2)释放空间避免内存泄漏。

realloc

cpp 复制代码
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 5);

realloc是当前面开辟的空间不够用时,就需要扩容一块新的空间,第一个参数是要扩容空间的指针,和重新开辟空间的字节数,注意:这个relloc分为原地扩容 ,即当原来的空间后面有多余的空间没被占用,且扩容空间较小时,扩容空间会直接在后面扩容,即p2,p3指向同一块地址,还有一种叫异地扩容,就是重新开辟一块空间,再将p2的按字节复制给p3。这里不用手动释放p2,在realloc函数中已经将p2释放掉了,但是需要手动释放p3。

3.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

3.1 new/delete操作内置类型

例如:

cpp 复制代码
void Test()
{
	// 动态申请一个int类型的空间
	int* ptr4 = new int;
	// 动态申请一个int类型的空间并初始化为10
	int* ptr5 = new int(10);
	// 动态申请3个int类型的空间
	int* ptr6 = new int[3];
	// 动态申请3个int类型的空间并且初始化
	int* ptr7 = new int[3] {1,2,3};
  	int* ptr8 = new int[5] {1,2,3};
    delete ptr4;
    delete ptr5;
    delete[] ptr6;
    delete[] ptr7;
    delete[]ptr8;
}

对于内置类型,没有初始化的为随机数,开辟数组时,初始化个的个数不够时,剩余部分为零。还要注意new和delete匹配使用:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new\[\]和delete\[\]。

3.2 new和delete操作自定义类型

例如:

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		_i++;
		cout << _i<<"A():" << this << endl;
	}
	~A()
	{
		_j++;
		cout <<_j<< "~A():" << this << endl;
	}
private:
	int _a;
	static int _i;
	static int _j;
};

int A::_i=0;
int A::_j=0;

int main()
{
	// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
	// 内置类型是几乎是一样的
	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最大区别是 new/delete对于自定义类型除了开空间还会调用构造函数和析构函数。

自定义类型也可以通过给定一定的参数初始化,这里是通过构造函数完成的。

例如:

cpp 复制代码
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;

};

int main()
{
	//A a1(1), a2(2), a3(3);
	//A* p6 = new A[3]{a1, a2, a3};
	A* p6 = new A[10]{ {1,1}, {2,2}, {3,3} };
	delete[] p6;

	return 0;
}

这里传参时应该传A类的对象进行初始化,但是可以利用隐式类型转化(小编前面C++类中有详细讲解),直接传构造函数需要的类型参数就可以了,这样方便书写。

4. operator new与operator delete函数

4.1 operator new与operator delete函数

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

cpp 复制代码
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来释放空间的。

从转到反汇编的角度可以看出:new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

5. new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

new的原理

  1. 调用operator new函数申请空间

  2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作

  2. 调用operator delete函数释放对象的空间

new TN的原理

  1. 调用operator new 函数,在operator new 中实际调用operator new函数完成N个对象空间的申请

  2. 在申请的空间上执行N次构造函数

delete 的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

  2. 调用operator delete\[\]释放空间,实际在operator delete\[\]中调用operator delete来释放空间

6.new,delete,malloc,free混用,会发生什么

例子1:(malloc和delete)

cpp 复制代码
int main()
{
	int* p5 = (int*)malloc(sizeof(int) * 10);
	delete p5;   
	return 0;
}

例子2:(new TN和delete)

cpp 复制代码
int main()
{
	int* p6 = new int[10];
	delete p6;
	return 0;
}

对于上述两种情况会报错吗?实际上不会,因为delete的底层就是用的free,对于内置类型就可以正常运行。

例子3:(new TN和delete)

cpp 复制代码
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;

};
int main()
{
	A* p7 = new A[10];
	delete p7;    
	return 0;
}

可以看到,报了关于内存的问题,并且发现只调用了一次析构函数,这是为什么呢?

从反汇编看:

p7的内存:

可以看到这里size是84,A是八个字节,十个不应该是80个吗,为什么是84呢?其实在new一块连续的空间时,会在前面开四个字节,来表示开辟数据的个数,这里是10,所以这里一共就80+4,84个字节,但返回的指针是从第一个有效数据空间开始的,所以当delete时,就会从开辟的连续空间的中间开始释放,而不是从指向表示数据个数的空间那里开始释放,这样,就会报错。

例子4:(当自定义类型,没有显示析构函数)

cpp 复制代码
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A():" << this << endl;
	}

	//~A()
	//{
	//	cout << "~A():" << this << endl;
	//}
private:
	int _a1;
	int _a2;

};
int main()
{
	A* p7 = new A[10];
	delete p7;    
	return 0;
}

这里又不会报错了,这又是为什么?这里没有显示的调用析构函数,编译器默认的析构函数,发现这里A类中没有需要释放的资源,就会优化,不去调用析构函数,也就不会去多开四个字节,去存数据个数,去记录掉几次析构,就直接free(delete底层就是free)掉了。

7. malloc/free和new/delete的区别

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

  1. malloc和free是函数,new和delete是操作符

  2. malloc申请的空间不会初始化,new可以初始化

  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象, 中指定对象个数即可

  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常

  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理释放

相关推荐
SXJR8 分钟前
langchain4j是如何保证tools或者funcation call不出错的
java·网络·数据库·ai·语言模型
谷谷地图下载器10 分钟前
全球、台湾省的无水印·街景数据(离线数据),专为可视化项目定制,支持国产化
javascript·c++·3d·arcgis·sqlite
程序大视界16 分钟前
【C++ 从基础到项目实战】C++(五):类与对象基础——构造、析构与访问控制
开发语言·c++·cpp
子一!!16 分钟前
spring基础学习
java·学习·spring
代码中介商16 分钟前
掌握C++ std::bind:参数绑定与灵活调用
开发语言·c++
拽着尾巴的鱼儿21 分钟前
Java 对象的深拷贝和浅拷贝
java·开发语言
数据法师25 分钟前
Crow Translate :开源桌面划词翻译工具
c++·qt·开源
龙腾AI白云33 分钟前
数字孪生:虚拟调试,真实交付
c语言·virtualenv·知识图谱
王璐WL1 小时前
【C++】经典易错题(2)
c++
我不是懒洋洋1 小时前
手写一个异步日志库:从printf到高性能无锁日志
java·c语言·开发语言·c++·visual studio