七、内存管理

Ⅰ、C语言内存管理回顾

01 内存分布位置

首先我们先观察下面一段代码,并回答问题:

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在哪里?____

 

答案:CCCAA  AAADAB 

栈区

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

执行函数时,函数内部局部变量的存储单元都可以在栈上创建。

函数执行结束后这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,

拥有很高的效率,但是分配的内存容量是有限的。

栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

堆区

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

一般由程序员自主分配释放,若程序员不主动释放,程序结束时可能由操作系统回收。

其分配方式类似于链表。

数据段

静态存储区,数据段存放全局变量和静态数据,程序结束后由系统释放。

代码段

可执行的代码 / 只读常量。代码段存放类成员函数和全局函数的二进制代码。

一个程序起来之后,会把它的空间进行划分,而划分是为了更好地管理。

函数调用,函数里可能会有很多变量,函数调用建立栈帧,栈帧里存形参、局部变量等等。

内存映射段

内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。

用户可使用系统接口创建共享共享内存,做进程间通信。

02 C语言中动态内存管理方式

malloc / calloc / realloc的区别?

malloc 

void* malloc(size_t size);

该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:

1、如果申请成功,则返回一个指向开辟好空间的指针。

2、如果申请失败,则返回 NULL 指针。

3、返回值类型为 void*,由使用者自己决定。

4、如果 size 为0,malloc的行为是标准未定义的,结果取决于编译器。

calloc

void* calloc(size_t num, size_t size);

该函数的功能是为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为0,返回指向它的指针。

与malloc相比,第一、calloc 有两个参数,分别为元素的个数和元素的大小。第二、calloc 会在返回地址前把申请的空间的每个字节初始化为0。

先看malloc:

int main()
{
	//malloc
	int* p = (int*)malloc(40);	//开辟40个空间
	if (p == NULL)
		return 1;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	free(p);
	p = NULL;
	return 0;
}

运行结果是10个随机值

再看calloc:

int main()
{
	//calloc
	int* p = (int*)calloc(10, sizeof(int));	//开辟10个,大小为int的空间
	if (p == NULL)
		return 1;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	free(p);
	p = NULL;
	return 0;
}

运行结果为:0 0 0 0 0 0 0 0 0 0

总结:说明calloc会对开辟的内存进行初始化,并将每个字节初始化为0 。如果对于申请的内存空降需要初始化,我们就可以使用calloc函数。

realloc

void* realloc(void* ptr, size_t size);

该函数用于重新调整之前调用 malloc 或 calloc 所分配的内存大小,可以对动态内存开辟的空间大小进行调整。具体情况如下:

1、ptr 指向要调整的内存地址。

2、size 为调整之后的新大小。

3、返回值为调整之后的内存起始位置,请求失败则返回空指针。

4、realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc 在调整空间时存在三种情况:

情况一:原有空间之后有足够大的空间

情况二:原有空间之后没有足够大的空间

情况三:realloc 有可能找不到合适的空间来调整大小

情况一: 当原有空间之后有足够大的空间时,直接在原有空间之后追加空间,原来的数组不发生变化。

情况二:当原有空间之后没有足够大的空间时,会在堆空间上另找一个大小合适的连续的空间来使用。函数的返回值将是一个新的内存地址。

情况三:如果找不到合适的空间,就会返回空指针。

Ⅱ、C++动态内存管理

01 C++ 兼容 C 的动态内存管理方式

c 语言的内存管理方式在 C++ 中同样适用,但有些地方使用起来并不是那么顺手。

为了解决这个问题,C++ 进化出属于自己的内存管理方式,通过使用 new 和 delete 进行动态内存管理。

02 使用 new 开辟空间

使用 new 开辟空间:

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

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

	//动态申请10个int类型的空间
	int* p3 = new int[10];
}

同时 new 不需要强制类型转换。

03 使用 delete 释放空间

使用 delete 释放空间:

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

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

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

	//单个对象,直接 delete
	delete p1;
	delete p2;

	//多个对象,使用 delete[]
	delete[] p3;

	//最好再全部置空
	p1 = nullptr;
	p2 = nullptr;
	p3 = nullptr;
}

04 初始化 new 数组

C++ 98 并不支持初始化 new 数组

int* p = new int[5];

但 C++11 允许大括号初始化,我们可以用 {} 列表初始化

int* p1 = new int[5]{1,2}         // 1 2 0 0 0
int* p2 = new int[5]{1,2,3,4,5};  // 1 2 3 4 5

05 new 和 delete 操作自定义类型

我们知道了,malloc / free 和 new / delete 对于内置类型来说没有本质区别,那么它存在的意义仅仅是用法更简洁,更方便使用嘛? 当然不是,我们接着往下看

malloc 和 new 的对比

对于自定义类型来说,你也是可以使用 malloc 的。

用 malloc 创建对象:
 

class A
{
public:
	A()
		:_a(0)
	{
		cout << "A():" << endl;
	}

	~A()
	{
		cout << "~A():" << endl;
	}
private:
	int _a;
};

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	A* a2 = (A*)malloc(sizeof(A) * 5);
}

现在来看看 C++ 的:

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	A* a2 = (A*)malloc(sizeof(A) * 5);

	A* a3 = new A;
	A* a4 = new A[5];
}

但仅仅只有书写更简洁嘛? 让我们调试观察一下:

new 这里不仅会发内存,还会调用对应的构造函数初始化,如果是一个数组,它会依次对创建的对象进行初始化。

free 和 delete 的对比

我们来对比一下 free 和 delete:

class A
{
public:
	A()
		:_a(0)
	{
		cout << "A():" << endl;
	}

	~A()
	{
		cout << "~A():" << endl;
	}
private:
	int _a;
};

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	A* a2 = (A*)malloc(sizeof(A) * 5);

	A* a3 = new A;
	A* a4 = new A[5];

	free (a1);
	free (a2);

	delete a3;
	delete[] a4;
}

相应的,free 只是把 p1 p2 指向的空间释放掉。

而 delete 不仅会把 p1 p2 指向的空间释放掉,还会调用相应的析构函数。

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

new:在堆上申请空间 + 调用构造函数输出

delete:调用指针类型的析构函数 + 释放空间给堆

06 new/delete 和malloc/free 要匹配使用

不建议大家混着用,有时编译器会报错

malloc / free          
delete / delete      
new[] / delete[] 

Ⅲ、new 和 delete 的底层

01 operator new 和 operator delete 函数

new 在底层调用 operator new来申请空间

delete 在底层调用 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 来申请空间

operator delete 最终也是通过调用 free 来释放空间

如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足的应对措施,

如果用户提供该措施就继续申请,否则就抛异常。

面向过程的语言处理错误方法:

int main()
{
    char* p1 = (char*)malloc(1024u * 1024u * 1024u *2u);
    if (p1 == nullptr)
    {
        printf("%d\n", errno);
        perror("malloc fail");
        exit(-1);
    } 
    else 
    {
        printf("%p\n", p1);
    }
 
    return 0;
}

面向对象的语言处理错误方法:

一般是抛异常----try catch

int main()
{
    char* p2 = nullptr;
    try 
    {
        char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
    } catch (const exception& e) {
        cout << e.what() << endl;
    }
    printf("%p\n", p2);
 
    return 0;
}

02 operator new 与 operator delete 的类专属重载

我们先看看C的方式写链表的结点的申请:

struct ListNode
{
	int _val;
	ListNode* _next;
	ListNode* _prev;
};

int main()
{
	struct ListNode* cur = (struct ListNode*)malloc(sizeof(struct ListNode));
	if (cur == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	cur->_next = NULL;
	cur->_prev = NULL;
	cur->_val = 0;

	return 0;
}

我们再来看看C++ 的方式:

struct ListNode
{
	int _val;
	ListNode* _next;
	ListNode* _prev;

	/*构造函数*/
	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}
};

int main()
{
	ListNode* cur = new ListNode(0);
	return 0;
}

在 C++ 里,因为 new 会自动调用构造函数去完成初始化,就很舒服。

而且还不需要去检查是否开辟失败,因为 new 失败不会返回空,而是抛异常。

再来看个熟悉的例子----Stack:

class Stack 
{
public:
	Stack(int capacity = 4)
		:_arr(new int[capacity])
		,_top(0)
		,_capacity(0)
	{}
	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _arr;
	int  _top;
	int  _capacity;
};

int main()
{
	Stack st;
	Stack* pst2 = new Stack; 
	delete pst2;  

	return 0;
}

Ⅳ、new 和 delete 的实现原理

01 对于内置类型

如果申请的是内置类型的空间,new 和 malloc ,free 和 delete 基本相似。

不同的是,new / delete 申请和释放的是单个元素的空间

new[] 和 delete[] 申请和释放的是连续空间,new 在申请空间失败时会抛异常

	A* a3 = new A;
	A* a4 = new A[5];

operator new 和 operator delete 就是对 malloc 和 free 的封装

operator new 调用 malloc 申请空间,失败后,改为抛异常

02 对于自定义类型

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 释放空间

Ⅴ、定位 new

01 定位 new 表达式

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

简单来说,定位 new 表达式可以在已有的空间进行初始化

分为带参和不带参

new(目标地址指针)类型                         // 不带参
new(目标地址指针)类型(该类型的初始化列表)       // 带参

02 定位 new 的使用场景

假如开的空间是从内存池来的,如果想初始化,我们就可以使用定位 new 

因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调用构造函数进行初始化。

03 定位 new 的使用演示

不带参:

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

	~A()
	{
		cout << "~A():" << endl;
	}
private:
	int _a;
};

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	new(a1)A;
	return 0;
}

带参:

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

	~A()
	{
		cout << "~A():" << endl;
	}
private:
	int _a;
};

int main()
{
	A* a2 = (A*)malloc(sizeof(A));
	new(a2)A(10);
	return 0;
}

模拟一下 new 的行为:

A* a3 = new A(3);
//等价于:
A* a4 = (A*)operator new(sizeof(A));
new(a4)A(3);

析构函数

析构函数可以显示调用(构造函数不可以)

int main()
{
	A* a1 = new A(3);
	delete a1;
	
	//等价于:
	A* a2 = (A*)operator new(sizeof(A));
	new(a2)A(3);

	a2->~A();
	operator delete (a2);

	return 0;
}

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值