new的六种重载形式:
全局的new有六种重载形式,
void *operator new(std::size_t count)
throw(std::bad_alloc); //一般的版本
void *operator new(std::size_t count, //兼容早版本的new
const std::nothrow_t&) throw(); //内存分配失败不会抛出异常
void *operator new(std::size_t count, void *ptr) throw();
//placement版本
void *operator new[](std::size_t count) //
throw(std::bad_alloc);
void *operator new[](std::size_t count, //
const std::nothrow_t&) throw();
void *operator new[](std::size_t count, void *ptr) throw();
所以, 刚才的用法, 就是使用new函数的一种重载形式.
如果A这个对象以同样实行重载了new函数的化, 作为成员函数
会被优先调用.
C++的各种new简介
1.new T
第一种new最简单,调用类的(如果重载了的话)或者全局的operator new分配空间,然后用
类型后面列的参数来调用构造函数,用法是
new TypeName(initial_args_list). 如果没有参数,括号一般可以省略.例如
int *p=new int;
int *p=new int(10);
int *p=new foo("hello");
通过调用delete来销毁:
delete p;
2. new T[]
这种new用来创建一个动态的对象数组,他会调用对象的operator new[]来分配内存(如果
没有则调用operator new,搜索顺序同上),然后调用对象的默认构造函数初始化每个对象
用法:
new TypeName[num_of_objects];
例如
int *p= new int[10];
销毁时使用operator delete[]
3.new()T 和new() T[]
这是个带参数的new,这种形式的new会调用operator new(size_t,OtherType)来分配内存
这里的OtherType要和new括号里的参数的类型兼容,
这种语法通常用来在某个特定的地址构件对象,称为placement new,前提是operator new
(size_t,void*)已经定义,通常编译器已经提供了一个实现,包含头文件即可,这个
实现只是简单的把参数的指定的地址返回,因而new()运算符就会在括号里的地址上创建
对象
需要说明的是,第二个参数不是一定要是void*,可以识别的合法类型,这时候由C++的重载
机制来决定调用那个operator new
当然,我们可以提供自己的operator new(size_,Type),来决定new的行为,比如
char data[1000][sizeof(foo)];
inline void* operator new(size_t ,int n)
{
return data[n];
}
就可以使用这样有趣的语法来创建对象:
foo *p=new(6) foo(); //把对象创建在data的第六个单元上
的确很有意思
标准库还提供了一个nothrow的实现:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
就可以实现调用new失败时不抛出异常
new(nothrow) int(10);
// nothrow 是std::nothrow_t的一个实例
placement new 创建的对象不能直接delete来销毁,而是要调用对象的析够函数来销毁对
象,至于对象所占的内存如何处理,要看这块内存的具体来源
4. operator new(size_t)
这个的运算符分配参数指定大小的内存并返回首地址,可以为自定义的类重载这个运算符,
方法就是在类里面声明加上
void *operator new(size_t size)
{
//在这里分配内存并返回其地址
}
无论是否声明,类里面重载的各种operator new和operator delete都是具有static属性的
一般不需要直接调用operator new,除非直接分配原始内存(这一点类似于C的malloc)
在冲突的情况下要调用全局的operator加上::作用域运算符:
::operator new(1000); // 分配1000个字节
返回的内存需要回收的话,调用对应的operator delete
5.operator new[](size_t)
这个也是分配内存,,只不过是专门针对数组,也就是new T[]这种形式,当然,需要时可以
显式调用
6.operator new(size_t size, OtherType other_value)
和operator new[](size_t size, OtherType other_value)
参见上面的new()
需要强调的是,new用来创建对象并分配内存,它的行为是不可改变的,可以改变的是各种
operator new,我们就可以通过重载operator new来实现我们的内存分配方案.
题目说明:
为上周题目中的 Fruit和Apple 添加 构造函数与 析构函数,并在构造函数与析构函数中打印控制台信息,观察构造和析构调用过程。然后为Apple类重载::operator new和::operator delete,在控制台打印信息,并观察调用结果。
附件2个文件夹,1是添加完构造函数和析构函数后,调用测试;2是添加了operator new、delete后的调用测试。由于题目里面::有一种全局的感觉,所以Apple类和全局的重载都写了,写完想想既然都写了,那么就把数组的也写一下(正好可以观察下第三周做法的内部实现)。
第一部分:测试了4种创建对象的方式:
在栈区创建Fruit对象,创建时调用了Fruit类的构造函数,销毁时调用Fruit类的析构函数。
在堆区创建Fruit对象,创建时会先分配空间,然后调用Fruit类的构造函数,并把指针传回,控制台只能显示调用构造函数这一步。销毁时会先调用析构函数,然后再释放内存,控制台只能显示调用析构函数这一步。
在堆区创建Apple对象,并用父类指针指向它(upcast),这样构造函数的调用次序依次是父类构造函数、子类构造函数。析构时先调用子类析构函数,再调用父类析构函数。
在栈区创建Apple对象,调用构造函数时,先父类再子类,调用析构函数时,先子类再父类。
第二部分:重载operator new和operator delete后观察调用结果:
同前。
同前。
在堆区创建Fruit对象,会调用全局new、delete运算符。
在堆区创建Apple对象,并用父类指针指向它(upcast),会调用Apple类的new、delete运算符。
在堆区创建Apple类数组,大小为5个sizeof(Apple)加上一个指针的大小,且后被创建的对象先被销毁。
在创建时强制调用全局的new运算符,在销毁时由于没有要求强制调用全局delete运算符,Apple类的delete运算符被调用了。为了保持一致性,应该在delete前面也加上::
为了呼应第三周的作业而写的测试代码,首先在堆区创建一个数组,每个元素是一个Fruit类型的指针,大小是5×指针大小,20(32位)或40(64位)。再创建5个Apple类型对象,以父类指针指向它们(upcast),会调用Apple类的new分配空间,父类构造函数,子类构造函数,把指针传回。销毁时先调用子类析构函数,父类析构函数,再销毁内存,最后调用全局delete运算符释放分配给指针数组的空间。
关于本次作业的思考:
结合第4周的作业,我们计算出了sizeof(Fruit)和sizeof(Apple),我觉得此题的第一个目的是让我们在调用的时候知道每次new和delete型别X的对象实际上是去呼叫operator new(sizeof(X)),delete时会据此释放对应的空间大小(sizeof(X))。同样的申请一个含N个X型别的数组,就会呼叫opreator new(N*sizeof(X))。
这道题目的另一个出题目的是让我们了解为什么基类需要有虚析构函数。假如我将提交的代码做小小改动,基类和子类的析构函数去掉virtual,为Fruit类写一个operator delete(为了打印delete内存大小),就会得到这样的结果(运行环境Win10 TDM-GCC 4.9.2 32-bit):
Apple::new(), size = 32, return: 0xc621a0
default Fruit ctor.this = 0xc621a0
default Apple ctor.this = 0xc621a0
Fruit dtor.this = 0xc621a0
Fruit::delete(), ptr = 0xc621a0, size = 24
很明显的,内存泄漏了。因为当使用upcast时,传给delete的大小是sizeof(Fruit)(基类的),并不是sizeof(Apple),为了得到正确的大小,必须将基类的析构函数设置为虚析构函数。
我在看《C++ Primer》和《The C++ Programming Language》发现了一点不一样。前者在做delete的时候是直接free(p)的(就像我提交的代码一样),但是后者对p做了判断,截图如下:
我用黄色高亮标注的if(p),如果单纯从free(p)这个操作来讲,是不需要再判断p是否非空的,不知道此处的用意如何。比如说p指向了别的程序内存,或者系统内存,那么delete就会出现严重后果,但这个时候p也不是空,照样会执行。系统默认的delete是否有机制去避免这样的情况。
点评中提到这道题目的本意是重载Apple类里的new和delete,一般做工程时也不会去“重载”全局new和delete,在Apple类里面可以调用全局的new和delete。
另前面提到的第三点,在TCPL里面也提到了,这里的delete是针对正常new出来内存做处理,如果自己随便给指针一个地址(悬浮指针),可能就会造成无法估计的错误。李老师提出可能p非nullptr的时候会做其他处理,而nullptr不需要,这也是一种可能。