< C++ >:C/C++ 内存管理

目录

1、C/C++ 内存分布

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

2.1、malloc/calloc/realloc 和 free 库(C/C++标准库)函数

3、C++ 中动态内存管理方式

3.1、new/delete 操作符操作内置类型

3.2、new 和 delete 操作符操作自定义类型

4、operator new 与 operator delete 全局库函数

4.1、operator new 与 operator delete 全局库函数

4.2、专属的非静态类成员函数 operator new 与 operator delete

5、操作符 new 和 delete 的底层实现原理

5.1、内置类型

5.2、自定义类型

6、定位 new 表达式 (placement-new)

7、常见面试题

7.1、库函数 malloc 和 free 与操作符 new 和 delete 的区别

7.2、内存泄漏

7.2.1、内存泄漏的分类

7.2.2、什么是内存泄漏?内存泄漏的危害?

7.2.3、如何检测内存泄漏?

7.2.4、如何避免内存泄漏?

7.3、如何一次在堆区上动态申请 4G 的内存空间?


        在 C++ 和 C 语言中,都对内存进行了划分,其实本质上是操作系统进行划分的,并不是 C++ 和 C 语言进行划分的、

1、C/C++ 内存分布

说明:

1、栈(栈区)又叫堆栈,非静态局部变量/函数参数/返回值(返回数据,返回地址等)等等,栈是向下增长的,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的栈内存容量有限、
2、内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库,用户可使用操作系统接口创建共享共享内存,做进程间通信,Linux 课程如果没学到这块,现在只需要了解一下、
3、堆(堆区)用于程序运行时动态内存分配,堆是向上增长的,一般由程序员动态分配和释放,若程序员不动态释放,程序结束时可能由 OS 回收,分配方式类似于链表、
4、数据段(静态区):存储全局数据和静态数据(静态局部数据和静态全局数据),此处的全局数据和静态数据(静态局部数据和静态全局数据),其生命周期都是整个工程,其中:全局数据和全局静态数据的作用域是整个工程,但局部静态数据的作用域是其当前所在的 { } 中,静态区中存放的所有数据的生命周期都是整个工程;程序结束后由操作系统自动释放、
5、代码段(常量区或者正文):存放函数的二进制代码,存储可执行的代码(本质上是二进制指令)或者是只读常量、

1、对于栈区来讲,是由编译器自动管理的,无需我们手工控制;对于堆区来说,动态释放工作由程序员控制;栈区主要存放非静态局部变量、返回值和函数参数等等,其空间的管理由编译器自动完成,无需手动控制;堆区是自己动态申请的内存空间,在不需要时需要我们自己手动动态释放、

2、对于栈区来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆区来讲,它的生长方向是向上的,是向着内存地址增加的方向增长;栈区中先声明和定义的变量放到栈底(数据结构),地址高,后声明和定义的变量放到栈顶(数据结构),地址低,因此是向下生长的,堆区则相反、

3、对于堆区来讲,频繁的 new/delete 等势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低;对于栈区来讲,则不会存在频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏的问题,因为栈区由于是自动管理的,不存在此问题、

4、32 位系统下,最大的访问内存空间为 4G,所以不可能把所有的内存空间都当做堆内存使用、         

5、堆区大小受限于操作系统,而栈区空间一般由操作系统直接分配、

6、堆区无法静态分配内存空间,只能动态分配内存空间;栈区可以通过库(C/C++标准库)函数 _alloca 进行动态分配内存空间,不过注意:所动态分配的内存空间不能通过 free 库(C/C++标准库)函数或 delete 操作符进行释放、
7、

ClassA* pclassa=new ClassA[5];

delete pclassa;

pclassa = nullptr;

//在c++语言中,自定义的类ClassA中的构造函数和析构函数的自动执行次数分别为(5,1)、
//此时会自动调用自定义的类的类体中的构造函数5次,delete由于没有使用[],此时只会自动调用自定义的
//类的类体中的析构函数1次,程序可能崩溃,也可能不会崩溃,但往往会引发程序崩溃,这是对于自定义类型而
//言的;若对于内置类型而言,则不会崩溃,也不会造成内存泄漏;由于是自定义类型,则此时就会造成内存泄漏
//,这是因为没有完全析构导致的,其次还和底层实现原理中的偏移指针有关,具体就不再进行研究了,所以需
//要使用[],使其完全析构,即:使得自定义的类ClassA中的析构函数自动执行次数5次,即最好要对应上、

8、new 是操作符,是保留字,不需要头文件支持;malloc 是库(C/C++标准库)函数,不是保留字,需要头文件支持,一般需要头文件 malloc.h,只是平时这个头文件已经被其他头文件所包含了,比如 stdlib.h,用的时候很少单独引入头文件 malloc.h,而是引入头文件 stdlib.h、

注意:        

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = {1, 2, 3, 4}; //int整型数组num1存放在栈区,即局部int整型数组、
	char char2[] = "abcd"; //字符数组char2也存放在栈区,即局部字符数组、
	//字符数组char2开辟5个内存空间,把常量字符串中的数据,包括字符'\0',都拷贝过来对该字符数组char2进行初始化、
	const char* pChar3 = "abcd";
	//pChar3是一个字符指针变量,该变量中存储的是常量字符串中首元素的地址,现在进行解引用操作,即:
// *pChar3,就找到了常量字符串中的首元素,也即找到了该常量字符串,而常量字符串又存在于代码段(常
//量区),故,*pChar3存在于代码段(常量区)中、
	int* ptr1 = (int*)malloc(sizeof (int)* 4);
	//ptr1是一个int整形指针变量,该变量中存放的就是在堆区上动态malloc开辟出来的内存空间的首地址
//,现在*ptr1代表的就是存放在堆区上的这一块内存空间、
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int)* 4);
	free(ptr1);
    ptr1 = nullptr;
	free(ptr3);
    ptr3 = nullptr;
}
//*char2存放在栈区上,因为char2是字符数组的数组名,代表字符数组首元素的地址,此时,*char2找到的就
//是字符数粗char2中的的第一个元素,也是存放在栈区上的,此时整个字符数组char2都存放在栈区上,则该
//数组中的所有元素也都存放在栈区上、

注意:

int main()
{
	const char* p = "hello";
	cout << p << endl; //hello
	cout << (char*)p << endl; //hello
	//此时的打印会自动识别类型,又因字符指针变量p是char*类型的,故会自动打印字符串,此时为了输出
//地址,可以将字符指针变量p强转为非char*的指针类型,如下所示:
	cout << (void*)p << endl; 
	//除此之外,若想要打印地址,也可使用printf库函数按照%p的格式进行打印,如下所示:
	printf("%p\n", p);
	return 0;
}
//空指针是第一个地址(0x00000000)的编号,char类型的字符在二进制的角度上存储的是该字符的ASCII码值
//,任何类型的一个指针变量中存储的都是一个字节(首字节)的地址、

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

2.1、malloc/calloc/realloc 和 free 库(C/C++标准库)函数

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<stdlib.h>
using std::cout;
using std::cin;
using std::endl;
void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	assert(p1);
	free(p1);
	p1 = nullptr;

	int* p2 = (int*)calloc(4, sizeof(int));
	cout << p2 << endl;
	assert(p2);
	//free(p2);
	//p2=nullptr;

    //当上述两条语句均写出或均不写出时,都可以成功运行且没有任何问题,但是两者代表的意义不同;
//当只写第二条不写第一条时,也可以成功运行,但是会造成内存泄漏;当只写第一条不写第二条时,则不可
//成功运行,因为此时int整形指针变量p2指向的内存空间已经被释放掉了,下面如果再在该被释放掉的内
//存空间的后面进行扩容就会出现错误、

	int* p3 = (int*)realloc(p2, sizeof(int));
	printf("%p\n", p3);
	assert(p3);
	free(p3);
	p3 = nullptr;
}
int main()
{
	Test();
	return 0;
}

问:malloc/calloc/realloc 库函数的区别?

答:具体见博客:动态内存管理(1)_脱缰的野驴、的博客-CSDN博客


3、C++ 中动态内存管理方式

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

3.1、new/delete 操作符操作内置类型

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;

//此时在堆区上动态开辟了一个int类型大小的内存空间,相当于在堆区上声明和定义了一个int类型的
//变量、
//int* ptr8 = new int;
//此时,在堆区上动态开辟了一个int类型大小的内存空间,该内存空间存在于堆区上,若不对该在堆区上声
//明和定义的一个int类型的变量进行赋值(初始化)的话,则默认是随机值(垃圾值),即如果不对在堆区上
//动态开辟的一个int类型大小的内存空间进行赋值(初始化)的话,则该内存空间中所存储的数据(即为前
//面所说的int类型的变量)默认就是随机值(垃圾值),除此之外,此时的int整形指针变量ptr8是一个全局
//int整形指针变量,该变量已经进行了赋值(初始化),所以不会被默认为空指针、

void Test()
{

	//new操作符的使用:

	//在堆区上动态申请一个int类型大小的内存空间但不进行赋值(初始化);不需要强制类型转换、
	int* ptr1 = new int; 
	//在堆区上动态开辟了一个int类型大小的内存空间,若不进行赋值(初始化)的话,那么该内存空间中
//所存储的数据就默认为随机值,也就是垃圾值、

	//在堆区上动态申请一个int类型大小的内存空间并且进行赋值(初始化)为10;不需要强制类型转换、
	int* ptr2 = new int(10);

	//在堆区上动态申请10个int类型大小的内存空间但不进行赋值(初始化);不需要强制类型转换、
	int* ptr3 = new int[10];

	//若想在堆区上动态申请10个int类型大小的内存空间,并且都进行赋值(初始化)为10,能不能使用下
//面的方法呢?
	//int* ptr4 = new int[10](10);
	//这种用法是错误的,当在堆区上动态申请多个int类型大小的内存空间时,赋值(初始化)时只能用{},这
//是C++11中才支持的语法(列表初始化的特性),注意:在C++98中,对于这种在堆区上动态申请多个int类型
//大小的内存空间时,没有赋值(初始化)的方式;其他类型也是如此,此处只是以int类型举例、
	
    //如下所示:								
	int* ptr5 = new int[10]{10};
    //但要注意,这种写法只能对在堆区上动态申请的10个int类型大小的内存空间中的第一个int类型大
//小的内存空间进行赋值(初始化)为10,剩下的9个动态申请的int类型大小的内存空间都赋值(初始化)为
//了0、

	//int* ptr6 = new int[2](1,2);
    //这也是错误的,当在堆区上动态申请多个(>=2)int类型大小的内存空间时,赋值(初始化)时只能用{}
//,如下所示:
	int* ptr7 = new int[2]{1,2};


	//delete操作符的使用:
	//1、
	//int整形指针变量ptr1和ptr2指向的在堆区上动态开辟的内存空间都只有1个int类型的大小,故在
//释放时,只需要使用delete加int整型指针变量即可,具体如下所示:
	delete ptr1;
    ptr1 = nullptr;
	delete ptr2;
    ptr2 = nullptr;

	//2、
	//int整形指针变量ptr3,ptr5和ptr7指向的在堆区上动态开辟的内存空间都有多个int类型的大小,
//故在释放时,需要使用delete[]加int整型指针变量;即只要是在堆区上动态开辟了多个内存空间,则在释
//放时,都必须要在操作符delete的后面加上[],而[]中都不需要加上确定的数字,具体如下所示:
	delete[] ptr3;
    ptr3 = nullptr;

	delete[] ptr5;
    ptr5 = nullptr;

    delete[] ptr7;
    ptr7 = nullptr;

	//delete ptr8;
    //ptr8 = nullptr;

	//上述两种情况下对应的方法最好要匹配,由于是内置类型,即使不匹配程序也不会崩溃,但是最好匹
//配上,由于是内置类型,所以即使不匹配也不会造成内存泄漏、
	//最好要匹配起来,不匹配(比如:delete ptr3;)不一定只会对在堆区上动态开辟的多个内存空间中
//的第一个内存空间进行释放,再如:delete[2] ptr3;不明确会对在堆区上动态开辟的多个内存空间中的
//哪两个内存空间进行释放,具体在后面进行阐述、

}


//在C++中,建议使用new操作符,而不建议使用malloc库函数、

//1、
//使用malloc库函数操作内置类型,即在堆区上动态开辟一个或多个内置类型大小的内存空间,相当于在堆
//区上声明和定义了一个或多个内置类型的变量,此时不能对这些一个或多个内置类型的变量进行赋值(初
//始化),即不能够对在堆区上动态开辟的一个或多个内置类型大小的内存空间进行赋值(初始化),所以此
//时在堆区上动态开辟的一个或多个内置类型大小的内存空间中所存储的数据(指的就是在堆区上声明和定
//义的一个或多个内置类型的变量)均为随机值(垃圾值);使用free库函数,只会释放在堆区上动态开辟的
//一个或多个内置类型大小的内存空间、
//使用new操作符操作内置类型,即在堆区上动态开辟一个或多个内置类型大小的内存空间,相当于在堆区
//上声明和定义了一个或多个内置类型的变量,此时能够对这些一个或多个内置类型的变量进行赋值(初
//始化),即能够对在堆区上动态开辟一个或多个内置类型大小的内存空间进行赋值(初始化),若不对这些
//一个或多个内置类型的变量进行赋值(初始化),即不对在堆区上动态开辟一个或多个内置类型大小的内存
//空间进行赋值(初始化),则在堆区上动态开辟的一个或多个内置类型大小的内存空间中所存储的数据(指
//的就是在堆区上声明和定义的一个或多个内置类型的变量)均为随机值(垃圾值),使用delete操作符的话
//,也只会释放掉在堆区上动态开辟的一个或多个内置类型大小的内存空间、
//其次,使用malloc库函数的话,需要对指针变量进行判空检查,但使用new操作符时,不需要对指针变量进
//行判空检查,这是因为new操作符使用失败时,不会返回空指针,而是会抛异常,具体在后面进行阐述、

//2、
//使用malloc库函数操作自定义类型,即在堆区上动态开辟一个或多个自定义类型大小的内存空间,相当于
//在堆区上声明和定义了一个或多个自定义类型的对象,此时不能对这些一个或多个自定义类型的对象进行
//赋值(初始化),即不能对在堆区上动态开辟的一个或多个自定义类型大小的内存空间进行赋值(初始化),
//也不会自动调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初始化),
//所以此时在堆区上动态开辟的一个或多个自定义类型大小的内存空间中所存储的数据(指的就是在堆区上
//声明和定义的一个或多个自定义类型的对象中的非静态类成员变量)均为随机值(垃圾值);使用free库
//函数,只会释放在堆区上动态开辟的一个或多个自定义类型大小的内存空间、
//使用new操作符操作自定义类型,即在堆区上动态开辟一个或多个自定义类型大小的内存空间,相当于在
//堆区上声明和定义了一个或多个自定义类型的对象,此时能够对这些一个或多个自定义类型的对象进行赋
//值(初始化),即能够对在堆区上动态开辟一个或多个自定义类型大小的内存空间进行赋值(初始化),此时
//可以自动调用自定义的类的类体中的非默认(蓝色)构造函数对类体中的非静态类成员变量进行赋值或赋
//值(初始化),我们也可以对此处这些在堆区上声明和定义的一个或多个自定义类型的对象不进行赋值(初
//始化),此时可以自动调用自定义的类的类体中的默认(蓝色)构造函数对类体中的非静态类成员变量进行
//赋值或赋值(初始化),使用delete操作符的话,他会先自动调用自定义的类的类体中的析构函数进行资源
//的清理,然后再释放掉在堆区上动态开辟的一个或多个自定义类型大小的内存空间、

int main()
{
	Test();
	return 0;
}

注意:
        在堆区上动态申请和释放单个内存空间,则使用 new 和 delete 操作符;在堆区上动态申请和释放多个连续的内存空间,则使用 new[ ] 操作符和 delete[ ] 操作符、

3.2、new 和 delete 操作符操作自定义类型

注意:

在 C++ 中声明和定义(以此为例)类时,什么情况下会使用关键字 struct 呢?

        在 C++ 中声明和定义类时,一般都使用关键字 class,但是在某一些情况下,也会使用关键字 struct,像设置链表中的节点时,一般就使用关键字 struct 来声明和定义类,这是因为,关键字 struct 声明和定义的类中若不使用访问限定符的话,则类成员函数和类成员变量的默认访问权限是 public,关键字 class 声明和定义的类中若不使用访问限定符的话,则类成员函数和类成员变量的默认访问权限是 private;其次我们知道,在类体中通常把类成员变量都设为私有或保护,但并不是绝对的,但通常情况下是这样的;类体中的类成员函数则需要根据自己的需求来设置其访问权限,通常上都是设置为公有的,但也不是绝对的、

        当需要经常在类体外直接访问类体中的类成员变量时,我们可以使用关键字 class 来声明和定义类,并且把类体中的类成员变量都设置为公有,或者使用关键字 class 来声明和定义类,把类体中的类成员变量都设置为私有或保护,再使用友元来解决,但是该方法在一些情况下还是不满足我们的要求,所以不建议使用;其次就是使用关键字 struct 来声明和定义类,且在该类体中不使用访问限定符,那么此时,该类体中的所有的内容都可以在类外直接进行访问,因为,关键字 struct 声明和定义的类中若不使用访问限定符的话,则类成员函数和类成员变量的默认访问权限是 public,即当某一个类中的所有的类成员变量和类成员函数都需要在类外直接进行访问时,或者是当需要经常在类体外直接访问类体中的类成员时,我们常使用关键字 struct 来声明和定义类,并且在该类的类体中不使用访问限定符的形式,其次,在后面所学的 STL 中,STL 源码中设置链表的节点时,也是使用的关键字 struct 来声明和定义的类,且在该类体中不使用访问限定符的形式、

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
	Date(int year)
		:_year(year)
	{
		cout << _year << endl;
		cout << "Date(int year)" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
};
int main()
{
	//new操作符操作自定义类型,会先在堆区上动态开辟内存空间,然后再自动调用自定义的类的类体中
//的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化)、
	Date* d1 = new Date(2022);
	delete d1;
    d1 = nullptr;
	//delete操作符操作自定义类型,会先自动调用自定义的类的类体中的析构函数进行资源的清理,然后
//再释放在堆区上动态开辟的内存空间、
	return 0;
}
//在C++中,建议使用new操作符,而不建议使用malloc库函数、

//1、
//使用malloc库函数操作内置类型,即在堆区上动态开辟一个或多个内置类型大小的内存空间,相当于在堆
//区上声明和定义了一个或多个内置类型的变量,此时不能对这些一个或多个内置类型的变量进行赋值(初
//始化),即不能够对在堆区上动态开辟的一个或多个内置类型大小的内存空间进行赋值(初始化),所以此
//时在堆区上动态开辟的一个或多个内置类型大小的内存空间中所存储的数据(指的就是在堆区上声明和定
//义的一个或多个内置类型的变量)均为随机值(垃圾值);使用free库函数,只会释放在堆区上动态开辟的
//一个或多个内置类型大小的内存空间、
//使用new操作符操作内置类型,即在堆区上动态开辟一个或多个内置类型大小的内存空间,相当于在堆区
//上声明和定义了一个或多个内置类型的变量,此时能够对这些一个或多个内置类型的变量进行赋值(初
//始化),即能够对在堆区上动态开辟一个或多个内置类型大小的内存空间进行赋值(初始化),若不对这些
//一个或多个内置类型的变量进行赋值(初始化),即不对在堆区上动态开辟一个或多个内置类型大小的内存
//空间进行赋值(初始化),则在堆区上动态开辟的一个或多个内置类型大小的内存空间中所存储的数据(指
//的就是在堆区上声明和定义的一个或多个内置类型的变量)均为随机值(垃圾值),使用delete操作符的话
//,也只会释放掉在堆区上动态开辟的一个或多个内置类型大小的内存空间、
//其次,使用malloc库函数的话,需要对指针变量进行判空检查,但使用new操作符时,不需要对指针变量进
//行判空检查,这是因为new操作符使用失败时,不会返回空指针,而是会抛异常,具体在后面进行阐述、

//2、
//使用malloc库函数操作自定义类型,即在堆区上动态开辟一个或多个自定义类型大小的内存空间,相当于
//在堆区上声明和定义了一个或多个自定义类型的对象,此时不能对这些一个或多个自定义类型的对象进行
//赋值(初始化),即不能对在堆区上动态开辟的一个或多个自定义类型大小的内存空间进行赋值(初始化),
//也不会自动调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初始化),
//所以此时在堆区上动态开辟的一个或多个自定义类型大小的内存空间中所存储的数据(指的就是在堆区上
//声明和定义的一个或多个自定义类型的对象中的非静态类成员变量)均为随机值(垃圾值);使用free库
//函数,只会释放在堆区上动态开辟的一个或多个自定义类型大小的内存空间、
//使用new操作符操作自定义类型,即在堆区上动态开辟一个或多个自定义类型大小的内存空间,相当于在
//堆区上声明和定义了一个或多个自定义类型的对象,此时能够对这些一个或多个自定义类型的对象进行赋
//值(初始化),即能够对在堆区上动态开辟一个或多个自定义类型大小的内存空间进行赋值(初始化),此时
//可以自动调用自定义的类的类体中的非默认(蓝色)构造函数对类体中的非静态类成员变量进行赋值或赋
//值(初始化),我们也可以对此处这些在堆区上声明和定义的一个或多个自定义类型的对象不进行赋值(初
//始化),此时可以自动调用自定义的类的类体中的默认(蓝色)构造函数对类体中的非静态类成员变量进行
//赋值或赋值(初始化),使用delete操作符的话,他会先自动调用自定义的类的类体中的析构函数进行资源
//的清理,然后再释放掉在堆区上动态开辟的一个或多个自定义类型大小的内存空间、
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
//#include<assert.h>
using std::cout;
using std::cin;
using std::endl;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new int[capacity]; 
		//等价于: 
		//_a = (int*)malloc(sizeof(int)*capacity);
		//assert(_a);
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[] _a;  //等价于: free(_a);
		//顺手将类成员变量_capacity和_top置为0、
		_capacity = 0;
		_top = 0;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	Stack st;
	return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using std::cout;
using std::cin;
using std::endl;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new int[capacity];
		//等价于: 
		//_a = (int*)malloc(sizeof(int)*capacity);
		//assert(_a);
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[] _a;  //等价于: free(_a);
		//顺手将类成员变量_capacity和_top置为0、
		_capacity = 0;
		_top = 0;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	//1、
	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
	assert(ps1);
    //使用malloc库函数操作自定义类型,即在堆区上动态开辟一个或多个自定义类型大小的内存空间,相
//当于在堆区上声明和定义了一个或多个自定义类型的对象,此时不能对这些一个或多个自定义类型的对象
//进行赋值(初始化),即不能对在堆区上动态开辟的一个或多个自定义类型大小的内存空间进行赋值(初
//始化),也不会自动调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初
//始化),所以此时在堆区上动态开辟的一个或多个自定义类型大小的内存空间中所存储的数据(指的就是在
//堆区上声明和定义的一个或多个自定义类型的对象中的非静态类成员变量)均为随机值(垃圾值);使用
//free库函数,只会释放在堆区上动态开辟的一个或多个自定义类型大小的内存空间、

	//此时在此处是没办法直接对在堆区上动态开辟的内存空间中所存储的数据,即自定义的类的类体中
//的非静态类成员变量进行赋值(仅仅是赋值)的,这是因为:类体中的类成员变量都是私有的,在类外不能
//直接访问;在不考虑友元的情况下,可以通过调用该自定义的类的类体中的构造函数进行赋值或赋值(初
//始化);但是,上述代码没办法调用该构造函数,且也不能通过:ps1->Stack(可传实参,也可不传实参);的
//形式来调用该构造函数,后期会讲解定位new,则可以通过该方式去调用该构造函数,具体在后面进行阐述
//,则此时,类体中的类成员变量_a就是野指针、

	//2、
	Stack* ps2 = new Stack;
	//new操作符操作自定义类型,会先在堆区上动态开辟内存空间,然后再自动调用自定义的类的类体中
//的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化)、

	free(ps1);
    ps1 = nullptr;
	//使用free库函数,只会释放在堆区上动态开辟的内存空间、

	delete ps2;
    ps2 = nullptr;
	//使用delete操作符操作自定义类型,则会先自动调用该自定义的类的类体中的析构函数进行资源的
//清理,然后再释放在堆区上动态开辟的内存空间;此处的资源清理并不是说把某一个类的类体中的非静态
//类成员变量所指向的所有的内存空间(主要指的就是在堆区上动态开辟的内存空间)都要释放掉,具体情况
//具体分析,比如在链表中的节点的类体中的非静态类成员变量_next和_prev所指向的内存空间(主要指的
//就是在堆区上动态开辟的内存空间)均不需要通过该节点类体中的析构函数进行释放、

	return 0;
}
//在C++中,建议使用new操作符,而不建议使用malloc库函数、
//使用malloc库函数的话,需要对指针变量进行判空检查,但使用new操作符时,不需要对指针变量进行判空
//检查,这是因为new操作符使用失败时,不会返回空指针,而是抛异常,具体在后面进行阐述、

注意:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		delete[] _a;  
		_capacity = 0;
		_top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	//malloc失败(动态开辟内存空间失败),则会返回空指针、
	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
	assert(ps1);
	free(ps1);
	ps1 = nullptr;

	//new操作符使用失败,则会抛异常,不再是返回空指针、
	Stack* ps2 = new Stack;
	delete ps2;
	ps2 = nullptr;

	//为了消耗堆区上的内存空间,使得下面两次在堆区上动态开辟内存空间都失败、
	void* p0 = malloc(1024 * 1024 * 1024); //1G,等于10亿个字节、
	assert(p0);
	cout << p0 << endl;
	free(p0);
	p0 = nullptr;


	void* p1 = malloc(1024 * 1024 * 1024); //1G,等于10亿个字节、
	//assert(p1);
	cout << p1 << endl; //00000000
	free(p1);
	p1 = nullptr;

	//char* p2 = new char[1024 * 1024 * 1024]; //1G,等于10亿个字节、
	//cout << (void*)p2 << endl; //为了打印出地址,则进行强制类型转换、
	//delete[] p2;
	//p2 = nullptr;
	
    //此时,new操作符使用失败,则会抛异常,若抛异常未进行捕获的话,则会报错、
	//面向对象的语言处理错误的方法大都是抛异常的方式、
	//异常依赖继承和多态的机制,在后期再进行阐述、

	//捕获异常的方法:
	//1、
	try
	{
		char* p2 = new char[1024 * 1024 * 1024]; //1G,等于10亿个字节、
		cout << (void*)p2 << endl; //为了打印出地址,则进行强制类型转换、
		//若此处的new操作符使用失败,则会抛异常,那么此时就不会执行代码:
//cout << (void*)p2 << endl;而是直接进入异常捕获的代码中,即catch所在的语句中、
		//若此处的new操作符使用成功,则不会抛异常,那么此时就会执行代码:
//cout << (void*)p2 << endl;从而打印出有效的地址、
	    delete[] p2;
	    p2 = nullptr;
	}
	catch (const exception& e) //捕获异常,捕获库中抛出的异常的方法、
	{
		cout << e.what() << endl; 
        //打印出抛出来的异常的信息:bad allocation,此时就不会再报错说是未捕捉抛出的异常、
	}
	//若每一个像这种存在抛异常可能性的地方,都按上面捕捉异常的方法1来做的话,显得十分冗余,此时
//可以使用捕捉异常的方法2,具体如下所示:
	return 0;
}

//捕获异常的方法:
//2、
//#define _CRT_SECURE_NO_WARNINGS 1
//#include<iostream>
//#include<assert.h>
//using namespace std;
//class Stack
//{
//public:
//	Stack(int capacity = 10)
//	{
//		_a = new int[capacity];
//		_capacity = capacity;
//		_top = 0;
//	}
//	~Stack()
//	{
//		delete[] _a;  
//		_capacity = 0;
//		_top = 0;
//	}
//private:
//	int* _a;
//	int _capacity;
//	int _top;
//};
//void Func()
//{
//	//malloc失败(动态开辟内存空间失败),则会返回空指针、
//	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
//	assert(ps1);
//	free(ps1);
//	ps1 = nullptr;
//
//	//new操作符使用失败,则会抛异常,不再是返回空指针、
//	Stack* ps2 = new Stack;
//	delete ps2;
//	ps2 = nullptr;
//
//	void* p1 = malloc(1024 * 1024 * 1024); //1G,等于10亿个字节、
//	assert(p1);
//	cout << p1 << endl;
//	free(p1);
//	p1 = nullptr;
//
//	char* p2 = new char[1024 * 1024 * 1024]; //1G,等于10亿个字节、
//	cout << (void*)p2 << endl; //为了打印出地址,则进行强制类型转换、
//	//若此处的new操作符使用失败,则会抛异常,那么此时就不会执行代码:cout << (void*)p2 << endl; 而是直接进入异常捕获的代码中,即catch所在的语句中、
//	//若此处的new操作符使用成功,则不会抛异常,那么此时就会执行代码:cout << (void*)p2 << endl; 从而打印出有效的地址、
//  delete[] p2;
//  p2 = nullptr;
//}
//int main()
//{
//	try
//	{
//		Func();
//	}
//	catch (const exception& e) //捕获异常,捕获库中抛出的异常的方法、
//	{
//		cout << e.what() << endl; //打印出抛出来的异常的信息:bad allocation,此时就不会再报错说是未捕捉抛出的异常、
//	}
//}
//由上面的捕捉异常的方法1和方法2可得出,当使用new操作符时,不需要对指针变量进行判空检查(判断是
//否为空指针),一般情况下,我们通常使用方法2、

4、operator new 与 operator delete 全局库函数

4.1、operator new 与 operator delete 全局库函数

注意:

        这里的 operator new 和 operator delete 全局库函数并不是对操作符 new 和 delete 进行的重载,这是两个全局库函数(C++标准库中的全局函数),分别是两个全局库函数的函数名、

        throw (std::bad_alloc) 异常规格表示可能会抛出 std::bad_alloc 的异常,throw() 表示不会抛出异常,noexcept 也表示不会抛异常、

拓展:

        在 C++11 中,C++ 标准库中的 operator new 和 operator delete 全局库函数已经不再抛异常,如下所示:

operator new 与 operator delete 全局库函数的源码如下:


//1、
//全局库函数operator new的源码:
//operator new:该全局库函数实际通过malloc库函数来动态申请堆区上的内存空间,当malloc库函数动
//态申请堆区上的内存空间成功时,对于malloc库函数而言,则会返回一个有效的地址,同时对于operator
// new全局库函数而言,也是返回一个有效的地址,该有效的地址就是由malloc库函数返回的那个有效的
//地址;当动态申请堆区上的内存空间失败时,尝试执行动态开辟堆区上内存空间不足应对措施,如果用户设
//置了应对措施,则继续申请,否则,malloc库函数就会返回空指针,而对于operator new全局库函数而言
//则会抛异常、

//_THROW1(_STD bad_alloc)是抛异常,异常的机制、
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	void *p;
	while ((p = malloc(size)) == 0)
	if (_callnewh(size) == 0)
	{
		//如果在堆区上动态申请内存空间失败,这里会抛出bad_alloc类型的异常、
		static const std::bad_alloc nomem;
		_RAISE(nomem);
	}
	return (p);
}
//operator new全局库函数封装了malloc库函数,若malloc库函数操作失败,对于malloc库函数而言,会返
//回空指针,但这里添加了抛异常的机制,故当malloc库函数操作失败时,对于malloc库函数而言,会返回空
//指针,但是对于operator new全局库函数而言,则是会抛异常,并不会返回空指针、


//2、
//全局库函数operator delete的源码:
//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);
	
    //operator delete全局库函数封装了_free_dbg库函数,而free库函数也封装了_free_dbg库函数
//,故,operator delete全局库函数封装了free库函数、
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
		return;
}
//operator delete全局库函数和free库函数的用法没有任何区别、

//库函数free的实现、
//free库函数也封装了_free_dbg库(C/C++标准库)函数、
#define free(p) _free_dbg(p, _NORMAL_BLOCK)  //宏函数、
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a; 
		_capacity = 0;
		_top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	//1、
	//operator new全局库函数的功能和库函数malloc的功能是一样的,只不过这里不需要进行判空,要
//注意:判空不仅仅只有断言一种方式,还有其他的方式; operator new 全局库函数操作失败会抛异常,不
//会返回空指针、
	Stack* st1 = (Stack*)operator new(sizeof(Stack));
	//此处不需要进行判空、

	operator delete(st1); 
    //operator delete全局库函数和free库函数的功能没有任何区别,都只是释放在堆区上动态开辟的
//内存空间,即使操作的是自定义类型、
	st1 = nullptr;

	//2、
	Stack* st2 = (Stack*)malloc(sizeof(Stack));
	assert(st2);
	free(st2);
	st2 = nullptr;

	//3、
	//operator new全局库函数和operator delete全局库函数都没有直接的使用价值,但他们在操作符
//new和操作符delete的底层实现中会起到作用、
	Stack* st3 = new Stack;  //该行代码在编译器编译后就会转换成二进制指令、
	//操作符new针对自定义类型时,会先在堆区上动态开辟内存空间,然后再自动调用自定义的类的类体
//中的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化),在堆区上动态开辟内存空间的过
//程中,若是使用malloc库函数的话,则动态开辟内存空间失败就会返回空指针,但对于面向对象的编程语
//言(C++,Java,Python等)而言,标准库中要求在出现错误时,最好是进行抛异常,然后捕捉异常,再打印出
//异常信息,所以,此处的在堆区上动态开辟内存空间的过程中,实际上自动调用的就是operator new全局
//库函数,这样一来,即满足了在堆区上动态开辟了内存空间,还使得在出现错误的地方进行抛异常,而不是
//返回空指针,所以对于操作符new在操作自定义类型的时候,底层原理就转换成了先自动调用全局库函
//数operator new,然后再自动调用自定义的类的类体中的构造函数来对类体中的非静态类成员变量进行
//赋值或赋值(初始化),对于操作符new在操作内置类型的时候,则底层原理就转换成了直接自动调用全局
//库函数operator new,就结束了、
	delete st3;
	//操作符delete针对自定义类型时,先自动调用自定义的类的类体中的析构函数进行资源的清理,然后
//再自动调用operator delete全局库函数对在堆区上动态开辟的内存空间进行释放;当操作符delete针
//对内置类型时,则会直接自动调用operator delete全局库函数对在堆区上动态开辟的内存空间进行释放
//,这里两处自动调用operator delete全局库函数对在堆区上动态开辟的内存空间进行释放,而不是直接
//自动调用free库函数来对在堆区上动态开辟的内存空间进行释放的原因是因为要与前面所谓的自动调
//用operator new全局库函数保持一致、
	st3 = nullptr;
	
	return 0;
}

4.2、专属的非静态类成员函数 operator new 与 operator delete

注意:
        内存(一般指的是在堆区上动态开辟的内存空间)池是一种内存(一般指的是在堆区上动态开辟的内存空间)分配方式,又被称为固定大小区块规划;通常我们习惯直接使用操作符 new 或 malloc 库函数等 API 在堆区上动态开辟内存空间,这样做的缺点在于:由于在堆区上所动态开辟的内存块的大小不定,且 malloc 库函数的调用非常慢,一般来说,少量的操作不会造成什么影响,但当频繁的在堆区上动态开辟内存空间时,就会造成堆区上产生大量的内存碎片并且降低性能,使得内存分配效率较低,内存池则是在真正使用堆区上的内存空间之前,先动态开辟好一定数量的、大小相等(一般情况下)的内存块留作备用,然后再对这些内存块进行管理,当有新的内存需求时,就从内存池中分出一部分内存块,若从内存池中分出来的一部分内存块不够使用,则再继续向内存池申请新的内存块进行使用,这样做的一个显著优点是:使得内存分配效率得到提升,在内核中有不少地方内存(一般指的是在堆区上动态开辟的内存空间)分配不允许失败,作为一个在这些情况下确保成功分配堆区上内存空间的方式,内核开发者创建了一个已知为内存池(或者是 "mempool" )的抽象,一个内存池真实的只是一类后备缓存,它尽力一直保持一个空闲内存列表给紧急时使用、

拓展:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
	Date(int year)
		:_year(year)
	{
		cout << _year << endl;
		cout << "Date(int year)" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
};
int main()
{
	//new操作符操作自定义类型,会先在堆区上动态开辟内存空间,然后再自动调用自定义的类的类体中
//的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化)、
	Date* d1 = new Date(2022);
    //注意:
    //此处的int整型2022并不是给C++标准库中的全局库函数operator new传实参,而是给当此处自动调
//用完毕在C++标准库中的全局库函数operator new后再自动调用的自定义的类体中的构造函数传实参,来
//对类体内的非静态类成员变量进行赋值或赋值(初始化);也就是说:此处的int整型2022既不会给自动调
//用的在C++标准库中的全局库函数operator new传实参,也不会给自动调用的在自定义的类体中显式声明
//和定义的非静态类成员函数operator new传实参、

	delete d1;
	d1 = nullptr;
	//delete操作符操作自定义类型,会先自动调用自定义的类的类体中的析构函数进行资源的清理,然后
//再释放在堆区上动态开辟的内存空间、
	return 0;
}


#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()   
{
    
    //此时在堆区上动态开辟了一个int类型大小的内存空间,相当于在堆区上声明和定义了一个int类型的
//变量、
    //在堆区上动态申请一个int类型大小的内存空间并且进行赋值(初始化)为1;不需要强制类型转换、
	int* p = new int(1);
    //注意:
    //此处的int整型1并不是给C++标准库中的全局库函数operator new传实参,而是只是用来赋值(初
//始化)在堆区上声明和定义的这一个int类型的变量的、
	delete p;
	p = nullptr;

	return 0;
}

        下面代码演示了,针对链表的节点 ListNode 类的类体中通过显式的声明和定义专属的非静态类成员函数 operator new 和 operator delete,实现链表节点使用内存池动态开辟和释放堆区上的内存空间,从而提高堆区内存空间的动态分配效率、

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
struct ListNode
{
	ListNode(int data=0)
		:_next(nullptr)
		,_prev(nullptr)
		, _data(data)
	{
		cout << "ListNode(int data=0)" << endl;
	}
	
    /*
    //专属的非静态类成员函数operator new、
    //这里的size_t n接收的是在堆区上动态开辟的内存空间的大小、
	void* operator new(size_t n)
	{
		//在此处向内存池动态申请堆区上的内存空间、
		void* p = nullptr;
		//内存池的机制、
		p = allocator<ListNode>().allocate(1);
		cout << "memory pool allocate" << endl;
		return p;
	}
   
	//专属的非静态类成员函数operator delete、
	void operator delete(void* p)
	{
		//在此处动态释放由内存池动态分配的在堆区上的内存空间,再还给内存池,供下一次向内存池动
//态申请堆区上的内存空间时使用、
		//内存池的机制、
		allocator<ListNode>().deallocate((ListNode*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
    */

	int _data;
	ListNode* _next;
	ListNode* _prev;
};
class List
{
public:
	List()
	{
		_head = new ListNode;
		_head->_next = _head;
		_head->_prev = _head;
	}
	~List()
	{
		Clear();
		delete _head;
		_head = nullptr;
	}
	void PushBack(int val)
	{
		ListNode* newnode = new ListNode;
		//若此时不在ListNode类的类体中显式的声明和定义operator new和operator delete非静态
//类成员函数的话,则此时自动调用的就是在C++标准库中的全局库函数operator new和operator
// delete(具体为什么自动调用的是在C++标准库中的全局库函数operator new和operator delete是其
//底层逻辑实现,我们不需要考虑);若要在ListNode类的类体中显式的声明和定义operator new和operator 
//delete非静态类成员函数的话,那么此时就会直接自动调用这两个非静态类成员函数(具体为什么能够直
//接自动调用这两个非静态类成员函数是其底层逻辑实现,我们不需要考虑),即:操作内存池,此处操作符
//new操作的是自定义类型ListNode,则要在ListNode类的类体中显式的声明和定义这两个非静态类成员
//函数、
		ListNode* tail = _head->_prev;
		tail->_next = newnode;
		newnode->_prev = tail;
		newnode->_next = _head;
		_head->_prev = newnode;
	}
	void Clear()
	{
		ListNode* cur = _head->_next;
		while (cur != _head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}
		_head->_next = _head;
		_head->_prev = _head;
	}
private:
	ListNode* _head;
};
int main()
{
	List l;
	int n = 0;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		l.PushBack(i);
	}
	l.Clear();
	cout << endl << endl;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		l.PushBack(i);
	}
	//此时就会频繁多次执行非静态类成员函数PushBack中的代码:ListNode* newnode = new ListNode;
//,由于操作符new操作的是自定义类型,故则会优先自动调用在C++标准库中的operator new全局库函数,
//然后该全局库函数再去调用malloc库函数,而malloc库函数的调用本身就特别慢,再加上此处又是频繁多
//次调用malloc库函数,从而会导致在堆区上的内存空间动态分配效率较低,如何避免这个问题呢?
	//此时就引出了池化技术,如:内存池,进程池等等,使用的均是池化技术,在此我们使用的是内存池、
	//现在如果需要得到在堆区上的内存空间,就不再直接向堆区进行动态申请,而是找内存池进行动态
//申请,此时动态释放的就是从内存池中动态分配的堆区上的内存空间,而不是直接从堆区上动态分配的在
//堆区上的内存空间,动态释放的从内存池中动态分配的在堆区上的内存空间,就会再返回到内存池中,供下
//一次向内存池动态申请在堆区上的内存空间时使用、
    //注意:使用malloc/realloc/calloc等库函数就相当于是直接向堆区动态申请内存空间,除此之外
//的在C++标准库中的operator new全局库函数也会调用malloc库函数,也相当于是直接向堆区动态申请
//内存空间;若在ListNode类的类体中显式的声明和定义出非静态类成员函数operator new和operator
// dalete的话,此时,再执行代码:ListNode* newnode = new ListNode;就会直接自动调用ListNode类
//的类体中的operator new非静态类成员函数,此时是直接向内存池动态申请在堆区上的内存空间,所以,
//即使频繁多次执行代码:ListNode* newnode = new ListNode;,也不会造成堆区内存空间动态分配效
//率较低的情况、
	return 0;
}

5、操作符 new 和 delete 的底层实现原理

5.1、内置类型

        此时操作符 new 和 malloc 库函数,操作符 delete 和 库函数 free 基本类似;操作符 new/delete 动态申请和释放的是堆区上单个的内存空间,而操作符 new[ ] 和 delete[ ] 动态申请和释放的是堆区上多个连续的内存空间;操作符 new 在堆区上动态申请内存空间失败时会抛异常,但库函数 malloc 在堆区上动态开辟内存空间失败时则会返回空指针、

5.2、自定义类型

操作符 new 的底层实现原理:
1、先自动调用在 C++ 标准库中的 operator new 全局库函数(不考虑内存池),直接向堆区动态申请内存空间、
2、之后再自动调用自定义的类的类体中的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化)、
操作符 delete 的底层实现原理:
1、先自动调用自定义的类的类体中的析构函数进行自定义类型的对象中资源的清理工作、
2、然后再自动调用在 C++ 标准库中的 operator delete 全局库函数(不考虑内存池)来释放在堆区上动态开辟的内存空间、
操作符 new T[N] 的底层实现原理,其中 T 可以为内置类型,也可以为自定义类型:
1、先自动调用在 C++ 标准库中的 operator new[ ] 全局库函数,在全局库函数
operator new[ ] 中实际调用了 N 次在 C++ 标准库中的 operator new 全局库函数(不考虑内存池),在堆区上动态开辟了 N 个自定义类型大小的内存空间,此时可以理解为:在 C++ 标准库中的 operator new[ ] 全局库函数封装了在 C++ 标准库中的 operator new 全局库函数(不考虑内存池)、
2、之后再自动调用 N 次自定义的类的类体中的构造函数来对类体中的非静态类成员变量进行赋值或赋值(初始化)、
操作符 delete[ ] 的底层实现原理:
1、先自动调用 N 次自定义的类的类体中的析构函数进行自定义类型的对象中资源的清理工作、
2、然后再自动调用在 C++ 标准库中的 operator delete[ ] 全局库函数释放在堆区上动态开辟的内存空间,实际在 operator delete[ ] 全局库函数中调用了 N 次在 C++ 标准库中的 operator delete 全局库函数(不考虑内存池)来释放在堆区上动态开辟的内存空间,此时可以理解为:在 C++ 标准库中的 operator delete[ ] 全局库函数封装了在 C++ 标准库中的 operator delete 全局库函数(不考虑内存池)、

 注意:        

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a; 
		_capacity = 0;
		_top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{
	//1、
	int* p = new int[10];
	delete p;//编译不会出错,且即不会造成内存泄漏,也不会崩溃、
    p = nullptr;

	//2、
	Stack* st = new Stack[10];
	//错误用法:
	//delete st;//编译不会出错,但可能会崩溃,也可能不崩溃,但往往会造成崩溃,也会造成内存泄漏、
    //st = nullptr;

	//正确用法:
	delete[] st;//编译不会出错,且即不会崩溃,也不会造成内存泄漏、
    st = nullptr;

	return 0;
}

6、定位 new 表达式 ( placement-new )

拓展:
        当使用操作符 new 和 delete 操作自定义类型时,不管在自定义的类的类体中是否显示的声明和定义了 operator new 和 operator delete 类成员函数,则此时均会自动调用自定义的类的类体中的构造函数和析构函数、
        定位 new 表达式在实际中一般是配合内存池使用的,因为内存池分配出的堆区上的内存空间中所存储的对象或变量中的数据并没有赋值或赋值(初始化),如果是自定义类型的对象,如果此时不能够自动调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初始化)的话,则可以通过定位 new 的表达式调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初始化),如果此时能够自动调用自定义的类的类体中的构造函数对类体中的非静态类成员变量进行赋值或赋值(初始化)的话,则就不需要使用定位 new 的表达式了;如果是内置类型的变量,也可以通过定位 new 表达式直接进行赋值(仅仅是赋值);除此之外,定位 new 表达式也会用在其他的一些情况下,并不一样非要配合内存池使用、
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 1)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a;
		_capacity = 0;
		_top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
int main()
{	
    //Stack* obj = (Stack*)operator new(sizeof(Stack));
	//此时在此处是没办法直接对在堆区上动态开辟的内存空间中所存储的数据,即自定义的类的类体中
//的非静态类成员变量进行赋值(仅仅是赋值)的,这是因为:类体中的类成员变量都是私有的,在类外不能
//直接访问;在不考虑友元的情况下,可以通过调用该自定义的类的类体中的构造函数进行赋值或赋值(初
//始化);但是,上述代码没办法调用该构造函数,且也不能通过:obj->Stack(可传实参,也可不传实参);的
//形式来调用该构造函数,则可以通过定位new的方式去调用该构造函数,注意:可以通过:obj->~Stack();
//的形式来调用自定义的类的类体中的析构函数、

	//使用场景:
	//1、
	Stack* obj = (Stack*)operator new(sizeof(Stack));
    //不需要进行判空检查、
	new(obj)Stack;  //new(place_address)type、
	delete obj;
	obj = nullptr;
    //如果写的是operator new或者operator delete的话(只是单纯的调用C++标准库中的这两个全局
//库函数),则调用的都是C++标准库中的全局库函数(并不是自定义的类的类体中的类成员函数,具体请见
//之前的博客),并且在调用全局库函数operator new之后,不会自动调用自定义的类的类体中的构造函数,
//在调用全局库函数operator delete之前,也不会自动调用自定义的类的类体中的析构函数、

	//2、
	//Stack* obj = (Stack*)operator new(sizeof(Stack));
	//new(obj)Stack(10);//new(place_address)type(initializer-list)、
    //注意:
	//此处的int整型10并不是给C++标准库中的全局库函数operator new传实参,而是给当此处自动调用
//完毕在C++标准库中的全局库函数operator new后再自动调用的自定义的类体中的构造函数传实参,来对
//类体内的非静态类成员变量进行赋值或赋值(初始化);也就是说:此处的int整型10既不会给自动调用的
//在C++标准库中的全局库函数operator new传实参,也不会给自动调用的在自定义的类体中显式声明和定
//义的非静态类成员函数operator new传实参、
	//delete obj;
	//obj = nullptr;

	//上述使用场景2等价于以下代码:
	//Stack* obj = new Stack(10);
	//delete obj;
	//obj = nullptr;

	return 0;
}

7、常见面试题

7.1、库函数 malloc 和 free 与操作符 new 和 delete 的区别

共同点: 都是动态获取在堆区上的内存空间,且都需要用户手动的动态释放这些内存空间、

不同点:

1、malloc 和 free 都是库函数,而 new 和 delete 都是操作符、
2、malloc 库函数在堆区上动态申请内存空间时,需要计算出所要动态申请的内存空间大小并传递;而操作符 new 只需在其后面跟上类型即可,如果需要在堆区上动态开辟多个连续的内存空间,则在 [ ] 中指出个数即可、
3、malloc 库函数的返回类型为 void*;操作符 new 在使用时不需要强制类型转换,因为操作符 new 后面跟的就是类型、
4、malloc 库函数在堆区上动态申请内存空间失败时,返回的是空指针,使用时最好要进行判空;而操作符 new 在使用时不需要进行判空,但是操作符 new 在使用时最好要捕获异常、

7.2、内存泄漏

7.2.1、内存泄漏的分类

在 C/C++ 程序中一般我们关心 两方面 的内存泄漏:

堆区内存泄漏 (Heap leak):

        堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等动态的获取在堆区上的内存空间,用完后必须通过相应的 free 库函数或者 delete 操作符来动态释放掉这些内存空间;假设程序的设计错误导致这部分内存空间没有被释放且指向这部分内存空间的指针丢失找不到,那么以后这部分内存空间将无法再被释放掉,从而导致无法再次被使用,就会产生 Heap Leak、
操作系统资源泄漏:
        指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定、

7.2.2、什么是内存泄漏?内存泄漏的危害?

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

内存泄漏的危害:

        在智能指针中会再次进行阐述;一些情况下即使内存泄漏也没有事,就比如当前我们在 VS2013 编译器下写的代码,即使造成内存泄漏也不会出现问题,因为当进程结束后,该被泄漏的内存空间就会自动的还给操作系统,在虚拟内存与物理内存的映射中会进行阐述,即:当进程在正常结束时,会把所有的资源都清理掉,即把所有的内存空间都会自动还给操作系统,这种机制就是为了防止内存泄漏的问题;但是如果使用的设备上堆区的内存空间很小时,若出现内存泄漏问题,就比较麻烦,或者长期运行的程序中,若出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死、


void MemoryLeaks()
{
  int* p1 = (int*)malloc(sizeof(int));
  assert(p1);
  free(p1);
  p1 = nullptr;

  int* p2 = new int;
  delete p2;
  p2 = nullptr;

  //异常安全问题、
  int* p3 = new int[10];
 
  Func();//这里Func函数抛异常导致 delete[] p3; 未执行、
 
  delete[] p3;
  p3 = nullptr;

}

7.2.3、如何检测内存泄漏?

7.2.4、如何避免内存泄漏?

1、工程前期良好的设计规范,养成良好的编码规范;比如在堆区上动态开辟的内存空间记着要手动的动态去释放、
Ps:这是理想状态,但是如果碰上异常时,就算注意动态释放了,还是可能会出问题,需要智能指针来管理才有保证、
2、采用 RAII 思想或者智能指针来管理资源、
3、有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项、
4、出问题了使用内存泄漏工具检测,不过很多工具都不够靠谱,或者收费昂贵、

总结:

        内存泄漏非常常见,解决方案分为两种:1、事前预防型,如智能指针等;2、事后查错型,如泄漏检测工具等、

7.3、如何一次在堆区上动态申请 4G 的内存空间?

//将程序编译成x64的进程,运行下面的程序试试?
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{
  void* p = new char[0xfffffffful];
  cout << "new:" << p << endl;

  delete[] p;
  p = nullptr;
  return 0; 
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脱缰的野驴、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值