返回   cpper编程论坛 > Blog > Meta
注册账号 论坛帮助 会员列表 日历事件 搜索 今日新帖 标记版面已读

为这篇文章评分

new和delete Step by Step (1)

发表于 2006-02-02 04:16 AM 作者: Meta
Meta 更新于 2008-03-27 02:53 PM
最近闲着没有事情干, 便对C++中的new和delete进行了一番研究总结. 我会分几个系列来仔细阐述这方面的心得.

谈到new和delete, 不得不谈传统C的malloc和free这2个函数. 我们知道, malloc和free是C的库函数, 其功能就是动态申请和释放堆上面的内存. 至于这块内存曾经是存储过什么类型的数据, 对于他们来说, 都没有意义, 因为他们返回和释放所操控的指针就是void*.

C++引入了new和delete这2个操作符来动态的创建对象. new操作符在对象的创建过程中, 需要做2件事情, 首先是分配对象所需要的内存, 其次是调用相应的构造函数来初始化对象. delete操作符正好相反, 它首先调用相应的析构函数来析构对象, 其次才是释放对象所占用的内存. 那么在C++语言中new和delete这2个操作符本身是固定在语言中, 其做什么是无法改变的, 也就是说你无法改变new和delete操作符做什么. 但是, C++提供了operator new和operator delete这2个函数来给用户进行内存分配和释放的自定义,而且是与类型无关的. C++允许用户自定义new和delete操作符怎么去做上面所提及的那2件事情, 但是你不能改变他们做哪些事情, 这个是已经内建于语言中的, 是无法改变的. 为了更清楚的让大家明白, 请看以下的代码:

代码:
// main.cpp
void* ip;

struct A
{
    void* operator new (size_t size)
    {
        ip = ::operator new(size);
        return ip;
    }
    void* operator new[](size_t size)
    {
        cout << "size = " << size << endl;
        ip = ::operator new[](size);
        return ip;
    }
    ~A()
    {
        cout << "dtor" << endl;
    }
};
然后我们如此调用,
A* pA = new A[10];

这里我想问大家2个问题:
1. pA和ip的值是否相等
2. operator new函数中输出的size, 值是多少.

你可能急着回答说, 这个还不简单, pA和ip当然相等了, 因为他们指向的是A对象数组起始地址;至于size的输出值嘛, 我知道,empty class由于为了区分数组中的每个元素, 因此它的大小一般为1 byte,因此, 此处size的输出值是 10 * 1 = 10. 呵呵, 很不幸, 这两个回答目前这种情况下都是错的.

原因是为什么呢? 下面我来一一解释. 正如我前面所说, new操作符是C++语言内置的, 你不允许去定义它去做什么, 因此它始终必须做的2件事情就是申请内存和调用相应的构造函数对这块内存区域进行初始化. 根据C++标准中的阐述, 任何new表达式的调用, 在申请内存这一步, 都会转换成对operator new(size_t size)或者operator new[](size_t size)的调用. 对于单个对象, size的值就是对象的大小; 而对于数组的大小, 其大小元素的个数*元素的大小 + x, 这个x是为了存储数组的相关信息,而产生的额外开销.因此此处size的真实输出值应当是 sizeof(A) * 10 + x, x的值是机器相关的, 一般是4. 你可能会说, 这额外的信息有什么实际意义呢, 我为什么需要知道数组里面有几个元素?反正都是一块连续的内存区域, 我直接释放就可以了. 呵呵, 别忘了, 有些对象的销毁是需要调用non-trivial的析构函数的, 而non-trivial的析构函数的调用次数是取决于数组中所含有元素的个数的. 由于ip的值是原始内存分配后的起始地址, 包括这段额外的内存开销;但是, 这个内存对于用户来说, 是没有实际意义的. 所以, 返回给用户的地址应当是首个对象的起始地址, 也就是 |pA - ip| = x. 扩展思维一下, 你可以通过以下的方法来取得这 x 个 bytes所存储的值:

int length = *(static_cast); // length的值此处应当为10

当然, 你也可以修改它的值:
*(static_cast) = 20;
修改以后, 你可以发现non-trivial的析构函数被调用了20次.

到这里, 你应当发现了C++编译器背着我们在底层做了多少的事情. 我再进一步问一个问题, 如果这个地方我去除掉析构函数, 这2个问题的答案是否会有变化?

可能的回答是, 哦, 申请分配的依然是对象数组, 所以, 我们亦然需要额外的x字节来存储数组的信息, 所以size的输出值是......

呵呵, 我们要具体问题具体分析. 我刚才说了, 编译器之所以需要知道数组的长度, 是为了决定释放对象数组时所需要调用的non-trivial析构函数的次数. 既然你此处没有显示地声明析构函数, 也就是意味着你除了释放内存以外, 并没有其他的善后工作需要做.注意, 这里编译器会为你自动声明一个trivial的析构函数. 很明显, 编译器的这种优化行为是正确而且具有意义的,我们没有必要存储一些并不会被使用的额外信息.

到现在为止, 我们深入讨论了new和delete在数组方面的实现方式. 下一次, 我会深入讨论new和delete方面的其他知识点.
发表在 C/C++
评论 3 Email文章
评论总数 3

评论

旧
polyrandom 的头像
这本质上是实现相关的。其实究竟是不是有最后的那个优化,每个编译器都不一定一样。包括编译器究竟怎样保存长度信息也是不确定的。唯一可以确定的是,如果有非trivial的 dtor,最后编译器总是会在某些地方做个记录的。曾经有些编译器使用map来记录这类信息,好处就是你用错了delete形式问题也不大,坏处就是滋长了某些程序员的坏习气
发表于 2008-01-18 06:10 PM 作者: polyrandom polyrandom 当前离线
旧
Meta 的头像
对于dtotr, 如果用户没有显示地自定义的话, 编译器是不会自动产生trivial或者non-trivial的dtor吧.
发表于 2008-01-18 06:11 PM 作者: Meta Meta 当前离线
旧
polyrandom 的头像
不一定。如果base class定义了dtor,它总得调用一下
发表于 2008-01-18 06:11 PM 作者: polyrandom polyrandom 当前离线
发表评论 发表评论

所有时间均为格林尼治时间 +9。现在的时间是 12:22 PM


Powered by vBulletin® 版本 3.7.0
版权所有 ©2000 - 2008,Jelsoft Enterprises Ltd.
(C) Copy Right All Right Reserved 2001 - 2007

Search Engine Friendly URLs by vBSEO 3.1.0