C++里允许用户通过自定义operator new的方式来更改new表达式的行为,这给了程序员定制内存管理方案的自由。但是享受这种自由的时候必须遵守一定的规范,具体可以参见《Effective C++ 2nd》的相关条款。本文补充解释一些特别容易引起误解的问题。
operator new和operator delete都有其正规形式(normal signature):
void* operator new(size_t size);
void operator delete(void *p);
void operator delete(void *p,size_t size);
普通的new与delete表达式在分配与释放内存时调用的就是它们。一般来说operator delete(void*)的优先级比operator delete(void*,size_t)要高,这意味着如果在同一空间(scope)定义了这两种形式的delete,拥有单一参数者优先被编译器选择。这一点在VC7.1中得到验证,不知其它编译器如何?
除了上面的正规形式外,我们还可以定义拥有更多参数的operator new和operator delete,只要保证前者的返回值和第一个参数分别是void*和size_t类型,而后者的分别是void和void*就行了。比如:
void* operator new(size_t size,const char* szFile,int nLine);
void operator delete(void *p,const char*,int);
表达式new("xxx",20) SomeClass实际上就是告诉编译器调用上面的多参数operator new来分配内存。但是不要依此类推出 delete("xxx",20) pObj,这是非法的。那么怎么才能调用到这个多参数的operator delete呢?实话告诉你,你没有这个权利。呵呵,别吃惊,容我慢慢解释。当两个operator new和operator delete有相等的参数个数,并且除了第一个参数之外其余参数的类型依次完全相同之时,我们称它们为一对匹配的operator new和operator delete。按照这个标准,上面两位就是匹配的一对了。在我们使用 SomeClass *pObj = new("xxx",20) SomeClass 于堆中构建一个对象的过程中,如果在执行SomeClass的构造函数时发生了异常,并且这个异常被捕获了,那么C++的异常处理机制就会自动用与被使用的operator new匹配的operator delete来释放内存(补充一点:在operator new中抛出异常不会导致这样的动作,因为系统认为这标志着内存分配失败)。编译期间编译器按照以下顺序寻找匹配者:首先在被构建对象类的类域中寻找,然后到父类域中,最后到全局域,此过程中一旦找到即停止搜寻并用它来生成正确的内存释放代码,如果没有找到,当发生上述异常情况时将不会有代码用来释放分配的内存,这就造成内存泄漏了。而如果一切正常,delete pObj 则总是会去调用operator delete的正规形式。现在明白了吧,多参数的operator delete不是给我们而是给系统调用的,它平常默默无闻,但在最危急的关头却能挺身而出,保证程序的健壮性。为了有个感性的认识,让我们看看下面的代码(试验环境是VC7.1):
#include <malloc.h>
struct Base
{
Base()
{
throw int(3);
}
~Base() {}
void* operator new( size_t nSize, const char*,int)
{
void* p = malloc( nSize );
return p;
}
void operator delete( void *p)
{
free(p);
}
void operator delete( void* p,const char*,int)
{
free( p );
}
};
#define NULL 0
#define new new(__FILE__, __LINE__)
int main( void )
{
Base* p = NULL;
try
{
p = new Base;
delete p;
}
catch(...)
{
}
return 0;
}
跟踪执行会发现:程序在 p = new Base 处抛出一个异常后马上跳去执行operator delete(void*,const char*,int)。注释掉Base构造函数中的throw int(3)重来一遍,则new成功,然后执行delete p,这时实际调用的是Base::operator delete(void*)。以上试验结果符合我们的预期。注意,operator new和operator delete是可以被继承和重定义的,那接下来就看看它们在继承体系中的表现。引进一个Base的派生类(代码加在#define NULL 0的前面):
struct Son : public Base
{
Son()
{
}
void* operator new( size_t nSize, const char*,int)
{
// class Son
void* p = malloc( nSize );
return p;
}
void operator delete( void *p)
{
// class Son
free(p);
}
void operator delete( void* p,const char*,int)
{
// class Son
free( p );
}
};
然后将main函数中的p = new Base改成p = new Son并且取消对Base()中的throw int(3)的注释,跟踪执行,发现这回new表达式调用的是Son重定义的operator new,抛出异常后也迅速进入了正确的operator delete,即Son重定义的多参数版本。一切都如所料,是吗?呵呵,别急着下结论,让我们把抛异常的语句注释掉再跑一次吧。很明显,有些不对劲。这次delete p没有如我们所愿去调用Son::operator delete(void*),而是找到了在Base中定义的版本。怎么回事?我愿意留一分钟让好奇的你仔细想想。
找到答案了吗?没错,罪魁祸首就是那愚蠢的Base析构函数声明。作为一个领导着派生类的基类,析构函数竟然不声明成virtual函数,这简直就是渎职。赶紧纠正,在~Base()前加上一个神圣的virtual,rebuild and run.....。谢天谢地,世界终于完美了。
可能你会疑惑,在没有给基类析构函数加virtual之前,当发生异常时C++为什么知道正确地调用派生类定义的多参数operator delete,而不是基类的?其实很简单,new一个对象时必须提供此对象的确切类型,所以编译器能够在编译期确定new表达式抛出异常后应该调用哪个类定义的operator delete。对于正常的delete p来说,如果p被声明为非基类类型的指针,编译器就会在编译时决定调用这种声明类型定义的operator delete(静态绑定),而如果p是某种基类类型指针,编译器就会聪明地把到底调用哪个类定义的operator delete留待运行期决定(动态绑定)。那么编译器如何判断p是否是基类指针呢?实际上它的根据就是p的声明类型中定义的析构函数,只有在析构函数是虚拟的情况下p才被看成基类指针。这就可以解释上面碰到的问题。当时p被声明为Base*,程序中它实际指向一个Son对象,但我们并没有把~Base()声明为虚拟的,所以编译器大胆地帮我们做了静态绑定,也即生成调用Base::operator delete(void*)的代码。不过千万不要以为所有编译器都会这样做,以上分析仅仅是基于VC7.1在本次试验中的表现。事实上在C++标准中,经由一个基类指针删除一个派生类对象,而此基类却有一个非虚拟的析构函数,结果未定义。明白了吧老兄,编译器在这种情况下没有生成引爆你电脑的代码已经算是相当客气与负责了。现在你该能够体会Scott Meyers劝你"总是让base class拥有virtual detructor"时的苦心吧。
至于数组版本的operator new[]和opeator delete[],情况一样。朋友们可以自己做试验确认一下。
最后要指出的是,试验代码中对operator new和operator delete的实现相当不规范,负责任的做法仍然请大家参考Scott Meyers的著作。
分享到:
相关推荐
在C++编程中,自定义`operator new`和`operator delete`是实现特定内存管理策略的重要手段。它们允许程序员根据需求调整内存分配和释放的行为。本文将深入探讨这两个操作符的自定义规则、使用场景以及潜在的陷阱。 ...
`operator new`可以被重载,以实现自定义的内存管理策略,如内存池、日志记录、异常处理等。重载`operator new`时,第一个参数必须是`size_t`类型,表示需要分配的内存大小,其他参数可以根据需求添加。 另外,C++...
new operator/delete operator就是new和delete操作符,而operator new/operator delete是函数。 new operator(1)调用operator new分配足够的空间,并调用相关对象的构造函数(2)不可以被重载 operator new(1)只...
1. **定义新的操作符**:创建名为`operator new`和`operator delete`的全局函数。这些函数必须具有与标准操作符相同的基本签名,但可以增加额外的参数或返回类型来实现自定义行为。 2. **覆盖分配器**:在`new`操作...
并且,如果重载了operator new,就应该也相应的重载operator delete,这是良好的编程习惯。 8. placement new可以实现new operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块...
在C++中,可以自定义`new`和`delete`操作符,以便在内存分配和释放时执行自定义操作。例如,你可以添加日志记录、检查内存池或者在分配失败时抛出异常等。这样做的好处是可以增加代码的可维护性和可控性,特别是在...
在描述中虽然没有提供具体的细节,但我们可以推测开发者可能在项目中包含了两个或多个不同的C++库,这些库可能都自定义了`operator delete`,导致链接器无法确定应使用哪个版本。这种问题在使用静态库(如.lib文件)...
标题和描述提到的方法是通过将`operator new`和`operator delete`设置为私有来实现这一目标。下面我们将详细探讨这个主题。 首先,`operator new`和`operator delete`是C++中的全局运算符,用于动态内存分配和释放...
与`new`操作符相对应的是`delete`操作符,用于释放由`new`分配的内存。 1. **delete 操作符**:用于释放由`new`分配的单个对象的内存。 2. **delete[] 操作符**:用于释放由`new[]`分配的数组的内存。 例如: ```...
本文详细介绍了 C++ 中的 new,包括 new operator、operator new、placement new 三种形态的内容,并且详细解释了每一种形态的行为和用法,同时也提到了 delete 的相关知识点,希望对读者有所帮助。
与 `new` 和 `new[]` 对应的是 `delete` 和 `delete[]`。这两个操作符分别用于释放由 `new` 和 `new[]` 分配的内存: - **`delete`**:释放单个对象的内存,并调用其析构函数。 - **`delete[]`**:释放数组对象的...
这意味着基类的`operator new[]`不会被子类的实例调用,而`delete[]`会调用子类的析构函数,但使用基类的`operator delete[]`。 5. **数组分配与释放**:`new[]`分配的内存不仅包含元素本身,还可能包含用于存储...
delete operator和operator delete是与new operator对应的操作符。operator delete也是可以重载的,并且,如果重载了operator new,就应该也相应的重载operator delete,这是良好的编程习惯。 8. new的使用注意 在...
简述 C++ 中的 new 和 delete 的基本原理,并讲述了内存检测子系统的实现原理、实现中的技巧,并对内存泄漏检测的高级话题进行了讨论。解决了本人在内存泄露很多多问题(我也是down的别人的),觉得非常好,所以给...
下面展示了如何自定义 `operator new` 和 `operator delete`: ```cpp void* operator new(size_t sz) throw (std::bad_alloc) { cerr ; void* mem = malloc(sz); if (mem) return mem; else throw std::bad_...
`delete`操作符会调用与分配内存时相同版本的`operator delete`,即如果使用的是类内的`operator new`,则会调用对应的`operator delete`。 在C++标准中,如果`new`运算符分配内存失败,它将抛出`std::bad_alloc`...