C和C++内存管理
本节内容
- C/C++内存分布
- C语言中动态内存管理方式
- C++中动态内存管理
- operator new与operator delete函数
- new和delete的实现原理
- 定位new表达式(placement-new)
- 常见面试题
C/C++内存分布
- 内存映射段里面包括我们在运行程序是时所需要的第三方库,在用到它的时候,需要先把他加载到内存里面。内存映射段,在栈和堆的中间
- 图上说的是栈是向下增长的,栈是向上增长的,但是不知道是从高地址向低地址还是从低地址到高地址,所以我们下去需要了解一下到底是从高到低,还是从低到高
- 上图的蓝色空间表示已经被使用了,空白的空间表明其还没有被使用而已
为什么要对内存进行划分
- 是因为为了让我们使用内存使用起来方便,从而对内存进行划分,如果不划分区域的话,那么将来进行管理的话,就会非常的不方便
#include<iostream>
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";
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在哪里?___C _ staticGlobalVar在哪里?___C_
// staticVar在哪里?__ C__ localVar在哪里?__A __
// num1 在哪里?__A__
// char2在哪里?__A__ * char2在哪里?A___
// pChar3在哪里?___A_ * pChar3在哪里?D____
// ptr1在哪里?___A_ * ptr1在哪里?_B___
// 2. 填空题:
// sizeof(num1) = __40__;
//sizeof(char2) = ____5; strlen(char2) = ___4_;
//sizeof(pChar3) = ____4; strlen(pChar3) = ___4_;
//sizeof(ptr1) = ____4;
- 数据段里面变量的生命周期和函数的生命周期是一致的
- phar3只所以在代码段是因为*pchar3所指向的是一个字符串所以在代码段,同时也是一个只读的字符串,所以他的空间处在代码段
- *char2之所以在栈是因为,数组名相当于是首元素的地址,那么,对首元素的地址进行解引用的操作,就会拿到首元素的值,局部变量所处在的空间是栈上。
- const常量所处在的空间是代码段
- 开辟的内存空间,那个指针本身的空间在栈上,只有指针指向的空间是处在堆上面的
- 处在代码段的数据是不可以被修改的
- 程序在运行起来之后,操作系统会给我们的程序单独的去划分一块内存空间,操作系统划分的这个空间是虚拟的内存空间,每个程序都有。每个程序的内存空间都是独立的,运行起来都是互相不会影响的。那么内存空间是如何分布的?
- 程序只有在加载在内存里面的时候才是可以运行的,内存首先可以一分为二,一部分是操作系统在运行期间它所需要的空间,另一部分是用户程序运行起来之后他的数据和代码所存放在的位置之处(一部分是内核空间,这部分空概念是给操作系统的,也就是说,用户是不能使用的,不同的操作系统可能给内核划分的空间的大小是不一样的,比如说在vs下4G的内存的话,可能内核空间占用一半,用户可操作的空间也占用一半,当然用户空间的话,用户可以自己去设置)
- 内存布局中的栈区和堆区和数据结构里面所学的栈和堆有什么区别?---->数据结构中所学的栈和堆只是一种数据结构的类型,数据结构中的栈具有先进后出的特性,而在内存布局中的栈是一块具有栈后进先出特性的一块空间而已。数据结构中的堆是用顺序存储方式来维护的二叉树,其具有大小堆之分,主要是看根节点和其孩子结点的大小关系,而在内存布局中的堆,同样的,只是一块内存空间罢了。
- vs编译器在默认情况是是按照32位的方式进行编译的
说明
- 栈又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据。
- 代码段–可执行的代码/只读常量
C语言中动态内存管理方式
malloc/calloc/realloc和free
- 用于申请空间的malloc/calloc/realloc
- 用于释放空间的free
- malloc/calloc/realloc的共同点:开辟的空间都是在堆上的;开辟的空间需要手动的进行空间的释放;在使用的时候需要进行强制类型转换;有可能会申请空间不成功,所以还需要判空;都是C标准库中的函数
- 如果realloc所指向的空间为空的话,那么这个函数的功能类似于malloc,就是申请上size个字节,然后
- 参数分别为元素的个数和一个元素的大小
问题:Realloc函数在调整p所指向空间的大小的时候,他是如何知道到底要将p所指向的空间调大还是调小呢?
C++内存管理方式
- 为什么C语言已经有了一套内存管理方式了,C++还要有一套内存管理方式?
- 麻烦就麻烦再需要手动传入需要开辟空间所占有的字节数,以及需要进行强制类型转化,还需要进行判空的操作,同时需要包含头文件,因为他们都是函数,所以是需要包含头文件的
- C语言内存管理方式在C++中可以继续使用(因为C++是兼容语言的),但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
- 在这里需要注意的是C++中有了自己的内存管理方式new和delete,注意new和delete不是函数而是操作符。
- C++中动态内存管理方式:有new和delete还有new[]和delete[],用来申请单个类型的空间以及用来申请连续的空间
- 注意:new/delete不是函数,而是C++中的关键字
new/delete操作内置类型
- 空间的申请
//new和delete针对内置类型
#include<iostream>
using namespace std;
int main()
{
//申请单个元素的空间
int* p1 = new int; //要new出来什么类型的空间,直接将这种类型跟在new的后面就可以了
//new出来的空间利用指针来进行接收
//new后面跟上的都是类型的名称
//使用这种方法的好处就在于不需要进行强制类型转化
//也不需要用户去计算需要开辟多大内存空间的空间(malloc的缺陷)
int* p2 = new int(10);
//并且用new申请单个空间的话,还可以为这段空间进行初始化的操作
//现在就是申请了一块整形的空间,然后让这个整形的空间的值为10
//也就是说使用new可以初始化成任意你希望它初始化成为的值
//但是在C语言里面是不行的,C语言里面只能初始化成0(calloc函数的功能)
//new可以将空间初始化成为任意的值
//申请连续的空间
int* p3 = new int[10]; //就是开辟了一段10个整形的连续空间
//并且在用new来申请连续空间的时候,可以同时对其进行初始化的操作,比如
int* p4 = new int[4]{ 1,2,3,4 }; //这是完全可以通过编译的
return 0;
}
- 空间的释放,用new申请出来的空间,一定要用delete来进行释放
//new和delete针对内置类型
#include<iostream>
using namespace std;
int main()
{
//申请单个元素的空间
int* p1 = new int; //要new出来什么类型的空间,直接将这种类型跟在new的后面就可以了
//new后面跟上的都是类型的名称
//使用这种方法的好处就在于不需要进行强制类型转化
//也不需要用户去计算需要开辟多大内存空间的空间
int* p2 = new int(10);
//并且用new申请单个空间的话,还可以为这段空间进行初始化的操作
//现在就是申请了一块整形的空间,然后让这个整形的空间的值为10
//也就是说使用new可以初始化成任意你希望它初始化成为的值
//但是在C语言里面是不行的,C语言里面只能初始化成0
//申请连续的空间
int* p3 = new int[10]; //就是开辟了一段10个整形的连续空间
//并且在用new来申请连续空间的时候,可以同时对其进行初始化的操作,比如
int* p4 = new int[4]{ 1,2,3,4 }; //这是完全可以通过编译的
//这个初始化的方法是C++11里面所提供的一个新的特性
//空间的释放
delete p1;
delete p2;
//p1,p2的空间在栈上,但是他们所指向的空间在堆上
//如果delete释放的是一段连续的空间的话,需要用delte[]来进行释放
delete[] p3;
delete[] p4;
//p3的p4的区别就是一个初始化了,一个没有进行初始化
return 0;
}
- 在使用的时候注意匹配就可以了(用哪一种方式进行申请,后面采用对应的方式进行空间的释放就可以了)
new和delete针对内置类型自定义类型
#include<iostream>
using namespace std;
class Test
{
public:
//构造函数
Test()
{
cout << "Test()" << this << endl; //打印当前构造的是哪个对象
}
//析构函数
~Test()
{
cout << "~Test()" << this << endl; //打印当前构造的是哪个对象
}
private:
int _t;
};
void TestNewDelete()
{
Test* pt1 = new Test; //new申请出来的空间不需要进行判空,大多数情况下都是可以保证申请空间成功的
delete pt1;
Test* pt2 = new Test[10]; //创建了10个对象,就会调用10此构造函数
delete[] pt2;
}
int main()
{
//用new来申请自定义类型的空间
TestNewDelete();
return 0;
}
- new会申请空间,调用构造函数,delete要释放空间,调用析构函数
- 而malloc只是去申请空间,不会调用构造函数,同时free也不会去调用析构函数,没有调用构造函数,就说明没有创建对象出来,所以不能调用类中的成员函数,因为没有对象
- 先创建的对象,最后被释放
- Test1内容
- Test2内容—在调试时走到ptr2的时候,用new来创建一个对象,我们发现了系统调用了构造函数(malloc和new申请的空间都处在堆上)
- delete在释放空间的时候,同时会调用析构函数
- 类体内的内容
- malloc不会调用构造函数,只是单纯的相当于把空间创建好了,new还会同时去调用构造函数,new和malloc申请的空间都处在堆上
- 所以得出原因2的结论:
既然malloc和new的区别只是在于是否调用了构造函数和析构函数,那么C++为什么不对malloc和free进行改写,而是要有一套新的呢
- malloc和free是C标准中的库函数,我不可能说是直接把C语言中的库给改写了,C++是没有权利对他去进行修改的,因此,C++自己只能给出一套出来了。
如果申请空间的格式和释放空间的格式没有匹配使用的话,那么会出现什么样的情况?
-
下面这个代码运行起来之后,并没有发生代码崩溃的问题,内存是否泄露,现在还看不出来
-
想要知道内存是否泄露了,就要知道在编译器如何进行代码内存泄漏的检测。
-
下面的代码经过_CrtDumpMemoryLeaks();第三方库检测之后,发现代码没有崩溃,也没有发生内存泄露的问题,所以结论就是对于内置类型,是否匹配使用没有什么影响
#include<iostream>
#include <crtdbg.h>
using namespace std;
void TestFunc()
{
int* p1 = (int*)malloc(sizeof(int));
delete p1;
int* p2 = (int*)malloc(sizeof(int));
delete[] p2;
int* p3 = new int;
free(p3);
int* p4 = new int;
delete[] p4;
int* p5 = new int[10];
free(p5);
int* p6 = new int[10];
delete p6;
//如果把这句delete去掉,就会出现下面的这种报错信息
}
int main()
{
TestFunc();
_CrtDumpMemoryLeaks(); //用于检测内存是否发生泄漏的方法
return 0;
}
-
也就是说,发生了内存泄漏了
-
对于自定义类型来说,有可能造成内存泄漏也有可能造成代码的崩溃
#include<iostream>
using namespace std;
class Test
{
public:
Test()
{
_ptr = new int[10];
}
~Test()
{
delete[] _ptr;
_ptr = nullptr;
}
private:
int* _ptr;
};
void TestFunc()
{
//崩溃
//代码崩溃的原因在于,使用malloc进行申请空间的时候,malloc是没有调用构造函数的
//但是下面的delete是会去调用析构函数的,那么这时候就没有对象明确的指向,调用析构函数的话
//是一定会崩溃的
//所以类中ptr的this指针是存在的,但是this指针所指向的内容是随机值,所以不能对其进行释放
//一释放就会发生崩溃的情况
//p1因为没有调用构造函数,所以p1指向的并不是一个对象,而delete会将p1所指向的空间当成对象释放
//但是现在根本没有对象,所以代码会崩溃
//因为_ptr指针并没有被初始化,所以代码会崩溃
Test* p1 = (Test*)malloc(sizeof(Test));
delete p1;
//崩溃
//道理和上面的是差不多的,同样的delete[]也是会去调用析构函数的
//但是你使用malloc的话,对象其实是没有的,因为malloc不会去调用构造函数的
//没有这个对象还非要去释放的话,那么是一定会发生崩溃的
Test* p2 = (Test*)malloc(sizeof(Test));
delete[] p2;
//内存泄漏
//原因在于用new去申请空间的话,是会去调用构造函数的,那么this指针实际上就是相当于有了
//明确的指向了,但是在释放对象的资源的时候,使用了free
//free是不会去调用析构函数的,所以对象的资源其实是没有被释放掉的
//所以是会造成内存泄漏的
//p3是指向对象的空间,并且这个对面里面其实是有资源的
//free并没有把对象里面的孔吉纳释放掉,所以说会造成内存泄漏的问题
Test* p3 = new Test;
//p3的空间是在栈上的,p3所指向的内容他的空间是在堆上的
free(p3);
_CrtDumpMemoryLeaks();
//这个方法可以用来检测内存是否泄露了
//但是缺点在于,他不会告诉我们,在代码中的哪个位置内存泄漏了
//崩溃
Test* p4 = new Test;
delete[] p4;
//崩溃
Test* p5 = new Test[10];
free(p5);
//崩溃+内存泄漏
Test* p6 = new Test[10];
delete p6;
}
//涉及到[]的位置都发生崩溃了
int main()
{
TestFunc();
return 0;
}
- 10那块空间是没有释放的,所以会造成内存泄漏的问题出现
先开辟空间还是先调用构造函数
- 先去开辟空间,然后在这个空间上去调用构造函数
如果类中,没有涉及到资源的管理,那么在有些情况下释放的时候是不会造成内存泄漏的,但是会崩溃,会不会崩溃只要取决于有没有析构函数
#include<iostream>
#include <crtdbg.h>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test()" << endl;
}
~Test()
{
cout << "~Test():" << this << endl;
}
private:
int _data;
};
void TestFunc()
{
//代码没有任何的问题,原因在于类中只有一个数据成员,而没有设计到资源的管理
//如果类中设计到了资源的管理的话,new和delete必须要匹配使用才可以
Test* p1 = (Test*)malloc(sizeof(Test));
delete p1;
//崩溃
Test* p2 = (Test*)malloc(sizeof(Test));
delete[] p2;
//没有发生内存泄漏
//因为类中没有涉及到资源的管理
//就好比说是日期类,其实日期类是否调用析构函数都是没有什么影响的
Test* p3 = new Test;
free(p3);
_CrtDumpMemoryLeaks();
//崩溃
Test* p4 = new Test;
delete[] p4;
//崩溃
Test* p5 = new Test[10];
free(p5);
//崩溃
Test* p6 = new Test[10];
delete p6;
}
总结
在类中没有涉及到资源管理的时候,有时候会崩溃,有时候是不会崩溃的,比如像下面这样, 就是没有崩溃的,会不会崩溃的本质在于,类里面有没有析构函数
#include<iostream>
#include <crtdbg.h>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test():" << this << endl;
}
//把析构函数屏蔽掉
// ~Test()
// {
// cout << "~Test():" << this << endl;
// }
private:
int _data; //没有涉及到资源的管理
};
void TestFunc()
{
//没有崩溃
Test* p1 = (Test*)malloc(sizeof(Test));
delete p1;
//没有崩溃
Test* p2 = (Test*)malloc(sizeof(Test));
delete[] p2;
//没有崩溃
Test* p3 = new Test;
free(p3);
_CrtDumpMemoryLeaks();
//没有崩溃
Test* p4 = new Test;
delete[] p4;
//没有崩溃
Test* p5 = new Test[10];
free(p5);
//没有崩溃
Test* p6 = new Test[10];
delete p6;
}
int main()
{
TestFunc();
return 0;
}
- operator new
#include<iostream>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test():" << this << endl;
}
// ~Test()
// {
// cout << "~Test():" << this << endl;
// }
private:
int _data;
};
int main()
{
Test* pt = new Test;
//new会先去申请空间,会去调用operator new的方法
//然后去调用构造函数(这也就和前面说的构造函数不负责来开辟空间呼应上了)
//只负责把对象初始化好,不负责去开辟空间
delete pt;
Test* pt = new Test[10];
delete[] pt;
return 0;
}
- operator delete---->不会去直接释放空间,而是在方法中调用free的方法去释放空间
#include<iostream>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test():" << this << endl;
}
// ~Test()
// {
// cout << "~Test():" << this << endl;
// }
private:
int _data;
};
int main()
{
Test* pt = new Test[10];
delete[] pt;
return 0;
}