先来看下C语言中内存分配的方式(有三种)
- 在静态存储区分配内存:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。
- 全局的未初始化的变量分配在BSS段,全局的已初始化的变量分布在data段即已初始化数据段。这两者都属于从静态存储区分配。
- BBS段是不占用可执行文件空间的,它只记录数据所需空间的大小,其内容由操作系统初始化(清零);
- 而data段却需要占用,其内容由程序初始化。
- 全局的static变量也是存放在数据段,具体放在哪看它有没有初始化。
- 全局的static变量相比于全局变量而言,不同点就是别的文件看不到static变量,注意静态全局变量尽量不要包含在头文件中,它是相对于不同的源文件而言(即如果静态全局变量定义在头文件中,只要源文件包含了这个头文件,还是可以访问到这个静态全局变量的)。
- 而静态局部变量也是会在编译阶段就分配好内存,并且{}块执行完之后,不会被销毁,因为它也是存储在数据段的并不是在栈中,虽然没有被销毁,但也不可用,只能在该函数内部用。理解全局变量与静态全局变量区别的一个例子如下
test1.cpp
int a;
static int b = 6;
test2.cpp
#include <iostream>
using namespace std;
extern int a;//extern表示去别的文件找定义,只在本文件声明
extern int b;
int main()
{
cout << a;//a的输出结果是0,有操作系统初始化(清零)
cout << b;
return 0;
}
上述的程序时会出现编译错误的,报错原因是因为b是一个无法解析的外部命令,即在源文件test1中,b被定义成了static静态全局变量,只在本文件中有效,test2源文件是看不到b这个变量的。
- 在栈上分配内存:栈上存放的是一些临时创建的局部变量,即{}中创建的变量(但不包括static变量,它存储在数据段)。除此以外,函数在被调用时,其参数也会被压入发起调用的栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存是有限的。
- 在堆上分配内存(也叫动态内存分配):程序在运行的时候用malloc和new申请任意多少的内存,程序员需要负责自己在何时用free和delete释放内存。堆的大小不固定,可以伸缩,使用非常的灵活
C语言中的动态内存分配
C语言中使用跟内存申请有关的函数有alloca,malloc,calloc,realloc来开辟空间,先来看看他们的区别
- alloca(很多系统中会宏定义成_alloca使用):向栈申请内存,因此无需释放
- malloc:malloc分配的内存是在堆中的,申请出来的内存没有初始化,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间
- calloc:calloc则将初始化这部分的内存,并将其中的内容全部初始化为0
- realloc:更爱以前分配区的长度(增加或减少)
当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少.但是内存泄漏仅仅指程序在运行时,程序退出时,OS将回收所有的资源。但是一般服务器上的进程不后悔轻易重启程序,所以在malloc之后一定要记得负责free掉。
malloc、calloc、realloc的声明(它们封装在头文件malloc.h中)
void* malloc(unsigned size);
void* calloc(size_t numElements,size_t sizeOfElement);
void* realloc(void* ptr,unsigned newsize);
它们的返回值都是请求出的内存的地址,需要进行指针强转才能转化为指定类型的指针。
- malloc:在内存的动态存储区分配一块长度为size的连续区域,size为需要内存空间的长度,返回该区域的首地址。用malloc分配内存并且用memset初始化的例子如下
int *ptr = (int*)malloc(sizeof(int) * 10);
memset(ptr, 0, sizeof(int) * 10);
- calloc:与malloc相似,参数sizeOfElement为申请地址的单位元素长度,numElements为元素个数,即在内存中申请(numElements*sizeOfElement)字节大小的连续地址空间,并且初始化为0。
int *ptr = (int*)calloc(10, 4);//与上面的malloc开辟的一样,并且都是初始化为0了
- realloc:对给定的指针所指的空间进行扩大或者缩小,参数ptr为原有的地址空间的指针,newsize是重新申请的地址长度。当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,就不变动指针地址;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块(该自由块可能是刚刚free出来的),现存的数据然后就被拷贝至新的位置,并释放原来的指针所指向的那块空间,原来那块空间则放到存储区上。
- realloc的一个参数指针可以为空,那么此时它的作用就和malloc一样。
int *ptr = (int*)calloc(10, sizeof(int));
realloc(ptr, 20*sizeof(int));
//realloc新开辟了20个int大小的空间,前十个int还是0,并没有变值,后十个就是没有初始化的随机值
free掉上面三个函数所申请的空间,被释放的空间通常被送入可用存储区池,可在调用上述三个分配函数时再分配。
- 以上三个alloc函数都会调用sbrk系统调用(所以说,malloc底下还有一层系统调用sbrk),该系统调用用来扩充(或缩小)进程的堆。
- 虽然sbrk系统调用可以扩充或缩小进程的存储空间,但是大多数malloc和free的实现都不减小进程的存储空间。释放的空间可以供以后再分配,但通常将他们保持在malloc池上而不返回给内核。
malloc函数的工作原理
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块(这也就是为什么malloc申请出来的空间为什么会比实际大小要大一点的原因)。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节,用来记录管理信息——分配块的长度、指向下一个分配块的指针等等)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。
常见的内存泄漏
void DoSomething()
{}
void MemoryLeaks()
{
//1.内存分配了忘记释放
int *ptr = (int*)malloc(sizeof(int) * 10);
assert(ptr);
DoSomething();
//这样导致了内存泄漏,但并没有造成程序崩溃
//2.程序逻辑不清,以为释放了,实际上没有释放
int *ptr1 = (int*)malloc(sizeof(int) * 10);
int *ptr2 = (int*)malloc(sizeof(int) * 10);
DoSomething();
ptr1 = ptr2;
free(ptr1);
free(ptr2);//实际两个释放的是同一块内存,会导致程序崩溃,注意一块内存释放过了再释放会导致程序崩溃
//3.程序误操作,将堆破坏
char* ptr3 = (char*)malloc(5);
strcpy(ptr3, "Memory Leaks!");
//实际上申请的空间不够存放这些字符,破坏了堆,之后的空间可能已经在用了
free(ptr3);//再去尝试free掉这一块空间的时候,会导致程序崩溃
//4.释放时传入的地址和申请时的地址不同
int* ptr4 = (int*)malloc(sizeof(int) * 10);
assert(ptr4);
ptr4[0] = 0;
ptr4++;
DoSomething();
free(ptr4);
//此处的ptr4是原先的ptr4++了,这个时候它在free掉10个int类型的空间,最后一个int不是申请出来的,导致程序崩溃
}
C++中动态内存管理
C++中使用new和delete来进行动态内存管理
void test()
{
int* ptr1 = new int;//申请一个int大小的空间,但没有初始化,所以是随机值
int* ptr2 = new int(3);//申请一个int大小的空间,并初始化为3
int* ptr3 = new int[3];//申请三个int大小的空间
delete ptr1;
delete ptr2;
delete[] ptr3;
}
new、delete和new[]、delete[]必须要配对使用,不然可能出现内存泄漏甚至是程序崩溃的问题。
operator new 和 operator delete(C++语言的标准库函数)
void *operator new(size_t); //allocate an object
void *operator delete(void *); //free an object
void *operator new[](size_t); //allocate an array
void *operator delete[](void *); //free an array
注意这些都是个函数!!!并且不是一个重载,都是可以直接使用的
我们通过一个实例来看一下new和delete干了什么
class A
{
private:
int _a;
public:
A(int a)
:_a(a)
{}
};
int main()
{
A* pa = new A(10);
delete pa;
return 0;
}
new干的事
- 调用operator new标准库函数,传入的参数是class A的大小,在上例中是四个字节(int _a),并且返回的是分配的内存的起始地址
- 上面的分配式未初始化的,也是未类型化的,第二步就在这一块原始的内存上对类对象进行初始化,调用的是类对象的构造函数
- 最后一步就是返回新分配并构造好的对象的指针,这里就是返回了pa,pa是一个指向A类型对象的指针
delete干的事
- 先调用pa指向对象的析构函数,
- 再调用operator delete函数来释放该对象的内存,传入的参数为pa的值,也就是该对象的地址
那么如何申请和释放一个数组呢?也就是一块连续的区间
string *psa = new string[10];
int *pia = new int[10];
在申请一个数组的时候用到了new []这个表达式来完成,第一个数组是string类型的,分配了保存对象的空间后,将调用string类型的默认构造函数依次初始化数组中的每个元素;第二个是申请具有内置类型int的数组,分配了存储10个int类型的空间,但并没有初始化
释放的时候用下面的语句
delete [] psa;
delete [] pia;
第一句话对10个string对象分别调用它们的析构函数,然后再释放掉为对象分配的内存空间
第二句话因为内置类型没有析构函数,直接释放为10个int类型数据分配的内存空间
那么在这里我们如何知道psa指向对象的数组的大小?怎么知道调用几次析构函数
C++的做法是在为数组分配空间的时候多分配了4个字节大小的空间,专门保存数组的大小,在delete []时就可以取出这个数
A* pa = new A[3];
从上图可以看出最终返回的是实际指向第一个对象的地址并不是多分配的4个字节大小空间的地址。
这样delete []在做什么就很好解释了
- 那么在delete []的时候,我们给operator delete []传的指针其实并不是第一个对象的地址,而是该地址-4,也就是保存数组空间大小的地址,这个函数会在这个地址第一个四个字节中取出数组的大小,并给这些对象调用它们的析构函数
- 同时,在释放完数组中的对象的时候,也会释放掉最开始的四个字节保存大小的空间
为什么new delete和 new [] delete []要配对使用呢?
介于这两者的工作原理,所以它们需要配对使用?但并不是不配对使用就一定会出问题的。
int *pia = new int[10];
delete []pia;
上面这样写肯定不会出问题,但是把delete []pia换成delete pia呢?
这就涉及到了一个问题。我们知道了new []中要加入4个字节数组大小的原因,是为了要在delete中调用析构函数的时候需要知道数组的大小。
但是如何我们不用调用析构函数呢(如内置类型,int 类型的数组)?我们在new []的时候就没有必要多分配那4个字节,delete []时不用执行第一步先调用对象的析构函数,直接跳到第二部释放掉为int数组分配的空间。如果这里使用detele pia ,那么将调用operator delete函数而不是operator delete []函数,传入的参数就是分配给数组的起始空间,所做的事情就是释放掉这一块内存。
这里使用new []创建对象并能用delete来释放对象的前提是:对象的类型是内置类型或者是无自定义的析构函数的类类型。
如果我们是带有自定义析构函数的类类型,用new []创建,并用delete析构会发生什么
A *pia = new A[10];
delete pia;
那么delete pia做了两件事
- 调用一次pia指向的对象析构函数
- 调用operator delete(pia)释放内存。
出现的问题
- 这里只对数组的第一个类对象调用了析构函数。这样释放的时候,后面两个对象就没有进行析构,会造成内存泄漏。
- 并且!直接释放pia指向内存的空间,程序必然会崩溃。因为分配的空间的起始地址是第一个对象的地址减去4个字节。但是传给operator delete的其实是第一个对象的地址。
new使用delete []为什么会失败
new的时候不会像new []一样多开辟4个字节来存储大小,delete时,会将传入的指针减去4个字节的地址传给operator delete [],这样编译器在进行释放内存的时候,基本上会导致内存越界而失败
分配方式 | 删除方式 | 结果 |
new | delete | 成功 |
new | delete [] | 失败 |
new [] | delete | 内嵌类型和没构造函数的自定义类型成功;自定义类型失败 |
new [] | delete [] | 成功 |
总结operator new和operator delete
- operator new/operator delete operator new[]/operator delete[] 和 malloc/free用法一 样。
- 他们只负责分配空间/释放空间,不会调用对象构造函数/析构函数来初始化/清理对象。
- 实际operator new和operator delete只是malloc和free的一层封装。
new_handler机制
在new中的底层实现如果获取不到更多的内存,会触发new_handler机制,留有一个set_new_handler句柄,看看用户是否设置了这个句柄,如果设置了就去执行。句柄的目的是看看能不能尝试着从操作系统释放点内存,找点内存,如果实在不行就抛出bad_allloc异常;而malloc就没有这种类似的尝试
void out_of_memory()
{
cout << "out of memory!" << endl;
}
int main()
{
//用户自己设置的一个set_new_handler句柄
//在超出内存的时候,会调用set_new_handler中的函数out_of_memory
//并看看能不能从操作系统中释放一些内存来使用
//不能则抛出bad_alloc异常
set_new_handler(out_of_memory);
int *p = new int[536870911];
return 0;
}
placement new(定位new表达式,使用前需要包含<new>头文件)
C++中new有三种用法
- 创建一个对象
-
创建一个对象数组。
-
定位new 表达式
-
前面二个我们之前已经讲过了,下面就来看一看定位new表达式。placement new表达式,在使用时需要我们传入一个指针,此时会在该指针指向的内存空间构造对象,该指针指向的地址可以是堆,可以是栈,也可以是静态存储区。说白了,这个操作符的作用就是创建对象,但是不分配内存,而是在已有的内存块上创建对象。它经常用于需要反复创建和删除的对象,可以降低分配释放内存的性能消耗。
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
// 预分配内存buf
char *buf = new char[sizeof(A) * 10];
// 在buf中创建一个Foo对象
A *pa = new (buf) A;
delete[] buf;
return 0;
}
另外需要注意的一点是,并不存在与定位new表达式匹配的定位delete表达式,即在上面的代码中,并不需要 delete pa,因为实际上定位new表达式并没有分配新的空间,而是用的已有的空间,所以不需要释放,只需要释放已有的空间就可以了
关于malloc/free和new/delete之间的区别和联系
-
它们都是动态管理内存的入口
-
malloc/free是C/C++标准库的函数,new/delete是C++操作符
-
malloc是从堆上开辟空间,而new是从自由存储区开辟。(自由存储区是C++抽象出来的概念,不仅可以是堆,还可以是静态存储区)
-
malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理
-
new可以通过new []来开辟一个连续的存储空间,即开辟一个数组,而malloc只是开辟一个固定大小的空间。
-
malloc/free需要手动设置开辟空间的大小,new只需要对象名,可以自己计算大小
-
malloc的返回值是(void *),在使用时需要强转,调用失败则返回NULL;new的返回值是返回对象的指针,调用失败则抛出异常
-
malloc和free不会调用对象的构造和析构函数,而new和delete会调用对象的构造和析构函数
-
new是类型安全的,而malloc不是,比如:
-
int* p = new float[2]; // 编译时指出错误
-
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
-
-
malloc开辟的内存如果太小,想要换一块大一点的,可以调用relloc实现,但是new没有直观的方法来改变
-
我们可以重载自己的operator new和operator delete,但是不可以重载new/delete/malloc/free
-
new在内存不足时有new_handler机制来尝试申请更多的内存,如果实在不行才会抛出异常,而malloc则没有这种机制