目录
1、C/C++内存分布
2、C语言中动态内管理存方式
2.1、malloc/calloc/realloc和free函数
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、堆(堆区)用于程序运行时动态内存分配,堆是可以上增长的、 4、数据段(静态区) :存储全局数据和静态数据(静态局部数据和静态全局数据),此处的全局数据和静态数据(静态局部数据和静态全局数据),其生命周期都是整个工程,其中,全局数据和全局静态数据的作用域是整个工程,但局部静态数据的作用域是其当前所在的{ }中,静态区中存放的所有数据的生命周期都是整个工程、 5、代码段(常量区或者正文):存储可执行的代码(本质上是二进制指令)或者是只读常量、
注意:????????
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };//整型数组 num1 存放在栈区,即局部整型数组、
char char2[] = "abcd";//字符数组 char2 也存放在栈区,即局部字符数组、
//字符数组char2开辟5个内存空间,把常量字符串中的数据,包括字符'\0',都拷贝过来对该字符数组char2进行初始化、
const char* pChar3 = "abcd";
//pChar3是一个字符指针变量,该变量中存储的是常量字符串中首元素的地址,现在进行解引用操作,即: *pChar3,就找到了常量字符串中的首元素
//也即找到了该常量字符串,而常量字符串又存在于代码段(常量区),故, *pChar3 存在于代码段(常量区)中、
int* ptr1 = (int*)malloc(sizeof (int)* 4);
// ptr1 是一个整形指针变量,该变量中存放的就是在堆区上动态malloc开辟出来的内存空间的地址,现在, *ptr1 代表的就是存放在堆区上的这一块内存空间、
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)* 4);
free(ptr1);
free(ptr3);
}
// *char2 存放在栈区上,因为,char2是数组名,代表数组首元素地址,此时, *char2 找到的就是字符数粗 char2 中的的第一个元素,
//也是存放在栈区上的,此时整个字符数组char2都存放在栈区上,则该数组中的所有元素也都存放在栈区上、
注意:
int main()
{
const char* p = "hello";
cout << p << endl; //hello
cout << (char*)p << endl; //hello
//此时的打印会自动识别类型,又因字符指针变量p是char*类型的,故会自动打印字符串,此时为了输出地址的
//话,可以将字符指针变量p强转为void*类型,如下所示:
cout << (void*)p << endl;
//除此之外,若想要打印地址,也可使用printf函数按照%p的格式进行打印,如下所示:
printf("%p\n", p);
return 0;
}
//空指针是第一个地址(0x00000000)的编号,char类型的字符在二进制的角度上存储的是该字符的ASCII码值,任何类型的一个指针变量中存储的都是一个字节(首字节)的地址、
2、C语言中动态内管理存方式
2.1、malloc/calloc/realloc和free函数
#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));
printf("%p\n", p2);
cout << p2 << endl;//使用该方法打印地址时,除了字符char类型的指针变量不能直接进行打印,因为会自动打印出对应的字符串,这是
//由于该种打印方法会进行自动识别类型的原因,但是其他类型的指针变量是可以直接打印其地址的、
assert(p2);
//free(p2);
//p2=nullptr;//若加上这一句,则下面的代码不会报错,若不加上这一句,则下面的代码会报错,因为,整形指针变量p2指向的内存空间已经
//被释放掉了,下面如果再在该被释放掉的内存空间的后面进行扩容就会出现错误,要想成功运行的话,则代码: free(p2);和p2=nullptr;
//均不能写出来,或者两句代码均都写出来,但要注意,这会造成两种不同的结果、
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* ptr8 = new int;
//此时,在堆区上动态开辟了一个int类型的内存空间,由于该内存空间存在于堆区上,故不对其进行初始化的话,则默认是随机值(垃圾值),
//除此之外,此时的整形指针变量ptr8是一个全局整形指针变量,该变量已经进行了初始化,所以不会被默认初始化为空指针、
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* 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、
//整形指针变量ptr1和ptr2指向的在堆区上的内存空间都只有1个int类型的大小,故在释放时,只需要使用delete加整型指针变量即可,具体如下所示:
delete ptr1;
delete ptr2;
//2、
//整形指针变量ptr3,ptr5和ptr7指向的在堆区上的内存空间都有多个int类型的大小,故在释放时,需要使用delete[]加整型指针变量,只要是在堆区上
//动态开辟了多个内存空间,则在释放时,都必须要在操作符delete的后面加上[],而[]中都不需要加上确定的数字,具体如下所示:
delete[] ptr3;
delete[] ptr5;
delete[] ptr7;
//上述两种情况下对应的方法要匹配,不匹配的话不一定会报错,可能报错,也可能不会报错,不匹配不一定会造成内存泄漏,可能会泄漏,也有可能不会泄漏,
//但可能会造成崩溃,所以一定要匹配起来,不匹配不一定只会对在堆区上动态开辟的多个内存空间中的第一个内存空间进行释放,具体在后面进行阐述、
}
//在 C++ 中,建议使用 new 操作符,而不建议使用 malloc 函数、
//1、
//对于内置类型而言,使用malloc函数和new操作符,除了用法不同之外,没有任何区别,两者都只是在堆区上动态开辟内存空间,都没有对其在堆区上动态开辟的内存空间进行初始化,故他们在堆区上动态开辟的内存空间中所存储的数据均是
//随机值,使用free函数和delete操作符,除了用法之外,没有任何区别,两者都只是对在堆区上动态开辟的内存空间进行释放,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,
//这是因为new操作符使用失败时,不会返回空指针,而是抛异常,具体在后面进行阐述、
//2、
//对于自定义类型而言,则两者会有差别,使用malloc函数只会在堆区上动态开辟内存空间,但不会对该内存空间进行初始化,故该内存空间中所存储的数据均是随机值,而这里的数据也就是自定义的类的类体中的类成员变量,而使用new操作符
//则会先在堆区上动态开辟内存空间然后再会自动调用该自定义类型对应的构造函数对其类体中的类成员变量进行初始化,使用free函数,只会释放在堆区上动态开辟的内存空间,而使用delete操作符的话,他会先自动调用该自定义类型对应的
//析构函数进行资源的清理,然后再会释放在堆区上动态开辟的内存空间,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,这是因为new操作符使用失败时,不会返回空指针,而是抛
//异常,具体在后面进行阐述、
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;
// delete 操作符操作自定义类型,会先自动调用该自定义类型对应的析构函数进行资源的清理,然后再释放在堆区上动态开辟的内存空间、
return 0;
}
//在 C++ 中,建议使用 new 操作符,而不建议使用 malloc 函数、
//1、
//对于内置类型而言,使用malloc函数和new操作符,除了用法不同之外,没有任何区别,两者都只是在堆区上动态开辟内存空间,都没有对其在堆区上动态开辟的内存空间进行初始化,故他们在堆区上动态开辟的内存空间中所存储的数据均是
//随机值,使用free函数和delete操作符,除了用法之外,没有任何区别,两者都只是对在堆区上动态开辟的内存空间进行释放,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,
//这是因为new操作符使用失败时,不会返回空指针,而是抛异常,具体在后面进行阐述、
//2、
//对于自定义类型而言,则两者会有差别,使用malloc函数只会在堆区上动态开辟内存空间,但不会对该内存空间进行初始化,故该内存空间中所存储的数据均是随机值,而这里的数据也就是自定义的类的类体中的类成员变量,而使用new操作符
//则会先在堆区上动态开辟内存空间然后再会自动调用该自定义类型对应的构造函数对其类体中的类成员变量进行初始化,使用free函数,只会释放在堆区上动态开辟的内存空间,而使用delete操作符的话,他会先自动调用该自定义类型对应的
//析构函数进行资源的清理,然后再会释放在堆区上动态开辟的内存空间,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,这是因为new操作符使用失败时,不会返回空指针,而是抛
//异常,具体在后面进行阐述、
#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;
}
//对于内置类型而言,使用malloc函数和new操作符,除了用法不同之外,没有任何区别,两者都只是在堆区上动态开辟内存空间,都没有对其在堆区上动态开辟的内存空间进行初始化,故他们在堆区上动态开辟的内存空间中所存储的数据均是
//随机值,使用free函数和delete操作符,除了用法之外,没有任何区别,两者都只是对在堆区上动态开辟的内存空间进行释放,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,
//这是因为new操作符使用失败时,不会返回空指针,而是抛异常,具体在后面进行阐述、
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;
}
//对于内置类型而言,使用malloc函数和new操作符,除了用法不同之外,没有任何区别,两者都只是在堆区上动态开辟内存空间,都没有对其在堆区上动态开辟的内存空间进行初始化,故他们在堆区上动态开辟的内存空间中所存储的数据均是
//随机值,使用free函数和delete操作符,除了用法之外,没有任何区别,两者都只是对在堆区上动态开辟的内存空间进行释放,其次,使用malloc函数的话,需要对指针变量进行断言检查,但使用new操作符时,不需要对指针变量进行断言检查,
//这是因为new操作符使用失败时,不会返回空指针,而是抛异常,具体在后面进行阐述、
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
//1、
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
assert(ps1);
//对于自定义类型而言,使用malloc函数只会在堆区上动态开辟内存空间,但不会对该内存空间进行初始化,故该内存空间中所存储的数据均是随机值,而这里的数据也就是自定义的类的类体中的类成员变量、
//此时在此处是没办法直接对在堆区上动态开辟的内存空间中的数据,即自定义的类的类体中的类成员变量进行初始化的,这是因为,类体中的类成员变量都是私有的,在类外不能直接访问,只能调用该自定义
//的类的类体中的构造函数进行初始化,但是,目前没办法显式的调用构造函数,在后面所讲的定位new是可以显式调用构造函数的,具体在后面进行阐述,则此时,类体中的类成员变量 _a 就是野指针、
//2、
Stack* ps2 = new Stack;
//对于自定义类型而言,使用new操作符则会先在堆区上动态开辟内存空间,然后再自动调用该自定义类型对应的构造函数对其类体中的类成员变量进行初始化、
free(ps1);
//使用free函数,只会释放在堆区上动态开辟的内存空间、
delete ps2;
//使用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;
void* p1 = malloc(1024 * 1024 * 1024); //1G,等于10亿个字节、
//assert(p1);
cout << p1 << endl; //00000000
//char* p2 = new char[1024 * 1024 * 1024]; //1G,等于10亿个字节、
//cout << (void*)p2 << endl; //为了打印出地址,则进行强制类型转换、
//此时, 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; 从而打印出有效的地址、
}
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;
//
// char* p2 = new char[1024 * 1024 * 1024]; //1G,等于10亿个字节、
// cout << (void*)p2 << endl; //为了打印出地址,则进行强制类型转换、
// //若此处的 new 操作符使用失败,则会抛异常,那么此时就不会执行代码: cout << (void*)p2 << endl; 而是直接进入异常捕获的代码中,即,catch所在的语句中、
// //若此处的 new 操作符使用成功,则不会抛异常,那么此时就会执行代码: cout << (void*)p2 << endl; 从而打印出有效的地址、
//}
//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 进行的重载,即,并不是 操作符?new?重载函数,操作符?delete?重载函数,这是两个全局库函数(标准库中的函数),分别是两个全局库函数的函数名、
??operator new 与 operator delete?全局库函数的源码如下:
//1、
//库函数 operator new 的源码:
/*
operator new: 该库函数实际通过 malloc 库函数来动态申请堆区上的内存空间,当 malloc 库函数动态申请堆区上的内存空间成功时,
对于 malloc 库函数而言,则会返回一个有效的地址,同时对于 operator new 库函数而言,也是返回一个有效的地址,该有效的地址就是由 malloc 库函数返回的那个有效的地址,
当动态申请堆区上的内存空间失败时,尝试执行堆区上的内存空间不足应对措施,如果用户设置了应对措施,则继续申请,否则,malloc 库函数就会返回空指针,而对于
operator new 库函数而言则会抛异常、
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) // _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 库函数、
#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的功能是一样的,都只是在堆区上动态开辟内存空间,都不会对在堆区上动态开辟的内存空间进行初始化,故,他们在堆区上动态开辟的内存空间中所存储的
//数据都是随机值,而他们在堆区上动态开辟的内存空间中所存储的数据,都是该 Stack类 类体中的类成员变量,只不过这里不需要进行检查失败,要注意,检查失败的方式不仅仅只有断言一种方式,还有
//其他的方式,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 库函数对在堆区上动态开辟的内存空间进行释放、
st3 = nullptr;
return 0;
}
4.2、专属的非静态类成员函数?operator new?与?operator delete
注意:
????????内存池是一种内存(一般指的是堆区上的内存空间)分配方式,又被称为固定大小区块规划,通常我们习惯直接使用操作符new、malloc库函数等API在堆区上动态申请分配内存空间,这样做的缺点在于:由于在堆区上所动态申请的内存块的大小不定,且malloc库函数的调用非常慢,一般来说,少量的操作不会造成什么影响,故当
频繁
的在堆区上动态开辟内存空间时,就会造成堆区上产生大量的内存碎片并且降低性能,使得内存分配效率较低,内存池则是在真正使用堆区上的内存空间之前,先动态申请分配一定数量的、大小相等(一般情况下)的内存块留作备用,然后再对这些内存块进行管理,当有新的内存需求时,就从内存池中分出一部分内存块,若从内存池中分出来的一部分内存块不够使用,则再继续向内存池申请新的内存块进行使用,这样做的一个显著优点是,使得内存分配效率得到提升,在内核中有不少地方内存(堆区上)分配不允许失败,作为一个在这些情况下确保成功分配堆区上内存空间的方式,内核开发者创建了一个已知为内存池(或者是 "mempool" )的抽象,一个内存池真实的只是一类后备缓存,它尽力一直保持一个空闲内存列表给紧急时使用、
????????下面代码演示了,针对链表的节点 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 、
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 非静态类成员函数的话,则此时
//调用的就是标准库中的库函数 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操作的是自定义类型,故
//则会先调用 operator new 全局库函数,然后该全局库函数再去调用 malloc 库函数,而 malloc 库函数的调用本身就特别慢,再加上此处又是
//频繁调用 malloc 库函数,从而会导致内存空间分配效率较低,那么能不能不直接在堆区上进行动态开辟内存空间呢,也即,能不能不直接使用
//malloc 库函数就能分配到堆区上的内存空间呢?
//此时就引出了池化技术,如:内存池,进程池等等,使用的均是池化技术,在此我们使用的是内存池、
//现在如果需要得到堆区上的内存空间,就不再直接向堆区进行动态申请,而是找内存池申请,此时,释放的就是由内存池分配的堆区上的内存空间,
//而不是由堆区分配的堆区上的内存空间,释放的由内存池分配的堆区上的内存空间,就会再返回到内存池中,供下一次向内存池申请堆区上的内存空
//间使用,此时,就不需要再调用 malloc 库函数,注意:只要使用 malloc/realloc/calloc 等库函数就相当于是直接向堆区动态申请内存空间,除此之外
//operator new 库函数也会调用 malloc 库函数,也相当于是直接向堆区动态申请内存空间,若在 ListNode类 的类体中显式的实现出非静态类成员函数
//operator new 和 operator dalete 的话,此时,再执行代码: ListNode* newnode = new ListNode; 就会直接去执行 ListNode类 类体中的
//operator new 非静态类成员函数,就不再调用 malloc 库函数了,所以,即使频繁多次调用代码: 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、先调用 operator new?全局库函数(不考虑内存池)在堆区上动态申请内存空间、
2、在堆区上动态申请的内存空间上自动调用该自定义类型对应的构造函数从而对该自定义的类的类体中的类成员变量进行初始化、
操作符 delete 的底层实现原理
1、先自动调用该自定义类型对应的析构函数,完成自定义类型的对象中资源的清理工作、
2、然后再调用 operator delete?全局库函数(不考虑内存池)来释放对象在堆区上动态开辟的内存空间、
操作符 new T[N] 的底层实现原理
1、先调用 operator new[ ]?全局库函数,在全局库函数 operator new[ ] 中实际调用了N次?operator new?全局库函数(不考虑内存池),在堆区上动态开辟N个自定义类型的内存空间,此时,可以理解为: operator new[ ]?全局库函数封装了?operator new?全局库函数(不考虑内存池)、
2、然后再自动调用 N次 该自定义类型对应的构造函数从而对该自定义类型的类的类体中的类成员变量进行初始化操作、
操作符 delete[ ] 的底层实现原理
1、先自动调用 N次 该自定义类型对应的析构函数,完成自定义类型的 N个 对象中资源清理的工作、
2、然后再调用 operator delete[ ]?全局库函数释放在堆区上动态开辟的内存空间,实际在 operator delete[ ]?全局库函数中调用了N次 operator delete?全局库函数(不考虑内存池)来释放在堆区上动态开辟的内存空间,此时,可以理解为: operator delete[ ]?全局库函数封装了?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; //不会报错,不会造成内存泄漏、
//此时,操作符 new 和 delete 操作的均是内置类型,所以不会自动调用某一个自定义类型对应的构造函数和析构函数,那么此时,就算没有匹配
//也不会报错,并没有造成内存泄漏,能够成功编译、
//2、
Stack* st = new Stack[10];
//错误用法:
//delete st; //会报错,会造成内存泄漏、
//正确用法:
delete[] st; //不会报错,不会造成内存泄漏、
//此时,操作符 new 和 delete 操作的均是自定义类型,此时,则会自动调用 10次 该自定义类型对应的构造函数,但是如果只自动调用 1次 该自定义类型
//对应的析构函数,则会报错,这是因为没有完全析构,其次还跟他们底层实现原理中的偏移指针有关,就不再深入研究,记住即可,所以此处必须要自动调用
// 10次 该自定义类型对应的析构函数才可以,才不会报错,才不会造成内存泄漏、
return 0;
}
6、定位 new 表达式(placement-new)
?????
??定位 new 表达式是在向 内存池或者堆区 已经动态申请出来了堆区上的一块内存空间后,在该内存空间中再显式的调用某一个自定义的类的类体中的构造函数去初始化该类类体中的类成员变量、
使用格式:
new (place_address) type? 或者? new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是该自定义类型的初始化列表、
使用场景:
????????当使用操作符?new?操作自定义类型时,会先调用?operator?new?库函数,该库函数再调用?malloc?库函数,当?malloc?库函数调用成功后,则?operator new?库函数就会返回一个有效的地址,然后会自动调用该自定义类型对应的构造函数,从而对该自定义的类的类体中的类成员变量进行初始化操作,但是,当使用操作符?new?操作自定义类型时,如果该自定义的类的类体中显式实现了非静态类成员函数?operator?new?的话,那么编译器先调用非静态类成员函数?operator?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));
//此时不能直接对该自定义的类的类体中的类成员变量进行初始化操作,这是因为,Stack类的类体中的类成员变量
//均是私有的,在类外不能直接访问,所以在不使用友元的情况下,只能通过调用该自定义类型的类的类体中的构造
//函数去对该 Stack类 类体中的类成员变量进行初始化操作,但由于构造函数又不能显式的去调用,那么有没有方
//式去显式的调用构造函数呢,答案是有的,这就是:定位new表达式,如下所示:
//使用场景:
//1、
Stack* obj = (Stack*)operator new(sizeof(Stack));
new(obj)Stack;
delete obj;
obj = nullptr;
2、
//Stack* obj = (Stack*)operator new(sizeof(Stack));
//new(obj)Stack(10);
//delete obj;
//obj = nullptr;
上述使用场景2中等价于以下代码:
Stack* obj = new Stack(10);
return 0;
}
7、常见面试题
7.1?库函数malloc和宏函数free与操作符new和delete的区别
共同点是:
都是从堆上动态申请内存空间,并且都需要用户手动释放该内存空间、
不同的地方是:
1、
malloc?
和?
free?都
是函数,而?
new?
和?
delete?都
是操作符、
2、
malloc 库函数在堆区上动态
申请的内置类型和自定义类型的内存空间都不会初始化,而操作符
new 在堆区上动态申请的自定义类型的内存空间可以通过编译器自动调用该自定义类型的类的类体中的构造函数去
初始化该在堆区上动态开辟的自定义类型的内存空间、
3、
malloc?库函数在堆区上动态
申请内存空间时,需要手动计算该内存空间大小并传递,而操作符
new?
只需在其后面跟上内存空间的类型即可,如果需要在堆区上动态开辟多个内存空间,在[ ]中指出内存空间的个数即可、
4、
malloc?库函数
的返回类型为?
void*,
在使用时必须强制类型转换,而操作符?
new?在使用时则
不需要强制类型转换,因为操作符?
new?
后面跟的就是内存空间的类型、
5、
malloc?库函数在堆区上动态
申请内存空间失败时,返回的是空指针
,因此使用时必须判空,而操作符?
new?在使用时不需要进行判空
,但是操作符?
new?在使用时
需要捕获异常、
6、
在堆区上动态
申请自定义类型的内存空间时,库函数?
malloc 和宏函数 free?
只会在堆区上动态申请和释放该自定义类型的内存空间,不会自动调用该自定义类型对应的构造函数与析构函数,而操作符?
new?
在堆区上动态开辟自定义类型的内存空间后会编译器会自动调用该自定义类型对应的构造函数来完成在堆区上动态开辟的自定义类型的内存空间的初始化操作,操作符?delete?
在释放堆区上动态开辟的自定义类型的内存空间前编译器会自动调用该自定义类型对应的析构函数完成堆区上动态开辟的该自定义类型的内存空间中资源的清理、
7.2、内存泄漏
7.2.1、什么是内存泄漏,内存泄漏的危害
内存泄漏
主要
是指在
堆区
上的内存空间的泄漏、
什么是内存泄漏:
????????
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况,内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费,内存泄漏是,指向该内存的指针丢了,内存永远不会丢、
内存泄漏的危害:
????????在智能指针中会再次进行阐述,普通的内存泄漏没有事,就比如当前我们在VS2013编译器下写的代码,即使造成内存泄漏也不会出现问题,因为当进程结束后,该被泄漏的内存就会自动还给操作系统,在虚拟内存与物理内存的映射中会进行阐述,即,当进程在正常结束时,会把所有的资源都清理掉,即把所有的内存空间(主要是指堆区上的内存空间)都会自动还给操作系统,这种机制就是为了防止内存泄漏的问题,但是如果使用的设备上,堆区的内存空间很小时,若出现内存泄漏问题,就比较麻烦,或者长期运行的程序中,若出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死,
7.2.2 内存泄漏分类
C/C++
程序中一般我们关心
两种
方面的内存泄漏:
堆区内存泄漏(Heap leak)
????????堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new?
等从堆中分配的一块内存,用完后必须通过调用相应的 free?
或者?
delete?释放
掉,假设程序的设计错误导致这部分内存没有被释放且指向这部分内存的指针丢失找不到,那么以后这部分空间将无法再被释放掉,从而导致无法再次被使用,就会产生Heap Leak、
系统资源泄漏
????????指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定、
7.2.4、如何避免内存泄漏
1、
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放、
ps
:这个理想状态,但是如果碰上异常时,就算注意释放了,还是可能会出问题,需要下一条智能指针来管理才有保证、
2、
采用
RAII
思想或者智能指针来管理资源、
3、
有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项、
4、
出问题了使用内存泄漏工具检测,
ps
:不过很多工具都不够靠谱,或者收费昂贵、
总结一下:
????????内存泄漏非常常见,解决方案分为两种: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;
return 0;
}
|