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;
}

    运行的结果如下:

OW~C{Q48Q)V5ND[M$62RN@2

    相应的,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源码解析》

posted @ 2012-03-10 17:37  云端小飞象cg  阅读(2471)  评论(5编辑  收藏  举报