【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

=========================================================================

相关代码gitee自取:

C语言学习日记: 加油努力 (gitee.com)

 =========================================================================

接上期:

【C++初阶】六、类和对象(初始化列表、static成员、友元、内部类)-CSDN博客

 =========================================================================

                     

目录

     一 . C/C++内存分布

C/C++中程序内存区域划分:


二 . C++内存管理方式

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

C++的内存管理方式

new / delete — 操作内置类型:

new / delete — 操作自定义类型:

常见面试题 — malloc / free 和 new / delete 的区别


三 . operator new 和 operator delete 函数

operator new / operator delete

operator new 全局函数:

operator delete 全局函数:

图示 — operator new / delete 全局函数:

new 和 delete 的实现原理

对于内置类型:

(重点)对于自定义类型:


四 . 定位new表达式(placement-new)(了解)


本篇博客相关代码

Test.cpp文件 — C++文件:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

一 . C/C++内存分布

                  

C/C++中程序内存区域划分:

                    

不同的数据有不同的存储需求,内存中有各种区域满足不同的需求

                  

  • 栈(堆栈):
    存放非静态局部变量 / 函数参数 / 返回值 ……,栈是向下增长的

                        

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

                         

  • 堆:
    用于程序运行时动态内存分配,堆是向上增长的

    (动态使用:数据结构、算法中需要动态开辟一些空间)

                          

  • 数据段(静态区):
    操作系统角度叫数据段,语言角度叫静态区。
    存储全局数据和静态数据

    (整个程序运行期间都可能会使用到的数据)

                   

  • 代码段(常量区):
    操作系统角度叫代码段,语言角度叫常量区。
    存储可执行代码(汇编指令)和常量

    (只读数据)

图示:

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

二 . C++内存管理方式

回顾:

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

                

之前学习C语言的时候有写过动态内存管理相关内容,
有需要的话可以进行查看:

学C的第三十二天【动态内存管理】_高高的胖子的博客-CSDN博客

                     

                     


                    

C++的内存管理方式

                   

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力了,

而且使用起来会比较麻烦,因此C++中又提出了自己的内存管理方式:

通过 new 和 delete 操作符进行动态内存管理

               

                

new / delete — 操作内置类型:

                  

  • new — 申请单个空间:
    内置类型指针 指针名 = new 内置类型;

                    

  • new — 申请多个空间:
    内置类型指针 指针名 = new 内置类型[申请单位空间个数];

                         

  • new — 申请单个空间并进行初始化:
    内置类型指针 指针名 = new 内置类型(初始化值);

                   

  • new — 申请多个空间并进行初始化:
    内置类型指针 指针名 = new 内置类型[申请单位空间个数]{第一个初始化值, 第二个初始化值……};

                           

  • delete — 释放new申请的空间:
    //释放new申请的单个空间:delete 内置类型指针名;//释放new申请的多个空间:delete[] 内置类型指针名;

                       

  • 对于内置类型的对象申请和释放,
    C++的 new / delete 和 C语言的 malloc / calloc / realloc / free
    除了用法上(“强转”和计算开辟空间大小)外,(底层)几乎没有任何区别
图示:

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

                          

                          

———————————————————————————————

                       ​​​​​​​
new / delete — 操作自定义类型:

                  

  • new — 申请单个空间:
    对于自定义类型,使用C++中的new开辟动态空间的话,
    会在开辟空间后顺便调用其构造函数进行自定义类型对象的初始化​​​​​​​
    //开辟单个空间并自动调用 默认构造函数 进行初始化:
    自定义类型指针 指针名 = new 自定义类型;
    //开辟单个空间并调用 有参构造函数 进行初始化:
    自定义类型指针 指针名 = new 自定义类型(初始化值);
    

                    

  • new — 申请多个空间:
    对于自定义类型,使用new申请多个空间时,
    同样会在开辟空间后顺便调用其构造函数进行自定义类型对象的初始化​​​​​​​
    //方式一:通过有名对象:(先初始化多个自定义类型对象);自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{有名对象1, 有名对象2……};//方式二:通过匿名对象:自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{匿名对象1, 匿名对象2……};//方式三:通过内置类型的隐式类型转换为自定义类型:自定义类型指针 指针名 = new 自定义类型[申请单位空间个数]{内置类型1, 内置类型2……};

                         

  • delete — 释放new申请的空间:
    //释放new申请的单个空间:delete 自定义类型指针名;//释放new申请的多个空间:delete[] 自定义类型指针名;

                       

  • 对于自定义类型的对象申请和释放,
    C++的 new 除了会开辟动态空间外,还会自动调用其构造函数进行初始化​​​​​​​​​​​​​​
图示:

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

                          

                          

———————————————————————————————

                       ​​​​​​​

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

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

三 . operator new 和 operator delete 函数

operator new / operator delete

              

new 和 delete 是C++中进行动态内存申请和释放的操作符,

operator new 和 operator delete 是系统提供的全局函数,

new 在底层会调用 operator new 全局函数来申请空间;

delete 在底层会调用 operator delete 全局函数来释放空间。

              

              

operator new 全局函数:

                  

  • 虽然函数名中有 operator ,但并不是重载函数

    ​​​​​​​           

  • C语言中,malloc 如果申请空间失败的话,会返回空指针,
    这不符合C++面向对象编程的要求,所以需要对其进行封装

                 

  • operator new 全局函数就是对 malloc 的封装,
    所以 operator new 全局函数底层会调用 malloc ,
    让 malloc 申请空间失败后会抛出异常,从而能够符合C++面向对象编程的要求,
    operator new 全局函数和 malloc 一样只会申请空间不会调用构造函数初始化

                          

                          

———————————————————————————————

                       ​​​​​​​

operator delete 全局函数:

                

  • operator delete 全局函数同样也不是重载函数,而是一个全局函数

                   

  • operator delete 全局函数是对 free 的封装,
    所以 operator delete 全局函数底层会调用 free ,
    相较 free ,operator delete 全局函数多了一些检查,
    operator delete 全局函数和 free 一样只会释放空间不会调用析构函数

                          

                          

———————————————————————————————

                       ​​​​​​​

图示 — operator new / delete 全局函数​​​​​​​:

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

                     

                     


                    

new 和 delete 的实现原理

            

对于内置类型:

               

如果申请的是内置类型对象的空间,new 和 malloc,delete 和 free 基本类似,
不同的地方是:

new / delete 申请和释放的是单个元素的空间;new[ ] / delete[ ] 操作的则是连续的空间,

而且 new 在申请空间失败时会抛出异常,而C语言中malloc则会返回空指针

                        

                          

———————————————————————————————

                       ​​​​​​​

(重点)对于自定义类型:

                

  • new 的原理(申请单个动态空间):

                            
    第一步 —  为自定义类型对象开辟动态空间 — 调用 operator new 全局函数

    (new  =>  operator new  =>  malloc)

                    
    第二步 —  初始化申请的空间 — 调用 构造函数 完成对象的初始化

                        

                    

  • delete 的原理(释放单个动态空间):

                         
    第一步 —  先清理自定义类型对象申请的资源 — 调用对应的 析构函数

                           
    第二步 —  再释放自定义类型对象的动态空间 — 调用 operator delete 全局函数

    (delete  =>  operator delete  =>  free)

                   

                    

  • new T[N] 的原理(申请多个动态空间):

                      
    第一步 —  调用 operator new[ ] 函数开辟动态空间,
    在 operator new[ ] 中实际也是调用了 operator new 全局函数,
    一次性完成了N个对象空间的申请

                   
    第二步 —  在申请的空间上执行N次构造函数,完成N个对象的初始化

                    

                   

  • delete[ ] 的原理(释放多个动态空间):

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

                       
    第二步 —  调用 operator delete[ ] 释放空间,
    ​​​​​​​在 operator delete[ ] 中实际也是调用了 operator delete 全局函数

图示:

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

四 . 定位new表达式(placement-new)(了解)

                         

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

    (通过对象指针能够显式调用构造函数进行初始化)

                        

  • 使用格式:
    调用默认构造函数 — new (place_address) type
    调用有参构造函数 — new (place_address) type (initializer-list)
    place_address:必须是一个指针 ;initializer-list:类型的初始化列表

                            

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

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             

本篇博客相关代码

Test.cpp文件 — C++文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include 
#include 
using namespace std;

全局变量(链接属性:其它文件也可用):
//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";
//	/*
//	* 这里没有使用数组符号[]进行拷贝,
//	* 所以指针是直接指向常量区中“abcd”的位置的
//	*/
//
//	//malloc开辟动态空间:
//	int* ptr1 = (int*)malloc(sizeof(int) * 4);
//	//calloc开辟动态空间:
//	int* ptr2 = (int*)calloc(4, sizeof(int));
//	//realloc开辟动态空间:
//	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
//}



A类:
//class A
//{
//public: //公有成员函数:
//
//	//构造函数(全缺省):
//	A(int a = 0)
//		: _a(a)
//	{
//		//调用则打印:
//		cout << "A():" << this << endl;
//	}
//
//	//析构函数:
//	~A()
//	{
//		//调用则打印:
//		cout << "~A():" << this < 构造函数 -> 产生临时对象
//	* 2、临时对象 -> 拷贝构造函数 -> (A类)匿名对象 
//		 -> 找到A类中的构造函数
//	*/
//
//	//释放自定义类型对象空间:
//	delete p6;
//	delete[] p10;
//	/*
//	* delete对于自定义类型:
//	* 先调用析构函数销毁对象清理资源,
//	* 再调用释放动态空间,
//	*/
//
//	return 0;
//}



//int main()
//{
//	try
//	{
//		char* p1 = new char[0x7fffffff];
//		/*
//		* 十六进制:0x7fffffff -- 接近2G
//		*
//		* 当new开辟的空间过大时可能会开辟失败,
//		* 开辟失败则会抛出异常
//		*(C语言开辟失败会返回空指针)
//		*/
//		
//		cout << (void*)p1 << endl;
//		/*
//		* char* 在被cout识别时后先被识别为char,
//		* 而不是我们想要打印的指针(地址),
//		* 所以要强转为void*类型
//		*/
//	}
//	catch (const exception& e)
//	{
//		//try……catch……捕获异常:
//		cout << e.what() << endl;
//	}
//
//
//	return 0;
//}


栈类:
//class Stack
//{
//public: //公有成员函数:
//
//	//构造函数:
//	Stack(int capacity = 4)
//	{
//		//调用了构造函数则打印:
//		cout << "Stack(int capacity = 4)" << endl;
//
//		//使用new开辟栈容量大小的空间:
//		_a = new int[capacity];
//
//		_top = 0; //栈顶值默认为0
//		_capacity = capacity; //设置栈容量
//	}
//
//	//析构函数:
//	~Stack()
//	{
//		//调用了析构函数则打印:
//		cout << "~Stack()" < 封装malloc/free -> 处理失败抛异常问题)
//	*/
//
//	//上面都是new开辟单个空间,那如果开辟多个空间呢:
//	Stack* p3 = new Stack[10]; //开辟多个空间
//	/*
//	* new 实现的两步: 1、开辟对象空间  2、调用构造函数初始化
//	* 
//	*				1、开辟对象空间
//	* new 开辟单个空间和开辟多个空间同样都会调用operator new,
//	* 开辟多个空间实际只会调用一次operator new,
//	* 一次性就开辟多个连续的空间(这里是10个连续的空间)
//	*(operator new[] -> operator new -> malloc)
//	* 
//	*			 2、调用构造函数初始化
//	* 调用10次Stack构造函数进行初始化
//	*/
//
//	delete[] p3; //释放new开辟的连续空间
//	/*
//	* 1、先调用10次析构函数;
//	* 2、释放空间:
//	* operator delete[] -> operator delete -> free
//	* 
//	*				补充:
//	* 前面我们用new开辟了10个连续的空间,
//	* 按理来说应该是120个字节(这里一个栈对象12个字节),
//	* 但实际开辟了124个字节,多的4个字节存储着开辟的空间个数,
//	* 这里存储就是10,这样第一步中就可以知道要调多少次析构函数,
//	* 第二步中也可以知道释放时要释放多少个连续的空间,
//	* 所以我们使用delete释放连续空间时“delete[]"中的[],
//	* 我们不需要显式写出要释放多少个连续空间,
//	* 因为在用new开辟连续空间的时候就已经存储好了该值
//	*(对于构造函数中申请了资源的自定义类型来说)
//	* 
//	* 所以 new 要和 delete 配对使用,
//	*    new[] 要和 delete[] 配对使用,
//	*   malloc 要和 free 配对使用
//	*/
//
//	return 0;
//}



//A类:
class A
{
public: //公有成员函数:

	//构造函数(全缺省):
	A(int a = 0)
		: _a(a)
	{
		//调用则打印:
		cout << "A():" << this << endl;
	}

	//析构函数:
	~A()
	{
		//调用则打印:
		cout << "~A():" << this <A(1); 

	//不能像437行那样显式调用构造函数,
	//但可以通过 定位new 显式调用构造函数:
	new(p1)A(1);
	/*
	* 定位new是在已分配的原始内存空间中调用构造函数
	* 来初始化一个对象
	* 
	*				格式:
	* 默认构造:new(对象指针)对象类名
	* 有参构造:new(对象指针)对象类名(初始化值)
	*/

	//虽然构造函数不能显式调用,但析构函数是可以的:
	p1->~A();
	//析构函数可以显式调用也可以自动调用
	
	//释放空间:
	operator delete(p1);

	/*
	*			某种程度上来说:
	* 
	*	A* p1 = (A*)operator new(sizeof(A));
	*				+
	*		  new(p1)A(1);
	* 
	* operator new开辟空间配合定位new,可以实现new的功能
	*(operator new开辟空间,定位new再显式调用构造函数初始化)
	*/

	/*
	*			某种程度上来说:
	* 
	*			p1->~A();
	*				+
	*		operator delete(p1);
	* 
	* p1->~A();显式调用析构函数配合operator delete释放空间,
	* 可以实现delete的功能
	*/

	/*
	* 虽然可以模拟实现new和delete,
	* 但一般也不会这么操作
	*  
	* 因为new有两步操作,
	* 1、operator new -> malloc(去堆中申请空间)
	* 2、调用 构造函数
	* 所以如果频繁使用new申请小对象的话,一直去找堆的话,
	* 效率可能会比较低。
	* 
	* 这时就需要使用到 内存池,
	* 把堆的内存较大地申请到内存池中,
	* 这时当需要申请内存时就不到堆中申请了,
	* 而是到内存池中申请,不够了再到堆中申请,
	* 这时就不用一直到堆中申请,提高效率
	* 内存池只开了空间,没有初始化,也不能初始化,
	* 因为数据可能会是私有的(池化技术)
	* 
	* 假设我们有一个内存池,在内存池中申请对象空间,
	* 这时的初始化工作就可以交给 定位new ,
	* 通过 定位new 显式调用构造函数来初始化对象,
	* 这时要释放空间还给内存池的话,
	* 就需要显式调用析构函数来释放空间
	*/

	return 0;
}


//C++常见面试题:指针和引用的区别、malloc和new的区别

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/235289de1a.html