malloc,free,new,delete解析(原)
使用C/C++的苦逼娃们经常深陷内存越界错误,资源泄漏错误等等问题,而且这样的惨剧每时每刻都在这个世界上重复的发生着。其实,我也是苦逼娃!也为了个神马越界的东西debug了整个下午过,为此也想砸电脑过。最后想想,其实不是C/C++苦逼,是我们的不小心而造成如此的苦逼。罪过,罪过。。。
在C语言库中,提供了这么两个函数,malloc和free,分别用于执行动态内存分配和释放。两个函数的声明如下:
void* malloc(size_t size);
void free(void *pointer);
从外观上看,malloc函数的参数是接受需要分配的内存字节数,如果内存能够满足请求量,那么将会返回:指向被分配的内存块起始位置的指针(引用自《C和指针》中的表述)。而且,这块内存是一块连续的内存。够爽吧!如果分配不成功的话,那么就返回null指针。例如:
struct Node
{
data_type value;
struct Node *pre_node;
struct Node *next_node;
};
typedef struct Node *List;
List newnode = NULL;
newnode = (List)malloc(sizeof(struct Node)); // 分配了Node的空间,返回了相应的指针
if (!newnode) // 为了防止空指针的存在,要检查是否分配成功
{
printf("内存分配不成功!\n");
exit(1);
}
在这里,我们又一次看到了void指针的功效。对于不同类型的指针转换,void指针都能应付自如,更重要的是,我们不用为每种类型都特化一个内存分配版本。这个会累死的。还有,对于分配出来的指针,我们要检查它是否为空,这是为什吗?因为,对于空指针的操作将会带来未知的错误,所以错误摁在萌芽阶段是我们的义务和责任。
内存的东西,向来都是要有借有还。有malloc就会有free。free函数的作用就是负责释放申请来的内存的。这个指向释放的内存指针是有讲究的,你不能释放申请来的一小部分的区域。例如:
int *pi;
pi = (int *)malloc(sizeof(int)*10);
free(pi + 5); // 这里就苦逼了!
其次,较为常见失误在于,我们对申请的内存指针进行操作,操作完之后,对其进行释放。此时,释放不成功的原理和上面这个例子一样,释放了部分的内存,而不是释放全部内存。一般的做法是设一个临时变量保存malloc来的指针,在释放的时候,就释放这个指针所以的内存区域,这样就不会出错了。
关于内存越界,这是一个很普遍的问题。这个只能你去亲自把握,记清楚你申请了多少,其边界在哪里,不要出错就埋怨电脑。
说了malloc和free,那就进入今天的主题new和delete了。
想必我们都写过这样的代码吧。
string *str = new string("memory alloc!");
我们都知道new在堆上创建了一个string类型对象,这个看似一个动作的事情却暗地包含了三件事:获得一块内存空间、调用构造函数、返回正确的指针。相当于下面:
void *pstr = operator new(sizeof(string));
new(pstr) string("memory alloc!");
string *str = static_cast<string*>(pstr);
你应该会心里犯嘀咕,卧槽,怎么出现了这么多的环节。其实呢,new可分为这么三种情形:new operator(我们平常用的new), operator new(用来分配内存的),placement new(构造对象的)。当然,你要用placement new的时候,你要加入<new>的头函数。
⑴ new operator
new operator的第一步分配内存实际上是通过调用operator new来完成的,这里的new是可以重载的。operator new默认情况下首先调用分配内存的代码,尝试得到一段堆上的空间,如果成功就返回,如果失败,则转而去调用一个new_hander,然后继续重复前面过程。
#include <iostream>
using namespace std;
class MyClass {
private:
public:
MyClass() { };
void* operator new(size_t size)
{
cout << "调用了operator new" << endl;
return ::operator new(size);
}
};
int main()
{
MyClass *a = new MyClass();
return 0;
}
运行的结果如下:
相应的,delete也有delete operator和operator delete之分,后者也是可以重载的。并且,如果重载了operator new,就应该也相应的重载operator delete。
⑵ placement new
placement new的作用是用来实现定位构造对象,可以说相当于new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象。其用法:
void construct(pointer __p, const _Tp& __val) { new(__p) _Tp(__val); }
其格式是这样的:new(指针指向的内存地址) 类型(值)。对照格式,可以这么说是在__p上构造一个_Tp类型的,值为__val。
如果是像上面那样在栈上使用了placement new,则必须手工调用析构函数:
void destroy(pointer __p) { __p->~_Tp(); }
当觉得默认的new operator对内存的管理不能满足我们的需求,我们可以自己手工打造内存管理,此时的placement new就相当有用了。以上两段代码源于sgi stl的alloc中,就说明问题了。
⑶ new operator 和 operator new 的取舍
两个关键词很容易让人迷糊,两者又都是骨肉相连的状态,取舍有点不易。对于取舍的评判,我挪用了《more effective C++》中的观点:“如果你想产生对象在heap上,那么就用new operator,它不但分配内存,而且为该对象调用了构造函数;如果你只打算分配内存,那么就用operator new ,只分配内存,其余的事情都没做。当然,使用operator new 后续的对象构造也要跟上,不然但分配内存也就没有意义了。”
总结,在这篇文章中,介绍了malloc,free,new,delete的使用和一些心得体会。当然,还有许多的内容没有做相应的介绍,比如new_handle等内容。抱歉,抱歉!
关于内存分配的问题,你还可以参考:内存分配机制。这篇文章介绍了C/C++内存分配的知识。
参考文献
1. 《C语言程序设计》
2. 《C和指针》
3. 《C专家编程》
4. 《more effective C++》
5. 《STL源码解析》