在delete和delete[]背后,编译器的不同对待

本文详细探讨了C++中delete与delete[]的区别及其在不同类型的对象上的应用效果,特别是对于内置类型和string类型的表现差异,并解释了析构函数对二者的影响。

先看下面程序:

复制代码
#include <iostream>
using namespace std;
#include <string>

int main()
{
    int *p=new int[5];
    //delete p;
    delete []p;
    p=NULL;

    //string *p=new string[5];
    //delete p;
    //delete []p;
    //p=NULL;

    return 0;
}
复制代码

对于int类型和string类型,delete p和delete []p编译器(vc6.0)会有不一样的结果,对于int类型delete p和delete []p均可以编译运行,而 对于string类型,delete p运行

后程序会崩溃。

下面是对这个问题网友的意见与我的总结:

(1)delete 与delete[]都会释放所有内存,它们的不同点仅在于,根据得到的类型信息是单个指针还是指向数组的指针,来决定调用析构函数的次数。而编译器对所要删除的那个指针到底是指向的单一对象还是对象数组的判断依据就是"[]"的有无。若有'[]'编译器得到的类型信息就是指向数组的指针,然后调用多次析构函数;若没有‘[]’编译器得到的类型信息就是单个指针,只调用一次析构函数。
其实delete操作本身就是做两件事:
<1> 针对此处内存调用析构函数
<2> 然后释放该处内存。
(详见《Effective C++》(2nd)item 5 或 (3rd)item 16)。

 

(2)vc6.0和vs05中,delete和delete[]语句都是调用operator delete(),在vs05中调试,在跟进operator delete()中,可看到有这样一句“_free_dbg( pUserData, pHead->nBlockUse );”而free要正确工作,有一个必要前提,即传给它的地址确实是当初申请的内存首地址,否则,会出现assert错误。即在重载delete操作符后,某种情况下发现delete操作的指针地址不同于new操作所获取的地址。从而出现assert错误,程序崩溃。

(3)在VC中,对于有显式析构函数的对象, 在分配数组时其前会有一个4字节前缀用来保存数组元素个数. 如果用delete来释放数组, 就会导致释放的内存地址与分配时的内存地址出现4字节偏差, 而导致灾难性的错误.


(4)那么什么情况下,二者操作的指针地址不匹配呢?
经过反复实验测试,发现只要对象类型定义有显式析构函数,那么这个4字节就肯定存在。也就是说,哪怕是我们自定义的一个类类型对象,只要我们没有为这个类定义析构函数,那么这个4字节问题就不会出现;那么该用delete[]的时候,用delete也不会报错;那么该用delete的时候,用delete[]也不会报错,那么...
本论题,由于内置类型没有析构函数,而string有析构函数,这样一种巧合,而把问题局限在了内置类型与自定义类型这样的分类范围上。所以我们这个问题准确的说,应该是显式析构函数的存在影响了delete与delete[]的处理。

 

(5)一句话:该用delete[]时莫省略,这是C++标准。



默认的构造函数和默认的析构函数是个什么样子的?

默认的构造函数里面实际就是分配了内存。
默认的析构函数里面实际就是释放了占用的内存,其他啥也没干了。
那么析构函数里面是怎么知道要释放多少内存的呢?就烦请大家去看看操作系统里的内存是怎么管理的,大致意思是:操作系统维护了一个内存分配表,指示内存的哪块用了,例如0x1000地址开始的100个字节是已经使用的内存,大概记录这种东西。释放内存的时候呢,必须一块一块的删除,例如0x1000开始的内存,不能从0x1001开始删除,必须从0x1000开始删除,因为free()或者delete函数都是给出的内存首地址,然后根据占用长度进行删除。就是这样的。举个例子,free(0x1000),首先要去操作系统的这个内存维护表里去找索引0x1000这个块,如果找到了,就说明给定的这个首地址是正确的,而且正使用着呢,于是就执行释放内存操作,实际就是把这一项从内存维护表里删除,至于操作系统做的其他的操作,大家去查查别的资料。再例如,free(0x1001),如果在内存维护表里面没有找到0x1001的已经使用的项,但存在某个内存使用块的内存范围里包含0x1001,例如(0x1000,100),这个范围就包含了0x1001,那么free操作就会出现运行时错误。如果出现的情况也不属于上面两种情况,free()实际上就什么也不做,也不会出现运行时错误,就返回了。
知道了上面这些,我们继续下面的讨论。
为什么带有析构函数的类,在创建对象数组时,会多出四个字节?为什么不带有析构函数的类,在创建对象数组时,就不需要这个四个字节呢?
因为带有析构函数的类的对象数组,在使用delete[] ptr时需要知道,到底需要执行多少次析构函数,然后再执行释放内存的操作。那么执行析构函数的次数,这个值就放到了申请空间的最最前面的四个字节,紧跟这个四个字节的才是实际的对象数组。但这个时候,编译器做了一下处理,因为这四个字节不属于任何对象,只是编译器知道就好了,因此返回对象数组的指针的时候,不是实际申请的内存空间,而是返回的跳过了开始的四个字节的地址。我在一个地方看到的编译器cookie的概念就包含这四个字节。
而没有析构函数的类的对象数组,它不需要知道要执行多少次析构函数,尽管!尽管课本上都有说有一个默认的析构函数。如果大家看过反汇编代码的话就知道了,这个默认析构函数里面什么都没有,甚至有些编译器直接处理成没有,只是逻辑上存在。因此C++编译器们在处理delete[]的时候,都选择了根本就不执行类的默认析构函数,而只需要执行释放内存的操作。因此它不需要这四个字节。
那么我们接着分析下面的问题就简单多了。
delete i_p; 没有析构函数,仅释放空间
delete []i_p; 没有析构函数,仅释放空间
大家读了上面的如何释放空间之后,就知道,这俩是一样的,但是也跟不同的编译器有关系,有些编译器就要求new[]的东西必须使用delete[]释放,比如VC++2010编译器的默认build选项就会报运行时错误。
string是带有析构函数的
delete s_p; //执行了一次s_p[0]对象的析构函数,析构函数中释放s_p[0]的空间,然后s_p数组的5个内存空间释放了。

delete[] s_p; //执行n次析构函数,每次执行析构函数就释放每个string对象的空间,最后s_p数组的5个内存空间释放了。


原帖如下:

(1)http://topic.youkuaiyun.com/u/20080826/16/0c479e3d-737f-45c7-995e-bd316f5fa166.html

(2)http://topic.youkuaiyun.com/u/20080221/20/7c7093b9-62d7-4d70-b959-3a06e4327d46.html

(3)http://topic.youkuaiyun.com/u/20070712/07/57c7cfc6-7314-400d-86d2-230a72581ea5.html



<think>我们讨论的是编译器默认的operator delete函数的实现方式。根据C++标准,编译器会提供默认版本的operator delete,用于释放由operator new分配的内存。默认的operator delete通常直接调用底层的内存释放函数,如free(在C库中)。但是,具体实现取决于编译器运行环境。 根据引用[3][4]中的信息: - operator delete(void* ptr)用于释放由operator new分配的内存。 - 默认的operator delete会调用free(ptr)来释放内存。 - 另外,还有一个带大小的版本:operator delete(void* ptr, size_t size),但标准规定,如果定义了operator delete(void* ptr, size_t size),则它会被优先用于释放带有大小信息的内存。然而,默认情况下,编译器提供的是operator delete(void* ptr)operator delete[](void* ptr)(用于数组版本)。 注意:我们无法直接看到编译器源代码,但可以模拟其行为。下面是一个模拟默认operator delete实现的示例: ```cpp // 默认的operator delete实现(模拟) void operator delete(void* ptr) noexcept { if (ptr) { free(ptr); // 调用标准库的free函数 } } // 默认的operator delete[]实现(模拟) void operator delete[](void* ptr) noexcept { if (ptr) { free(ptr); } } // 注意:C++17引入了带对齐版本的operator delete,但这里不展开。 ``` 另外,引用[4]提到,我们可以自定义operator newoperator delete,但是编译器会自动在new表达式中调用构造函数,在delete表达式中调用析构函数。因此,自定义的operator delete只需要负责释放内存,而不负责调用析构函数(析构函数由编译器delete表达式中显式调用)。 对于引用[1][2]中提到的一些特殊情况: - 处理0字节的申请:在默认operator new中,如果申请0字节,会将其视为1字节(引用[4]中的自定义operator new示例也遵循了这一点)。同样,在operator delete中,释放空指针是安全的(C++标准规定delete空指针是无操作的)。 - 关于placement newplacement delete:它们的行为较为特殊,但这里我们只讨论普通的operator delete。 另外,引用[5]中的错误信息表明,当链接器找不到operator new或operator delete的实现时,会报错。因此,编译器必须提供这些函数的默认实现,否则程序无法链接。 总结:编译器默认的operator delete函数通常实现如下(以GCC为例,实际在libstdc++中): - 对于非数组形式:调用free(ptr) - 对于数组形式:调用free(ptr)(在operator delete[]中) 注意:默认的operator delete函数被声明为noexcept(在C++11之后)。 下面给出一个模拟的代码示例: ```cpp #include <cstdlib> // 提供free函数 // 全局operator delete(非数组) void operator delete(void* ptr) noexcept { std::free(ptr); } // 全局operator delete(数组) void operator delete[](void* ptr) noexcept { std::free(ptr); } // 带大小的版本(C++14引入,但默认实现可能调用不带大小的版本) void operator delete(void* ptr, std::size_t size) noexcept { (void)size; // 参数size未被使用 std::free(ptr); } void operator delete[](void* ptr, std::size_t size) noexcept { (void)size; std::free(ptr); } ``` 注意:在C++14标准中,引入了带大小的operator delete,但是默认情况下,它只是调用不带大小的版本。编译器可能会利用大小信息进行优化,但是标准库的默认实现通常忽略这个大小。 因此,我们可以认为编译器默认的operator delete函数就是简单地调用free函数来释放内存。 如果用户需要更详细的信息,比如特定编译器(如GCC、Clang、MSVC)的实现,可以进一步查阅相关编译器的源代码。例如,在GCC的libstdc++中,operator delete的实现最终会调用全局的free函数。 最后,请注意,这些全局的operator delete函数可以被用户自定义版本替换。 §§相关问题§§ 1. 如何自定义operator delete函数? 2. 为什么需要带大小的operator delete? 3. 在C++中,delete运算符的执行步骤是怎样的? </think> ### 编译器默认 `operator delete` 的实现原理 在 C++ 中,编译器默认的 `operator delete` 实现遵循标准规范,其核心行为是调用底层内存管理函数释放内存。以下是关键实现细节(基于常见编译器的标准行为): #### 1. **基础实现逻辑** ```cpp // 默认全局 operator delete 伪代码实现 void operator delete(void* ptr) noexcept { if (ptr == nullptr) return; // 删除空指针为安全操作 free(ptr); // 调用 C 标准库的 free 函数 } // 带大小的版本 (C++14 引入) void operator delete(void* ptr, size_t size) noexcept { (void)size; // 参数通常被忽略 operator delete(ptr); // 调用基础版本 } ``` - **空指针处理**:删除空指针是安全的无操作(no-op)[^1][^3] - **核心操作**:调用 `free(ptr)` 释放内存(实际可能使用系统级分配器如 `HeapFree` 或 `munmap`) - **大小参数**:`size_t size` 参数由编译器自动传递(对象原始大小),但默认实现通常忽略该参数 #### 2. **关键特性** | **特性** | 说明 | |------------------------|---------------------------------------------------------------------| | **异常安全** | 声明为 `noexcept`,释放过程绝不抛出异常[^1][^4] | | **内存对齐** | 默认保证基础对齐(通常 8/16 字节),特殊对齐需自定义 | | **对象销毁分离** | 仅释放内存,析构函数由 `delete` 表达式在调用前执行 | | **数组版本** | `operator delete[]` 实现相同,但用于数组对象 | #### 3. **与 `delete` 表达式的交互** 当执行 `delete ptr;` 时: 1. 调用对象的析构函数(用户定义或默认) 2. 调用 `operator delete(ptr, sizeof(Object))` - 编译器自动传递对象大小参数 - 默认实现忽略大小参数 ```mermaid graph TB A[delete ptr] --> B[调用析构函数] B --> C[调用operator delete ptr, size] C --> D[释放内存块] ``` #### 4. **特殊场景处理** - **零字节释放** 即使对象大小为 0,`delete` 仍会调用 `operator delete`(传递 size=0),但释放空指针安全: ```cpp struct Empty {}; Empty* p = new Empty; delete p; // 安全:调用 operator delete(p, sizeof(Empty)=0) ``` - **构造函数异常** 若 `new` 时构造函数抛出异常: ```cpp MyClass* p = new MyClass; // 若构造函数抛出异常 ``` 编译器自动调用匹配的 `operator delete(p, sizeof(MyClass))` 释放内存[^2] #### 5. **编译器实现差异** - **GCC/libstdc++**:通过 `_GLIBCXX_OPERATOR_DELETE` 宏转发到系统分配器 - **Clang/libc++**:调用 `::free()` 或平台特定 API(如 `_aligned_free`) - **MSVC**:调用 Windows API `HeapFree()` 或 `_free_dbg`(调试模式) > **验证示例**(引用[4]扩展): > ```cpp > #include <iostream> > #include <cstdlib> > > // 拦截默认operator delete > void operator delete(void* ptr, size_t size) noexcept { > std::cout << "Deleting " << size << " bytes at " << ptr << "\n"; > free(ptr); // 实际释放 > } > > struct Data { double arr[100]; }; // 大小=800字节 > > int main() { > Data* p = new Data; > delete p; // 输出:Deleting 800 bytes at 0x... > } > ``` > 此代码验证了 size 参数的传递过程。 ### 设计准则 1. **不依赖 size 参数**:自定义 `operator delete` 时应保持与默认行为兼容,避免假设 size 值可用 2. **匹配 new/delete**:自定义 `operator new` 必须搭配对应的 `operator delete` 3. **空指针安全**:始终处理 `ptr == nullptr` 的情况 > **性能提示**:默认实现经过深度优化,在多数场景下优于自定义版本,仅当有明确需求(如内存追踪、对象池)时才应重载[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值