1. C/C++内存分布
这里要说的第一个问题是C/C++是怎么对内存进行管理的呢?可以先想想从用途和储存角度看程序中主要有那些类型的数据?第一部分有我们现在用,用了一会就销毁了的局部数据;还有静态数据和全局数据,它们的特点是生命周期在全局一直可以用,只是全局哪里都能用,静态只能在一个局部域或当前文件用;还有常数据;还有动态中申请的数据。程序中需要存储一些数据,以上是数据的分类。下面再简单了解一个东西:平时自己写的程序是存在磁盘上的,把编译好的程序运行起来的本质是以一个进程角度在操作系统上运行。程序运行起来后要对数据的存区域进行划分,划分的区域整体而言每个进程叫做进程地址空间,进程地址空间就要对这些数据进行存储,它会完成划分区域:

最上面是高地址,给Linux内核用的。下面分别是栈,堆,静态区,常量区,以上是从语言角度进行划分的,从操作系统角度静态区和常量区分别叫数据段和代码段,栈向下生长,堆向上生长。局部数据存在栈,因为局部数据通常定义在函数里面,每次调用函数要建立栈帧,建立栈帧的本质是为了即用即销毁,存储局部数据。有些地方也要静态数据和全局数据,要不断用,所以它们存在静态区。常量和代码(符号)在常量区,动态申请的数据在堆。有这么多区域是因为程序有很多这样的要求,了解了这些我们看看下面的题:

其他的都好说,这里解释一下char2,char2本质是一个数组,相当于把代码段的数据拷贝了一份放到了数组中,所以char2在栈中,解引用后也在栈中,pchar3是直接指向了代码段。
2.C语言中动态内存管理方式:malloc/calloc/realloc/free

简单说malloc就是直接开相应的空间,calloc就是直接开的同时初始化,realloc会对已有的空间进行相关的扩容。那需要free p2吗?不需要,因为用realloc扩容时有这样的规定:1.原有的空间足够就原地扩容。2.原有的空间不足够就在新的位置开空间,把p2的内容拷贝过去再释放p2原来的空间,返回p3,安全起见把p2置为空就行。
3. C++内存管理方式
上述C语言的那一套C++可以继续用,下面再来看看C++的内存管理方式,它通过new和delete操作符进行动态内存管理。

比如动态开4个大小的空间,C语言是用malloc,C++是用new,它是一个操作符,用法是new这个关键字+类型就可以了,不用了就delete就可以了。这里发现C++的方式比较简洁,那如果开多个变量:

C语言用malloc开,C++用new int[10]开,代表开10个int的数组,不用时要释放数组。C++这里有个特点是可以初始化:

p5和p4的区别是p5是(),p4是[],p5是开一个int,10是在初始化,p4是开10个int的数组。那单个空间可以初始化,数组可以初始化吗?也是可以的:

这里初始化只初始化了前3个值,后面的值默认是0。那为什么弄一个new出来?难道是为了初始化起来方便一些吗?其实祖师爷早就看不惯C语言的malloc了:

在单链表中C语言要个结点需要写一个BuyListNode函数,函数里面再malloc开空间。C++这里想的是能不能向构造函数那样定义出来就初始化了,所以C++new的时候还能顺便调一下构造函数。所以new和malloc对于内置类型除了用法上区别不大,真正的区别在自定义类型,malloc是纯粹的开空间,new除了开空间还可以通过调构造函数来初始化。因此祖师爷弄出来new并不是因为好用,而是要作很大价值,所以C++在这的核心价值之一是:

针对自定义类型,如果继续用malloc和free,是可以从中感受到本质区别的,那本质区别是什么呢?通过调试来看:

p1这除了开空间没有初始化,也没有调什么函数;p2这除了开空间还有初始化,且调用了构造函数。free就是单纯把p1指向的空间释放了;delete也把p2指向的空间释放了,但还调用了析构函数。基于这样的原因从此以后申请内存用new更好,它的优点有使用上更简洁,内置类型和自定义类型它都可以搞定。再来看一下下一个不同:

如果这里是多个对象呢?运行结果发现,如果在这new了一个10个对象的数组,那我会针对这10个对象调用10次构造函数,delete会针对这里调用10次析构函数。这时有人可能会问p2那里new的时候给值调用构造函数初始化了,那p6这里new的时候没有给值有没有初始化?

调式时看到也有初始化,因为A中有缺省参数,提供了默认构造。

如果没有默认构造函数怎么办?编译时报错说没有默认构造函数可以用。

(改为4个方便演示)在没有默认构造函数的情况下,这里如果给值初始化是可以的,这里至少是隐式类型的转化。实在不行还可以这样给:

就是直接给A类型的几个匿名对象,这样给主要是防止构造函数是多参数的情况:

那new这里会不会优化呢?这里是调用构造+拷贝构造还是直接调构造呢?运行后会发现编译器还是很智能的(new开空间时调构造,A(1,1)传值又调拷贝构造),按理来说构造+拷贝构造,但这里只有构造,还是优化了。之前说内置类型时,new一个4个大小的数组,只初始化两个后面的会默认初始化为0,那像下图这样可不可以呢?

显然不可以,因为第四个不给它怎么知道初始化为什么,但如果有默认构造函数:

此时只给3个最后一个不给也是可以的。

之前说过一定要匹配使用,内置类型不匹配用可能暂时发现不了什么问题,但上图这里发现不匹配用程序崩了,因此千万不要交错使用。
4.operator new与operator delete函数
下一部分来看看new和delete的底层,要理解底层首先要学的第一个东西是operator new和operator delete,这个又是什么呢?第一眼看上去感觉它是new和delete的运算符重载,但注意它不是我们这里直接的运算符重载,它是一个全局函数,是库里面的函数。

上图是库里面的内容,不认识的不用管,这里就简单看看。先看operator new,它的参数是size,返回值是void*,发现和malloc很像,它的底层是去调用malloc,如果malloc失败它会做一件事情叫抛异常。再看operator delete,中间有很多东西,也会检查异常,但里面调了一个_free_dbg,实际平时用的free是个宏函数,free这个宏函数调的也是_free_dbg。那operator new和operator delete到底是什么呢?

首先如上图我们可以用,这是库里面的函数,所以可以直接使用。调式时发现operator new和operator delete用法和malloc和free是一样的。那C++里面弄出来这个是用来干嘛的呢?从用法上来说和malloc和free是一样的,但它产生的价值并不是给我们直接用,而是为下一个部分做准备,那为谁做准备呢?先来细细分析一下:new和malloc的区别是什么?重大区别之一是new要调构造函数。除了调用构造函数要先开空间,那new是如何开空间的呢?开空间都是在系统堆中开,有现呈的肯定去用现呈的,所以开空间就是用malloc。但是祖师爷在这面临第二个问题:malloc有个点不符合C++需求,malloc失败后返回空,而面向对象的语言失败后不希望用返回值,更建议用抛异常,异常抛出后是需要捕获的。因为C语言遇到异常一般返回一个编号,比如失败返回-1,文件读取失败返回对应的编号错误等,所以做好直接返回到底出了什么错误。下面演示一下它们失败后处理情况是怎么处理的:

比如上图看看p1在这申请了多少次1mb的内存,通过调试+任务管理器观察,从7.1变到1912左右失败,失败了返回空循环停止。再换为new看一看:

new失败时期望抛异常,同意通过调试+任务管理器,最终可以看到出现未经处理的异常。异常有个特点是必须要被捕获:

要用到try和catch(这里暂时了解一下就行),抛异常就失败的那次直接跳到catch里运行的地方e.what(),然后捕获异常就能拿到信息,这里看到出现bad allocation申请内存失败。理解了这块我们就明白:C++它期望出错了是用抛异常的方式处理错误,C语言是返回错误码。由于这样一系列原因,这时祖师爷在设计new的时候开空间就不能直接调用malloc了,因为C++要求申请空间失败了要抛异常,那这时怎么办呢?

再来看看库里实现的,直接调malloc不行,但申请内存的那一下确实可以找malloc,它在这做的事是operator new就是对malloc的封装,如果失败后抛异常。可以这样理解,new在这申请内存调的是operator new,operator new是给new用的,operator new里面又去调用malloc,然后再调用构造函数。delete是先调用析构函数清理资源,然后调用operator delete,operator delete里面去调用free。

为了更清楚上述过程下面来看一下汇编:

可以看到operator new就是调用operator new这个函数,new的话中间做了其他操作可以不管,但有两个我们想看到的核心操作就是调用了operator new和构造函数。再看delete,A::scalar不是直接调析构,可能某些原因封装了一层,里面看到先调用了析构再调用了operator delete。这就是new和delete的底层原理,也就是说祖师爷也想直接用malloc和free,奈何它们不是很符合我们需求,所以就用operator new和operator delete封装了。operator new和operator delete实际不是给我们用的,而是给new和delete用的。下面看个样例感受一下这样的顺序在什么地方非常有价值:

以前就是Stack st这样定义,它的构造函数会初始化,析构函数会释放资源。假设现在需要申请一个堆上的栈对象,这里用malloc不合适,malloc没有出初始化,free也没有清理资源。

如果按照上图一样用new此时有两层概念:

new Stack时先调用operator new,operator new再调用malloc在堆上开12字节大小的空间,然后调用构造函数来初始化。delete时先调用析构函数清理资源,再调用operator delete,operator delete再调用free释放堆上开的空间。
5.定位new表达式(placement-new)
还有一个需要了解的是定位new,也叫做placement-new,这个不是new,是new的其他一些操作,它可以对一块已有的空间调用构造函数。

上图这有一块空间是malloc的,它没有调用构造函数,定位new可做的一件事情是显示调用构造函数。我们以前的构造函数都是自动调用的,比如实例化定义对象,new的时候自动调用。p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行。那想显示调用怎么办呢?

就是new(p1)A,这就是显示调用构造函数,再显示调用析构函数再free,这样模拟的就是new的功能。定位new的用法是提前开好空间,我要显示调用构造函数,对已有的空间调构造函数。new(p1)A中括号里给的指针,括号外给的构造函数,有需要后面还可以跟参数new(p1)A(1)。这里析构不显示调用p1也不会自动去调用析构,因为自定义类型对象才会自动调用构造和析构,p1是内置类型,所以不自动调用。这个知识目前感觉没有什么用,那什么情况下有用呢?计算机行业有个玩法叫池化技术,就是用个池子把东西装起来,目的是为了提高效率。

假设我们住山顶上,山底有一条小溪,我们想喝水的时候从山上跑下来喝口水再回去,想洗手的时候再从山上跑下来洗完手回去。这里就发现需要用水的时候从山顶到山底有极大浪费,所以有个方法是建个风车,在离山顶近的地方建立个蓄水池,通过风车把水放池子里面。这样再需要水的时候就去池子弄水,这样距离近了,整体而言浪费少了。所以为了提高效率,我们以后可能会见到很多池子:内存池、线程池、连接池等。以后实践中也有这样的特点:

OS管理的内存上有块区域是堆,直接去找OS的堆要内存是一件相对麻烦的事情,可以理解为堆像小溪一样。现在有个应用要频繁申请和释放内存,那我每次从堆申请很麻烦。因此建立个池子,提前申请一大块内存放池子里面,要内存时可以不去堆而直接去池子要,池子中没有了继续从堆获取放池子里,这样就提高了效率,因为从池子取的距离比从堆取的距离近。内存池不是直接new和malloc的,可能提供了一个单独接口,从内存池申请的只有内存,是没有用构造函数初始化的:比如链表中以前开个结点就Node* n1 = new Node(1),为了节省效率找内存池可能就Node* n2 = pool.alloc(sizeof(Node)),但这块空间没有调用构造函数,此时就用定位new显示调用构造函数。
6. malloc/free和new/delete的区别
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在释放空间前会调用析构函数完成空间中资源的清理。
7.内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不 是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费。
下面说说关于内存泄漏的危害有什么:

这里申请了1个G的内存,运行发现无限打印乱码,原因在于cout不能打印char*的指针。因为cout通过函数重载识别类型,int*会识别为指针,char*会被识别为字符串,字符串通过\0截止。这里申请内存没有初始化,此时就一直打印找\0。

如果给点值初始化带上\0发现就可以停止了。那想强制按指针打印怎么办?

按上图方式写就可以了。这里没有写delete,按理来说运行几次内存就没有了,但系统怎么不挂?因为OS担心我们不释放,一个进程结束OS会自动回收资源。所以我们平常写的普通程序,有内存泄漏影响也不是很大,进程正常结束会释放资源。但长期运行的程序,内存泄漏危害很大,比如游戏服务,电商服务,出现内存泄漏会导致响应越来越慢,最终卡死。