【C++从入门到踹门】第五篇:new与delete


在这里插入图片描述


1. C/C++的内存分布

  • 栈区(stack):由编译器自动分配和释放,存放为运行时函数分配的非静态局部变量、函数形参,返回值等,其操作类似于数据结构中的栈(先进后出)。栈是向下生长的。
  • 堆区(heap):由用户自行申请的空间,且用完后需要用户自行释放。程序结束后可能由操作系统回收。堆是向上生长的。
  • 数据段(静态区):存放全局变量,静态变量。一旦静态区的内存被分配,直到程序终止时才会被释放。
  • 常量区:存放常量(程序在运行期间不能被修改的量,比如字面值10,字符串常量“abcd”,数组的名字等)。
  • 代码段:存放函数体等程序编译的机器指令。代码区是只读的防止程序意外的修改指令。

下面我们将根据代码来分别认识变量存在于哪些区:
在这里插入图片描述


2. C++的内存管理方式

C语言在堆上申请空间分别有三个函数malloccalloc(初始化为0),realloc(扩容——原地扩容,异地扩容),空间申请失败返回NULL。释放空间为函数free

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QagpzuOi-1645325209084)(image/【C++从入门到踹门】第五篇:内存管理/1640093986433.png)]

malloc

在这里插入图片描述

calloc

在这里插入图片描述

realloc

在C++中我们可以继续使用C语言的这些函数在堆上申请空间,但是C++有自己的操作符进行动态内存管理——newdelete

2.1 new和delete操作内置类型

new是一个操作符

int main()
{
	//申请int对象空间及初始化
	int* ptr1 = new int;
	int* ptr2 = new int(3);
	
	//申请int数组空间及初始化
	int* ptr3 = new int[3];
	int* ptr4 = new int[3]{ 1,2,3 };//<c++11>

	//删除单个int对象空间
	delete ptr1;
	delete ptr2;

	//删除数组空间
	delete[] ptr3;
	delete[] ptr4;

	return 0;
}

申请和释放单个元素的空间,使用newdelete操作符,申请和释放连续空间,使用new[]delete[]

针对于内置类型,new/delete和malloc/free是一样的,区别在于处理自定义类型。

2.2 new和delete操作自定义类型

我们自定义一个类,然后分别使用new/delete 和 malloc/free 来构造和销毁对象,查看其中的区别。

struct ListNode
{
	//struct ListNode* next;

	//c++实例化struct类型对象,可以省略关键字struct
	ListNode* _next;
	ListNode* _prev;
	int _val;

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

	}
	//析构函数
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{

	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	free(n1);

	ListNode* n2 = new ListNode(5);
	delete n2;


	return 0;
}

我们对其进行调试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BL6aY2VH-1645325209085)(image/【C++从入门到踹门】第五篇:内存管理/1640096895714.png)]

我们发现在面对自定义类型时,new在堆上申请空间后会调用构造函数初始化,delete会调用析构函数然后再释放空间,但是malloc和free不会调用构造和析构函数。于是我们可以利用new调用构造函数的特性,来初始化对象

//注意:如果没有默认构造函数,那new对象时,就一定要手动初始化了。

new/delete ,new[] / delete[] , malloc/free 需匹配使用,不可混用。即使是new [1] 也需要delete[]来释放空间。

new之所以被设计出来还有一点是因为:
malloc若是申请失败,将返回错误码。但是C++要求出错是需要抛异常,于是这一任务也交到了new的身上。

在这里插入图片描述

malloc没有抛异常

在这里插入图片描述

总结一下

C++提出new和delete是解决两个问题:

  1. 自定义类型对象动态申请时,初始化和清理的问题。new/delete可以构造/析构函数
  2. new失败了以后要求抛异常,这样才符合面向对象语言的出错处理机制。

ps:free/delete一般不会失败,除非释放空间出现了越界或释放的指针位置不对。


4.operator new 和operator delete函数

4.1 newoperator new

  • new是C++中的操作符,用法是A* a=new A

  • operator new是对new操作符的重载函数,并非运算符。

    operator new分为

    1. 全局重载void* ::operator new(size_t size )(并非在std命名空间中)

    2. 类重载void* A::operator new(size_t size)

newoperator new又存在什么联系呢?正如下面代码:

A* a=new A;

这里分为三步:1.申请内存空间 2.调用A()构造函数 3.返回指针。

使用new时系统会隐式调用operator new函数

在这里插入图片描述

事实上申请内存空间是operator new(size_t)来完成的,如果你对类A重载了operator new,那么将调用A::operator new(size_t),否则将调用全局的::operator new(size_t)后者由C++默认提供。于是真正的步骤将分为:

  1. 调用operator new(sizeof(A))
  2. 调用构造函数A()
  3. 返回指向适当类型的指针

在底层中operator new 该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了(重载),则继续申请,否则抛异常。

4.2 operator new的三种重载形式

throwing (1)	
void* operator new (std::size_t size) throw (std::bad_alloc);
nothrow (2)	
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
placement (3)	
void* operator new (std::size_t size, void* ptr) throw();
  • (1) 分配size个字节大小的空间,返回此块空间第一个字节的void型指针。失败时会抛出bad_alloc异常。
  • (2)在分配空间上于(1)号一致,除了在失败时它返回一个空指针,而不是抛出异常。

(1)(2)的区别在于是否抛出异常,当分配失败时,前者会抛出bad_alloc异常,后者返回NULL不会抛异常。他们都分配一个固定大小的连续空间,但不会进行初始化

A* a1=operator new(sizeof(A)) ;//调用throwing(1)
A* a2=operator new(sizeof(A),nothrow);//调用nothrow(2)

注意以上两个函数只是在堆上申请空间,并不会初始化。

(3)是placement new ,该函数不分配空间,他接受一个指针参数ptr,然后正确执行初始化(如果指针指向自定义类型,将会调用其构造函数对其初始化),随后返回指针ptr,他的调用形式:

A* p=operator new(sizeof(A));
new(p) A();//也可以调用A(5)等带参数的构造函数

关于placement new的更多使用方法将在第6节详述。

使用new实例

#include <iostream>
using  std::cout;
using  std::endl;

struct MyClass
{
	int data[100];
	MyClass()
	{
		cout << "constructed[" << this << "]\n";
	}
};

int main()
{
	//(1)
	MyClass* p1 = new MyClass;
	//将调用:operator new(sizeof(MyClass)) 分配空间,
	//随后调用构造函数初始化

	//(2)
	MyClass* p2 = new(std::nothrow) MyClass;
	//将调用:opertor new(sizeof(MyClass),std::nothrow)分配空间,
	//随后调用构造函数初始化

	//(3)
	new(p2) MyClass;
	//不分配空间,将调用:operator new(sizeof(MyClass),p2) 在p2指向的位置构造对象

	MyClass* p3 = (MyClass*) ::operator new(sizeof(MyClass));
	//用:operator new(sizeof(MyClass)) 分配空间,不会调用构造函数初始化

	delete p1;
	delete p2;
	delete p3;

	return 0;
}

在这里插入图片描述

4.3 operator delete

operator delete底层也是由free实现的。

operator delete 是一个常规函数,可以像任何其他函数一样显式调用。 但在 C++ 中, delete是一个具有非常特殊行为的运算符:带有 delete 运算符的表达式,首先调用适当的析构函数(对于类类型),然后调用释放函数。
类对象的释放函数是一个名为 operator delete 的成员函数(如果存在)。 在所有其他情况下,它是一个全局函数操作符 delete(即这个函数——或更具体的重载)。 如果删除表达式前面有作用域运算符(即 ::operator delete),则只考虑全局释放函数。


5.new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

  • new
  1. 调用operator new申请空间
  2. 在申请的空间上调用构造函数,完成在堆上的对象的建立。
  3. 下面是new的等价代码:
//operator new
A* p1 = (A*)operator new(sizeof(A));

//对p1地址的空间调用构造函数初始化
new(p1)A(1);

//等价于直接用 new A

在这里插入图片描述

  • delete
  1. 首先执行析构函数,完成对象中资源的清理工作。。
  2. 调用operator delete函数释放对象的空间
  3. 下面是delete的等价代码:
//以下等价于delete p1;   ——   析构+operator delete(free)
p1->~A();//析构函数的显式调用
operator delete(p1);

在这里插入图片描述


6 operator new /operator delete的类专属重载 与 定位new(placement-new)

6.1 operator new /operator delete的类专属重载

通过上文,我们了解到new将内存分配和对象构造组合在了一起。这也造成了new有一些灵活性上的局限。
类似的,delete将对象析构和内存释放组合在了一起。

我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。但是当我们频繁的申请内存空间时,不仅大幅增加了系统调用的次数,还会产生大量的内存碎片。

于是就有了内存池技术。

内存池(memory pool)是在当前系统中请求一大片连续的内存空间,然后在运行时根据实际需要分配出去的技术。

当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象的创建操作(同时付出一定开销)。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。

关于内存池我会在未来的文章中提到,当前先按下不表。

这里我们写了链表的类,链表是一种会频繁向内存申请和释放空间的数据结构,我们通过重载类专属的operator newoperator delete,实现链表结点使用内存池申请和释放内存:

struct ListNode
{
	ListNode* _pre;
	ListNode* _next;
	int _data;

	ListNode(int val=0) :_data(val), _next(nullptr), _pre(nullptr)
	{

	}

	//定制ListNode专属的operator new/operator delete
	void* operator new(size_t n)
	{
		void* p = nullptr;
		p = allocator<ListNode>().allocate(1);//STL中的内存池--空间配置器
		cout << "memory pool allocate" << endl;
		return p;
	}

	void operator delete(void* p)
	{
		allocator<ListNode>().deallocate((ListNode*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
};

class List
{
public:
	List()
	{
		_head = new ListNode(-1);
		_head->_next = _head;
		_head->_pre = _head;
	}

	void PushBack(int val)
	{
		ListNode* newnode = new ListNode(val);//先调用类专属operator new,随后调用构造函数
		ListNode* tail = _head->_pre;
		tail->_next = newnode;
		_head->_pre = newnode;
		newnode->_pre = tail;
		newnode->_next = _head;

	}

	~List()
	{
		ListNode* cur = _head->_next;
		while (cur!=_head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}
		delete _head;
		_head = nullptr;
	}
private:
	ListNode* _head;
};

int main()
{
	List l;
	l.PushBack(1);
	l.PushBack(2);
	l.PushBack(3);
	l.PushBack(4);
	return 0;
}

定制了类专属的operator new函数,调用new时就可以在该函数中向内存池请求空间。

在这里插入图片描述

假如没有定制专属的类重载new函数,那就默认用全局的重载new函数,此时内置的malloc会向系统请求空间而非内存池。

6.2 定位new(placement-new)

之前谈到placement-new不会申请空间,而是负责初始化,当这块空间是malloc/ 内存池/operator new 申请来的,那我们可以使用定位new在已分配的内存空间中调用构造函数初始化。

  • 定位new的使用方法:

    1. new(place address) type
    2. new(place address) type(initializer-list)

    place address 是指针,initializer-list 是类型初始化列表

举个例子

面对一个已经申请的空间的类,当成员变量为私有时便不易初始化(要么自己写一个初始化函数)。这时利用定位new便可以对其初始化。

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1) :_year(year), _month(month), _day(day)
	{
		cout << "Date:" << this << endl;
	}
	~Date()
	{
		cout << "~Date:" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//malloc场景
	Date* d1 = (Date*)malloc(sizeof(Date));
	//d1->Date();//Date()是匿名对象,无法视为构造函数调用
	new(d1) Date;//定位new 
	//new(d1) Date(2022,2,20);//带参的初始化
	d1->~Date();//可以显示调用析构函数
	free(d1);

	//operator new 场景
	Date* d2=new Date(2022,2,21);
	delete d2;
	//等价于
	Date* d3 =(Date*)operator new(sizeof(Date));//开辟空间 
	new(d3) Date(2022,2,21);//+初始化
	d3->~Date();//调用析构
	operator delete(d3);

	return 0;
}
  • 注意

    不能显示调用构造函数初始化,因为在调用构造函数时,Date()实际上是创建了一个Date类的匿名对象,d1->Date()会报错,Date()此刻为另一个对象,而非类内成员!

    构造函数并不像普通函数那样进行一段处理,而是创建了一个对象,并且对该对象赋初值,所以显式调用构造函数无法实现给私有成员赋值的目的。

  • 定位new使用场景

    单纯向系统申请空间时使用operator new+定位new和使用new没有区别。

    但当内存池的一大块空间从系统内存申请来时不可能直接初始化,等到真正使用时,可以使用定位new来初始化。


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

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在释放空间前会调用析构函数完成空间中资源的清理。

8.内存泄露

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

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;
}

青山不改 绿水长流
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值