c虽然也提供了动态内存分配的相关函数,但与c++还是有很大的不同。这里先介绍c++的内存分配,再与c的进行对比。
c++使用new来进行内存申请,new运算符是基于类型进行内存分配的,即程序员要告诉new,要为哪种数据类型分配内存,数据类型可以是基本数据类型,也可以是自己定义的结构或者是类。返回一个指向已分配内存块的指针,我们要做的就是把这个内存块的地址赋给一个对应类型的指针。
typeName * pointerName = new typeName;
指针本身只标注了内存块的起始位置,将他赋给对应类型的指针变量之后就能得到类型和长度信息,从而访问内存上的数据。与变量在定义时从栈分配的内存不同,new分配的内存属于堆或自由存储区
在某些情况下,计算机的内存不足导致无法分配内存,无法满足new的请求,这时new会返回一个异常(之后将会讨论异常处理机制),目前仅需知道c++有检测并处理内存分配失败的工具。
当使用完内存时,需要将内存空间还给系统,使用delete来释放用new申请的内存
注意:只有使用new分配的内存区域才能被delete释放,普通的变量声明分配的内存无法被delete释放,另外,不要释放已经释放的内存地址,这样的行为是未定义的。因此最好不要创建指向同一地址的两个不同指针以减小重复释放的概率。
使用new创建动态数组
这里的数组就不一定是基本类型数组,对于大型结构来说,使用new创建动态结构数组比较方便。对于数组内存有两种方式,在编译时为数组分配内存即数组大小确定,称为静态联编;而对于在程序运行过程中分配数组内存的方式称为动态联编。
int * pt = new int[10];
delete [] pt;
上面的例子就用new分配了一个长度是10的int数组,并且返回了首元素地址。程序中需要程序员自己去跟踪数组元素的数目,并且保证在释放内存时使用的指针指向内存块的首地址。
delete []表示将整个数组释放,事实上程序的确跟踪了数组分配的内存量,以便在delete[]时能够正确地释放内存,但这个信息不是共享的,比如不能用sizeof确定动态数组的字节数。
仍需注意,new和delete必须一一对应使用,用new []分配的内存也必须使用delete[]释放。
new的其他注意点
一般来说,编译器将程序的内存划分为三块:静态区保存自动全局变量和static变量(包括局部static变量);栈区保存局部变量,在代码块执行完后即释放;堆区保存动态存储变量,由malloc系列函数或new操作符请求的变量,在手动释放之前一直存在。
在了解了c++的内存模型后,在使用new时应注意的是,new本身分配的内存需要手动释放,但若将其返回的内存块地址赋给一个局部指针变量,这个局部指针变量在离开其作用之后就会被释放,若没有将内存块释放或用其他指针变量继承该地址,会导致不可预见的内存泄漏。
new的更多特性
1、使用new运算符进行初始化:new分配空间之后可以对对应类型进行初始化,如:
int *pi = new int {6};//*pi set to 6
double *pd = new double {88.90};
...
struct where{double x; double y;double z};
where *pt1 = new where {2.33,6.66 2.13};//also can be used to iitilize the menber of struct or class or array
此外,当用new对类对象分配空间是会调用构造函数,同理,在用delete释放时会调用类的析构函数,总结来说:
new做的事:
1.调用operator new分配空间
2.调用构造函数初始化空间
delete做的事:
1.调用析构函数清理对象
2.调用operator delete释放空间
new[N]做的事:
1.调用operator new分配空间
2.调用N次构造函数分别初始化每个对象
delete做的事:
1.调用N次析构函数清理对象
2.调用operator delete释放空间
2、new失败时不会返回空指针,而是会引发异常std::bad_alloc
3、new系列运算符的内部定义:包括分配函数和释放函数
void * operator new(std::size_t);//used by new
void * operator new[](std::size_t);//used by new[]
void * operator delete(void *);
void * operator delete[](void *);
在运行如下代码时,实际上是经过转换后调用对应的函数:
int * pi = new int;
//转换后
int * pi = new(sizeof(int));
int *pt = new int[40];
//转换后
int * pt = new(40*sizeof(int));
delete pt;
//转换后
delete (pt);
在c++中,这些函数是可替换的,故可以对new和delete提供替换函数,以满足特殊的内存分配方式。可定义作用域为类的new函数,在对该类进行new操作的时候就会调用为该类特别设计的函数。
4、定位new运算符
通常new是在堆中寻找一个满足要求的内存块,此外还有一个变体,可以指定要使用的内存位置,称为定位new运算符。可以利用这个特性设置专门的内存管理规程或者在处理需要使用特定地址访问的硬件或是在特定位置创建对象。
这种特性需要包含头文件new,其有四种用法
#include<new>
struct chaff
{
char dross[20];
int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
chaff *pt1, *pt2;
int *p3, *p4;
//regular form
p1 = new chaff;
p3 = new int[20];
//placement form
p2 = new (buffer1) chaff;//place structure in buffer1
p4 = new (buffer2) int[20];//place array in buffer 2
定位new运算符不会检查当前位置是否被使用,但会检查数组下标越界,即不允许分配超过预分配缓冲区大小的对象:
#include<bits/stdc++.h>
using namespace std;
int main()
{
char buffer1[20];
int * pt = new (buffer1) int[310] {1,2,3};
cout << pt;
system("pause");
return 0;
}
以上代码能通过编译,但会抛出运行时错误。
定位new的内部原理:
定位new直接接受一个地址并在其上分配内存而不会跟踪其是否已经被使用,也就是对同一个地址多次使用定位new就会覆盖原有的数据。
其次其实现是对指针进行强制类型转换,虽然地址相同,但缓冲区的指针是char*类型,new对其进行强制类型转换为void*再返回以便赋给任意一种类型的指针
由于定位new不会跟踪内存的使用情况,因此不能使用delete释放被定位new分配的内存块,delete只适用于堆中的动态内存。如果缓冲区是用new分配的动态内存,则可以使用delete释放整个缓冲区内存块。
不能使用delete释放也意味着只能通过手动地删除被创建的对象,按照与创建时相反的顺序删除对象,以防止后面创建的对象依赖前一个对象导致的错误。另外,系统不会自动调用析构函数进行清理,因此需要显式地调用析构函数来清理。最后再将整个缓冲区释放。
对比new和malloc
1.它们都是动态管理内存的入口。
2.malloc/free是c/c++标准库的函数,new/delete是c++操作符。
3.malloc/free只是动态分配/释放内存空间。而new/delete出来分配空间还会调用构造函数和析构函数进行初始化与清理。
4.malloc/free需要手动计算类型大小且会返回void*, new/delete可以自己计算类型的大小,返回对应类型的指针。
关于c的动态内存分配详见另一个笔记。事实上new、delet是对malloc、free的封装