写这篇文章的原因是:有一次,我见到了类似下面的代码,我感到很惊奇,从来没见到new的这种用法,后来查看MSDN,发现一个常用的new运算符居然有这么多语法规定,甚是感慨!所以记录在此,日后备查!
Blanks *pBlanks = new(0xa5) Blanks;
一 new运算符语法
new运算符负责从堆内存区分配对象或者对象数组的内存空间,返回一个指向对象的非0指针。其语法为
[::] new [placement] new-type-name [new-initializer]
[::] new [placement] ( type-name ) [new-initializer]
其中:
placement:占位符,当重写operator new时,提供了一个传递额外参数的方式;占位符可以包含多个参数。
type-name:分配内存的类型,可以是内置的或者类类型。
initializer:对象的初始化值。不能用于数组。只有类具有默认构造函数的时候,new 运算符才会创建这个类的数组。
上述就是new操作符的基本语法,然后先把它放在一边。看一下操作符new如何工作。
二 new 如何工作(理解运算符new 和函数operator new)
分配运算表达式(即包含new操作符的表达式)会做三件事情:
- 当分配一个或多个对象时,定位并保留内存空间。这个阶段完成后,就分配了正确数量的内存空间,但这还不是一个对象。
- 初始化对象。一旦初始化完成,在内存空间就有了足够的信息,使其成为一个对象。
- 返回一个继承自此对象的指针。程序可以通过这个指针访问新建的对象。
new运算符会调用函数operator new. 对于任意类型的数组,非class, struct,或者union类型的对象,编译器会调用全局函数——::operator new——来分配内存。类类型的对象可以定义他们自己的operator new静态成员函数。
在代码中,当用new运算符为类类型type的对象分配内存时,编译器会调用 type::operator new( sizeof( type ) )成员函数;当这个类没有自定义的operator new运算符时,编译器会调用::operator new(sizeof(type))全局函数。请注意,使用new运算符编译时,编译器会自动计算类的大小后传递给函数operator new 。这样,new 运算符能够给对象分配正确数量的内存。
new语法中的一个选项允许指定占位符placement中的参数。placement参数仅仅允许使用在类中自定义的operator new实现中;它允许传递给opeator new额外的信息。例如下面的表达式:
T *TObject = new ( 0x0040 ) T;
当类中具有自定义的new操作符时, 会被编译为
T *TObject = T::operator new( sizeof( T ), 0x0040 ); //请注意,T::operator new的第一个参数是编译器自动添加的
否则, 被编译为
T *TObject = ::operator new( sizeof( T ), 0x0040 ); //请注意,::operator new的第一个参数是编译器自动添加的
请注意,尽管上述例子中在placement字段中只有一个参数,但实际上可以传递给operator new的参数没有限制。
即便类类型中有自定义的operator new,全局的操作符依然可以通过以下方式使用
T *TObject =::new TObject;
作用域解析运算符(::)强制使用全局的new操作符。
三 operator new函数
上面说过了,new运算符会调用operator new函数,而根据作用域不同,operator new分为以下两种类型:
Operator | Scope |
---|---|
::operator new | Global |
class-name::operator new | Class |
operator new函数的第一个参数类型必须是size_t(在STDDEF.H中定义),然后返回类型永远是void *.
当new操作符用在分配内置类型、无自定义operator new成员函数的类类型时、任意类型的数组时 ,编译器会调用全局的operator new函数。
当new 操作符用在分配一个有自定义operator new(静态)成员函数的类类型时 ,编译器会调用类自己的自定义operator new 函数。
类自定义operator new 函数是一个静态成员(因此,不可能是virtual了),在分配这个类对象的内存时,会隐藏全局的operator new 。考虑下面的例子。
// spec1_the_operator_new_function1.cpp
#include <malloc.h>
#include <memory.h>
class Blanks
{
public:
Blanks(){}
void *operator new( size_t stAllocateBlock, char chInit );
};
void *Blanks::operator new( size_t stAllocateBlock, char chInit )
{
void *pvTemp = malloc( stAllocateBlock );
if( pvTemp != 0 )
memset( pvTemp, chInit, stAllocateBlock );
return pvTemp;
}
// For discrete objects of type Blanks, the global operator new function
// is hidden. Therefore, the following code allocates an object of type
// Blanks and initializes it to 0xa5
int main()
{
Blanks *a5 = new(0xa5) Blanks;
return a5 != 0;
}
new括号中的实参值0xa5传递给Blanks::operator new的chInit 参数。全局的operator new函数这时候被隐藏了,所以下面的代码会报错:
Blanks *SomeBlanks = new Blanks; //error,参数错误,缺一个参数
在Visual C++ 5.0和更早的版本,用new 运算符分配非类类型和 所有数组(不管数组元素是否为类类型)均是调用全局的operator new函数。
从Visual C++ 5.0以后,编译器支持了成员数组new和delete函数。
// spec1_the_operator_new_function2.cpp
class MyClass
{
public:
void * operator new[] (size_t)
{
return 0;
}
void operator delete[] (void*)
{
}
};
int main()
{
MyClass *pMyClass = new MyClass[5];
delete [] pMyClass;
}
四 一个综合性的例子
在下面的例程中,你会理解如何给operator new传递参数、如何解除类对全局运算符new的屏蔽、new运算符如何初始化对象等等知识点。
#include <malloc.h>
#include <memory.h>
class Blanks
{
public:
Blanks() {}
Blanks(int i, double d) {
int m = i;
double dd = d * 2;
}
//注意,这里重载了operator new函数,接受两个参数,在使用new运算符时,第一个参数编译器会自动传递进去
void *operator new(size_t stAllocateBlock, char chInit);
};
void *Blanks::operator new(size_t stAllocateBlock, char chInit)
{
void *pvTemp = malloc(stAllocateBlock);
if (pvTemp != 0)
memset(pvTemp, chInit, stAllocateBlock);
return pvTemp;
}
int main()
{
//1.理解如何给operator new传递参数
//这一句实际上被转换为 Blanks *a1 = Blanks::operator new(sizeof(Blanks),0xa5);
Blanks *a1 = new(0xa5) Blanks;
//2.理解何时会屏蔽全局的new
//Blanks *a2 = new Blanks;//error,因为类的自定义new屏蔽了全局new,而自定义的new有两个参数,
//3.如何解除类对全局运算符new的屏蔽
//即使类中有自定义的new,我们仍然可以通过作用域解析运算符使用全局new
Blanks* a3 = ::new Blanks;
//4.new运算符如何初始化对象
//new的参数是通过new后面括号中参数进行传递的,而Blanks的初始化是通过Blanks其后的括号参数传递进去的
Blanks* a4 = new(0xa5) Blanks(1, 7.5);
}
五 使用new可能导致的内存泄漏
如果使用无参数的new操作符,并且编译器设置了 /GX, /EHa, 或者/EHs选项,那么当new抛出异常时,编译器将会产生调用操作符delete的代码。(MSDN原文:If you use the operator new without any extra arguments, and compile with the /GX, /EHa, or /EHs option, the compiler will generate code to call operator delete if the constructor throws an exception. )
但是,如果使用new运算符的placement new形式(即除了分配大小之外还包含其他参数的形式),那么如果构造函数抛出异常,编译器将不支持delete运算符的placements形式。例如:
// expre_new_Operator2.cpp
// C2660 expected
class A {
public:
A(int) { throw "Fail!"; }
};
void F(void) {
try
{
//剖出异常时,由于编译器自动产生了::operator delete(void*)代码,指向pal的堆内存会被释放
// heap memory pointed to by pa1 will be deallocated
// by calling ::operator delete(void*).
A* pa1 = new A(10);
}
catch (...) { }
try
{
//当A::A(int)抛出异常时,我们应当调用::operator delete(void*, char*, int)
//去释放pa2指向的内存;但是因为::operator delete(void*, char*, int) 并没有实现,
//析构无法发生,内存出现了泄漏
// This will call ::operator new(size_t, char*, int).
// When A::A(int) does a throw, we should call
// ::operator delete(void*, char*, int) to deallocate
// the memory pointed to by pa2. Since
// ::operator delete(void*, char*, int) has not been implemented,
// memory will be leaked when the deallocation cannot occur.
A* pa2 = new(__FILE__, __LINE__) A(20);
}
catch (...) { }
}
int main() {
A a;
}