前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++ 初阶路】--- C++内存管理

【C++ 初阶路】--- C++内存管理

作者头像
用户11029269
发布2024-07-14 08:30:33
620
发布2024-07-14 08:30:33
举报
文章被收录于专栏:计算机语言及操作系统的学习

一、C/C++内存分布

代码语言:javascript
复制
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);
} 
  1. 选择题:

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

  • 全局变量,静态区

staticGlobalVar在哪里?

  • 静态全局变量,静态区

staticVar在哪里?

  • 静态局部变量,生命周期延长,静态区

localVar在哪里?

  • 局部变量,出了函数作用域就销毁,栈区

num1在哪里?

  • 在栈上开辟的数组num1,大小40字节,出栈销毁,数组名num1为指向第一个元素的指针,存放在 栈区

char2在哪里?

  • "abcd"原是在常量区,后拷贝到栈区形成数组,char2指向栈上数组的第一个字符,存放在 栈区

*char2在哪里?

  • 由上,解引用*char2得到拷贝到栈上的数组的第一个字符a栈区

pChar3在哪里?

  • 字符串"abcd"在常量区,pChar3指向这个字符串(地址),但pChar3本身为指针,存放在 栈区

*pChar3在哪里?

  • 由上*pChar3,解引用后指向常量字符串,在 常量区

ptr1在哪里?

  • 同理,malloc()在堆区上开辟了一段空间,ptr1指针指向这段动态开辟的堆区空间,指针本身还在 栈区

*ptr1在哪里?

  • 由上,malloc()开辟空间在堆区,*ptr1解引用后拿到堆区上的数据,所以在 堆区
  1. 填空题: sizeof(num1) = 40; // sizeof(int)* 10 — sizeof(数组名),此时为整个数组大小 sizeof(char2) = 5; // 还有一个'\0' strlen(char2) = 4; // 到'\0'结束,此时char2为字符数组的第一个元素的地址 sizeof(pChar3) = 4 or 8; // 指针大小固定为 4 or 8 区别在于机器位数 strlen(pChar3) = 4; sizeof(ptr1) = 4 or 8; 如还需进一步了解,还可参考 详解sizeof()和strlen()的细节及用法 一文。sizeof是一个运算符,在编译时根据类型大小定义,自定义类型根据内存对齐规则计算大小(编译时就是一个具体的值!);而strlen是一个函数,运行时计算字符长度,事实上在编译时就没有此函数了,被转换为了一个个指令(即call此函数实现地址,执行内部指令`)!

【说明】

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

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

【面试题】: malloc/calloc/realloc的区别? 参考 【c语言】详解动态内存管理 一文。

二、C++内存管理方式

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

2.1 new/delete操作内置类型

  1. 用法上,变简洁了
代码语言:javascript
复制
int* p0 = (int*)malloc(sizeof(int));
int* p1 = new int;
int* p2 = new int[10]; // new 10 个int 对象

delete p1;
delete[] p2;
  1. 可以控制初始化
代码语言:javascript
复制
int* p3 = new int(10); //动态申请一个int类型的空间
int* p4 = new int[10] {1, 2, 3, 4, 5};  //动态申请十个int类型的空间并初始化为{...}, 其余为0

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

2.2 new和delete操作自定义类型

  1. new/delete对于自定义类型除了开空间还会调用构造函数和析构函数,内置类型是几乎是一样的
代码语言:javascript
复制
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		std::cout << "A():" << this << std::endl;
	}
	~A()
	{
		std::cout << "~A():" << this << std::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;
	return 0;
}

调用new动态开辟内存,编译器会自动帮我们计算要开辟的空间,并调用operator new全局函数(其是对malloc的封装,失败抛异常也是在这一层,为了实现new),然后再调用自定义类型的构造函数。从汇编角度,如下:

new [n]是会调用operator new[]函数(其是对operator new的封装) 和 n 次构造函数。


delete释放空间也相似,只不过先调用析构函数,再释放空间。 至于为什么,参考如下情况:

代码语言:javascript
复制
class MyStack
{
public:
	MyStack()
		: _a((int*)malloc(sizeof(int) * 4))
		,_capacity(4)
		, _top(0)
	{}

	~MyStack()
	{
		free(_a);
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	MyStack* st = new MyStack;
	delete st;
	return 0;
}

若先调用operator delete_a指针变量所在的地址空间将被释放,无法找到malloc开辟的堆上空间!


再来观察如下现象,new A开辟的是4字节空间,但是new A[10]开辟的却是44字节空间,这是为什么呢?

new A调用一次operator new和一次构造函数;同理new A[10]调用十次operator new和十次构造函数,因为[]中传有开辟对象个数。那么delete调用一次析构和一次operator delete,但是delete[]可就不一样了,因为[]中没有传析构次数,所以编译器就不知道。那么为了让编译器知道次数,就在开辟的空间顶上多开辟4个字节来存放对象个数(X86环境,实测X64环境下多开辟8字节),只有这样delete[]才知道调用多少次析构函数。

当然也有很多情况不会在顶上多开辟空间:1. new内置类型,不需要析构;2. 没有显示写析构函数的自定义类型。(基于编译器的优化)


newdelete不匹配问题:

一个非常典型的问题(基于编译器的优化)就是:new多个自定类型时(A* p = new A[10]),且直接使用delete A,如果A类显示实现析构函数就会报错,如果不写析构函数就不会报错! 这与上面那个问题密切相关,即是否多开辟空间存对象个数。

如果显示实现了析构函数,p3并没有指向动态开辟内存的起始位置,且delete又不知道要向前偏移,所以直接释放了动态开辟的内存的中间位置,导致报错! 而不实现析构函数,就不会多开辟空间,也就避免了这样的问题。当然两者情况都可能会导致内存泄漏的问题!

所以newdelete一定要匹配使用,因为导致的结果可能是不确定的!


  1. new失败了以后抛异常,不需要手动检查,捕获异常方式:
代码语言:javascript
复制
try
{
	func(); // 其中调用new
}
catch(const std::exception& e)
{
	std::cout << e.what() << std::endl;
}

运用如上这些定理我们自己实现单链表也变得方便的多了!首先我们可以先创建一个类来描述单链表,然后单独实现创建链表的函数。

可以先创建一个哨兵位(MyList head(-1);栈上开辟,此节点为了方便后续链表节点的链接,且在创建单链表函数结束时自动销毁);然后通过cin输入链表节点值(val),并在堆上开辟链表节点(new MyList(val);,此时还会调用MyList类的构造函数);最后再链接各节点,并返回哨兵位后一个节点(head._next),即链表初始节点(哨兵位节点,栈上空间,出作用域自动销毁)。

代码语言:javascript
复制
//C++中List单链表的创建
struct MyList
{
	MyList(int val = 0)
		:_next(nullptr)
		,_val(val)
	{}

	MyList* _next;
	int _val;
};

MyList* CreatList(int n)
{
	MyList head(-1);//哨兵位  ---  出栈销毁

	MyList* tail = &head;
	int val;
	std::cout << "请以此输入" << n << "个节点的值:> " << std::endl;
	for (size_t i = 0; i < n; i++)
	{
		std::cin >> val;
		tail->_next = new MyList(val);  // 堆上开辟,链表实体; 且自动调用构造函数
		tail = tail->_next;
	}
	//返回哨兵位后面一个节点
	return head._next;
}
int main()
{
	MyListNode* head = CreatListNode(1);
	return 0;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而mallocfree不会。

三、operator new与operator delete函数

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

代码语言:javascript
复制
/*
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来释放空间的。

四、new和delete的实现原理

4.1 内置类型

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

4.2 自定义类型

new的原理

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

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间

new T[N]的原理

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

delete[]的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-07-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、C/C++内存分布
  • 二、C++内存管理方式
    • 2.1 new/delete操作内置类型
      • 2.2 new和delete操作自定义类型
      • 三、operator new与operator delete函数
      • 四、new和delete的实现原理
        • 4.1 内置类型
          • 4.2 自定义类型
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档