共同点:
由于malloc和new都是在堆上分配的空间,无法自动释放,需要调用free/delete释放空间。malloc分配的空间用free释放,而new分配的空间用delete释放。
malloc()到底从哪里得到了内存空间?
答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。
1-调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。
2-然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。
3-接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
调用free函数时,它将用户释放的内存块连接到空闲链上。
4-到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
申请的时候实际上占用的内存要比申请的大。因为超出的空间是用来记录对这块内存的管理信息 大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。
例如:free的源代码:
struct mem_control_block {
int is_available; //这是一个标记
int size; //这是实际空间的大小
}
void free(void *ptr)
{
struct mem_control_block *free;
free = ptr - sizeof(struct mem_control_block);
free->is_available = 1;
return;
}
堆内存分配(引用自百度百科)
操作系统有一个记录空闲内存地址的链表;
1.当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,
然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
2.对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
3.另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。堆内存是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。
4.堆内存的大小受限于计算机系统中有效的虚拟内存。由此可见,堆内存获得的空间比较灵活,也比较大。
堆内存是由new分配的内存,一般速度比较慢,而且容易产生内存碎片。
不同点:
1. malloc是库函数,在C++程序中,要用到malloc函数,必须包含malloc.h。
malloc函数的原型是:void *malloc( size_t size ),其中,size是分配的空间大小(字节),
失败返回NULL。
该函数返回一个void类型的指针,因此,在用malloc函数时,往往需要进行数据类型的强制转换:
int *pm = (int*)malloc(2);
myclass *pO1 = (myclass *)malloc(2); //myclass是一个自定义的类
new是C++的一个运算符,
new分配失败时,抛出std::bad_alloc异常一般用法如下:
int *p = new int[10];
myclass *pO = new myclass;
同理delete会调用类的析构函数,free也不会调用析构函数
2. 在分配空间时,malloc只是分配了指定大小的空间,并没有初始化,没有调用类的构造函数,
所以
得到的一片新内存中,其值将是随机的。
而new不仅分配了空间,同时进行了初始化工作,调用了该对象的构造函数。
new初始化的方式:显示初始化和隐式初始化
显示初始化:
nt *pi=new int(100); //指针pi所指向的对象
初始化
为100
string *ps=new string(10,’9’);//*ps 为“9999999999”
如果不提供显示初始化,对于类类型,用该类的默认构造函数初始化;而内置类型的对象则无初始化。
int *pi=new int( );//初始化为0
int *pi=new int;//pi 指向一个没有初始化的int
string *ps=new string( );//初始化为空字符串 (对于提供了默认构造函数的类类型,没有必要对其对象进行值初始化)
在delete之后,重设指针的值
delete p; //执行完该语句后,p变成了不确定的指针,在很多机器上,尽管p值没有明确定义,但仍然存放了它之前所指对象的地址,
然后p所指向的内存已经被释放了,所以p不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。
一旦删除了指针所指的对象,立即将指针置为0,这样就非常清楚的指明指针不再指向任何对象。(零值指针:int *ip=0;)
问题1:既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?
这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
--------------------------------------------------------------------------------------------------------------------------------------------
扩充知识:《计算机操作系统》内存管理中的动态分区分配
连续分配方式:为一个用户程序分配一个连续的内存空间。
其中的动态分区分配:根据进程的实际需要,动态地为之分配内存空间。
1.分区分配中的数据结构:
空闲分区表;
空闲分区链
2.分区分配算法(从空闲分区表或空闲分区链中选出一分区分配)
1) 首次适应算法(first fit)
2) 循环首次适应算法
3) 最佳适应算法
4) 最坏适应算法
5) 快速适应算法(将空闲分区根据其容量大小进行分类,对于每一类具有
相同容量的所有空闲分区,单独设立一个空闲分区链表)
1) 分配内存
流程图如下:
注意:将分配区的首址返回给调用者
2) 回收内存
4种情况:
(1) 回收区与插入点的前一个空闲分区F1相邻接;
(2) 回收分区与插入点的后一空闲分区F2相邻接;
(3) 回收区同时与插入点的前、后两个分区邻接;
(4) 回收区既不与 F1邻接,又不与 F2邻接
-------------------------------------------------------------------------------------------------------------------------
深入new 操作符(new operator)
原链接:http://www.cnblogs.com/xue-wen/archive/2009/11/07/1597983.html
new的过程
当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:
获得一块内存空间、
调用构造函数、
返回正确的指针。当然,如果我们创建的是简单类型的变量,那么第二步会被省略。假如我们定义了如下一个类A:
class A
{
int i;
public:
A(int _i) :i(_i*_i) {}
void Say() { printf("i=%dn", i); }
};
// 调用new:
A* pa = new A(3);
那么上述动态创建一个对象的过程大致相当于以下三句话(只是大致上):
A* pa = (A*)malloc(sizeof(A));
pa->A::A(3);
return pa;
虽然从效果上看,这三句话也得到了一个有效的指向堆上的A对象的指针pa。
但区别在于,
当malloc失败时,它不会调用分配内存失败处理程序new_handler,而使用new的话会的。因此我们还是要尽可能的使用new,除非有一些特殊的需求。
new的三种形态
到目前为止,本文所提到的new都是指的“new operator”或称为“new expression”,但事实上在C++中一提到new,至少可能代表以下三种含义:
new operator、operator new、placement new
。
new operator就是我们平时所使用的new,其行为就是前面所说的三个步骤,我们不能更改它。
但具体到某一步骤中的行为,如果它不满足我们的具体要求 时,我们是有可能更改它的。三个步骤中最后一步只是简单的做一个指针的类型转换,没什么可说的,并且在编译出的代码中也并不需要这种转换,但前两步就有些内容了。
new第一步:operator new
new operator的第一步分配内存,
实际上是通过调用operator new来完成的,这里的new实际上是像加减乘除一样的操作符,因此也是可以重载的。
operator new默认情况下首先调用分配内存的代码,尝试得到一段堆上的空间,如果成功就返回,如果失败,则转而去调用一个new_hander,然后继续重复前面过程
。如果我们对这个过程不满意,就可以重载operator new,来设置我们希望的行为。例如:
class A
{
public:
void* operator new(size_t size)
{
printf( "operator new calledn");
return ::operator new(size);
}
};
A* a = new A();
这里通过::operator new调用了原有的全局的new,实现了在分配内存之前输出一句话。全局的operator new也是可以重载的,但这样一来就不能再递归的使用new来分配内存,而只能使用malloc了:
void* operator new(size_t size)
{
printf( "global newn");
return malloc(size);
}
相应的,delete也有delete operator和operator delete之分,后者也是可以重载的。并且,如果重载了operator new,就应该也相应的重载operator delete,这是良好的编程习惯。
operator new的默认行为是请求分配内存,如果成功则返回此内存地址,如果失败则调用一个new_handler,然后再重复此过程。于是,想要从operator new的执行过程中返回,则必然需要满足下列条件之一:
l
分配内存成功
l
new_handler中抛出bad_alloc异常
l new_handler中调用exit()或类似的函数,使程序结束
于是,我们可以假设默认情况下operator new的行为是这样的:
void* operator new(size_t size)
{
void* p = null
while(!(p = malloc(size)))
{
if(null == new_handler)
throw bad_alloc();
try
{
new_handler();
}
catch(bad_alloc e)
{
throw e;
}
catch(…)
{}
}
return p;
}
在默认情况下,new_handler的行为是抛出一个bad_alloc异常,因此 上述循环只会执行一次。但如果我们不希望使用默认行为,可以自定义一个new_handler,并使用std::set_new_handler函数使其 生效。在自定义的new_handler中,我们可以抛出异常,可以结束程序,也可以运行一些代码使得有可能有内存被空闲出来,从而下一次分配时也许会成 功,也可以通过set_new_handler来安装另一个可能更有效的new_handler。例如:
void MyNewHandler()
{
printf(“New handler called!n”);
throw std::bad_alloc();
}
std::set_new_handler(MyNewHandler);
这里new_handler程序在抛出异常之前会输出一句话。
new第二步:placement new
new的第三种形态——
placement new是用来实现定位构造的,因此可以实现new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象,这有点类似于前面代码中的“p->A::A(3);”这句话,但这并不是一个标准的写法,正确的写法是使用placement new:
#include <new.h>
void main()
{
char s[sizeof(A)];
A* p = (A*)s;
new(p) A(3);
//p->A::A(3);
p->Say();
}
对头文件<new>或<new.h>的引用是必须的,这样才可以使用placement new。
1]这里“new(p) A(3)”这种奇怪的写法便是placement new了,它实现了在指定内存地址上用指定类型的构造函数来构造一个对象的功能,后面A(3)就是对构造函数的显式调用。注:指定的地址既可以是栈,又可以是堆,placement对此不加区分。
2】但是,除非特别必要,不要直接使用placement new ,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步骤而已。使用new operator地编译器会自动生成对placement new的调用的代码,因此也会相应的生成使用delete时调用析构函数的代码。如果是像上面那样在栈上使用了placement new,则必须手工调用析构函数,这也是显式调用析构函数的唯一情况:
p->~A();
STL的内存分配与traits技巧
在《STL原码剖析》一书中详细分析了SGI STL的内存分配器的行为。与直接使用new operator不同的是,SGI STL并不依赖C++默认的内存分配方式,而是使用一套自行实现的方案。首先SGI STL将可用内存整块的分配,使之成为当前进程可用的内存,当程序中确实需要分配内存时,先从这些已请求好的大内存块中尝试取得内存,如果失败的话再尝试整块的分配大内存。这种做法有效的避免了大量内存碎片的出现,提高了内存管理效率。
为了实现这种方式,STL使用了placement new,通过在自己管理的内存空间上使用placement new来构造对象,以达到原有new operator所具有的功能。
template <class T1, class T2>
inline void construct(T1* p, const T2& value)
{
new(p) T1(value);
}
此函数接收一个已构造的对象,通过拷贝构造的方式在给定的内存地址p上构造一个新对象,代码中后半截T1(value)便是placement new语法中调用构造函数的写法,如果传入的对象value正是所要求的类型T1,那么这里就相当于调用拷贝构造函数。类似的,因使用了placement new,编译器不会自动产生调用析构函数的代码,需要手工的实现:
template <class T>
inline void destory(T* pointer)
{
pointer->~T();
}
与此同时,STL中还有一个接收两个迭代器的destory版本,可将某容器上指定范 围内的对象全部销毁。典型的实现方式就是通过一个循环来对此范围内的对象逐一调用析构函数。如果所传入的对象是非简单类型,这样做是必要的,但如果传入的 是简单类型,或者根本没有必要调用析构函数的自定义类型(例如只包含数个int成员的结构体),那么再逐一调用析构函数是没有必要的,也浪费了时间。为 此,STL使用了一种称为“type traits”的技巧,在编译器就判断出所传入的类型是否需要调用析构函数:
template <class ForwardIterator>
inline void destory(ForwardIterator first, ForwardIterator last)
{
__destory(first, last, value_type(first));
}
其中value_type()用于取出迭代器所指向的对象的类型信息,于是:
template<class ForwardIterator, class T>
inline void __destory(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destory_aux(first, last, trivial_destructor());
}
// 如果需要调用析构函数:
template<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for(; first < last; ++first)
destory(&*first); // 因first是迭代器,*first取出其真正内容,然后再用&取地址
}
//如果不需要,就什么也不做:
tempalte<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __true_type)
{}
因上述函数全都是inline的,所以多层的函数调用并不会对性能造成影响,最终编译 的结果根据具体的类型就只是一个for循环或者什么都没有。这里的关键在于__type_traits<T>这个模板类上,它根据不同的T类 型定义出不同的has_trivial_destructor的结果,如果T是简单类型,就定义为__true_type类型,否则就定义为 __false_type类型。其中__true_type、__false_type只不过是两个没有任何内容的类,对程序的执行结果没有什么意义,但 在编译器看来它对模板如何特化就具有非常重要的指导意义了,正如上面代码所示的那样。__type_traits<T>也是特化了的一系列模 板类:
struct __true_type {};
struct __false_type {};
template <class T>
struct __type_traits
{
public:
typedef __false _type has_trivial_destructor;
……
};
template<>
// 模板特化
struct __type_traits<int>
//int 的特化版本
{
public:
typedef __true_type has_trivial_destructor;
……
};
…… //其他简单类型的特化版本
如果要把一个自定义的类型MyClass也定义为不调用析构函数,只需要相应的定义__type_traits<T>的一个特化版本即可:
template<>
struct __type_traits<MyClass>
{
public:
typedef __true_type has_trivial_destructor;
……
};
模板是比较高级的C++编程技巧,模板特化、模板偏特化就更是技巧性很强的东 西,STL中的type_traits充分借助模板特化的功能,实现了在程序编译期通过编译器来决定为每一处调用使用哪个特化版本,于是在不增加编程复杂 性的前提下大大提高了程序的运行效率。更详细的内容可参考《STL源码剖析》第二、三章中的相关内容。
带有“[]”的new和delete
我们经常会通过new来动态创建一个数组,例如:
char* s = new char[100];
……
delete s;
严格的说,上述代码是不正确的,因为我们在分配内存时使用的是new[],而并不是简单的new,但释放内存时却用的是delete。
正确的写法是使用delete[]:delete[] s;
但是,上述错误的代码似乎也能编译执行,并不会带来什么错误。
事实上,new与new[]、delete与delete[]是有区别的,特别是当用来操作复杂类型时。假如针对一个我们自定义的类MyClass使用new[]:
MyClass* p = new MyClass[10];
上述代码的结果是在堆上分配了10个连续的MyClass实例,并且已经对它们依次调 用了构造函数,于是我们得到了10个可用的对象。
换句话说,使用 这种写法时
MyClass必须拥有不带参数的构造函数,否则会发现编译期错误,因为编译器无法调用有参数的构造函数。
当这样构造成功后,我们可以再将其释放,释放时使用delete[]:
delete[] p;
当我们对动态分配的数组调用delete[]时,其行为根据所申请的变量类型会有所不 同。
如果p指向简单类型,如int、char等,其结果只不过是这块内存被回收,此时使用delete[]与delete没有区别,
但如果p指向的是复杂 类型,delete[]会针对动态分配得到的每个对象调用析构函数,然后再释放内存。
delete[]是如何知道要为多少个对象调用析构函数的?
对一个复杂类型使用new[]来动态分配数组时其真正的行为:
1.它分配了比预期多4个字节的内存并用它来保存对象的个数
是否在前面添加4个字节,只取决于这个类是否需 要调用构造函数/或析构函数。
(自己的理解需要调用的情况:类成员有构造函数,继承自有构造函数的基类,虚函数,虚基类)
2.然后对于后面每一块空间使用placement new来调用无参构造函数(这也就解释了为什么这种情况下类必须有无参构造函数)
3.最后再将首地址返回。
在默认情况下operator new[]与operator new的行为是相同的,operator delete[]与operator delete也是,
不同的是new operator与new[] operator、delete operator与delete[] operator------可以重载带有和不带有“[]”的operator new和delete
类似的,动态申请简单类型的数组时,也不会多申请4个字节。于是在这两种情况下,释放内存时使用delete或delete[]都可以,但为养成良好的习惯,我们还是应该注意只要是动态分配的数组,释放时就使用delete[]。
释放内存时如何知道长度?
既然申请无需调用析构函数的类或简单类型的数组时并没有记录 个数信息,那么operator delete,或更直接的说free()是如何来回收这块内存的呢?这就要研究malloc()返回的内存的结构了。与new[]类似的是,实际上在 malloc()申请内存时也多申请了数个字节的内容,只不过这与所申请的变量的类型没有任何关系,我们从调用malloc时所传入的参数也可以理解这一 点——它只接收了要申请的内存的长度,并不关系这块内存用来保存什么类型。
当我们要分配一段内存时,所得的内存地址和上一次的尾地址至少要相距8个字节,这8个字节中的前两个字节记录了一次分配内存的长度信息,后面的六个字节可能与空闲内存链表的信息有关,在翻译内存时用来提供必要的信息。这就 解答了前面提出的问题,原来C/C++在分配内存时已经记录了足够充分的信息用于回收内存,