C++ Gotchas 条款62:替换Global New和Global Delete

原创 2003年04月21日 08:57:00

Gotcha #62: Replacing Global New and Delete

Gotcha条款62:替换Global NewGlobal Delete

 

operator newoperator deletearray new亦或array delete的标准global版本替换为自定制版本,这几乎从来都不是个好主意——即使C++标准允许你这么做。这些函数的标准版本一般都针对通用目的(general-purpose)之存储管理做了极大优化,而用户自定义的替代版本则不大会做得更好了。(然而,针对特定的类别或类别阶层体系采用(自定制的)成员函数形式的操作来定制其内存管理,则通常是合理的。)

 

如果operator newoperator delete针对特定目的之实现版本作出了与标准版本相异的行为,其就可能引入臭虫,因为许多标准程序库和第三方程序库的正确性皆依赖于这些函数缺省的标准实现版本。

 

比较安全的方案是对global版本的operator new等函数进行重载,而不是替代它们。假设我们要以特定的字符样式(character pattern)填充新分配的存储空间:

 

void *operator new( size_t n, const string &pat ) {

  char *p = static_cast<char *>(::operator new( n ));

  const char *pattern = pat.c_str();

  if( !pattern || !pattern[0] )

    pattern = "/0"; // note: two null chars

  const char *f = pattern;

  for( int i = 0; i < n; ++i ) {

    if( !*f )

      f = pattern;

    p[i] = *f++;

  }

  return p;

}

 

operator new版本接收一个字串样式作为引数,并将其拷贝到新分配的存储空间中。经由重载解析,编译器就可以区分标准operator new与我们自己的“接收两个引数之版本”。

 

string fill( "<garbage>" );

string *string1 = new string( "Hello" ); // 标准版本

string *string2 =

  new (fill) string( "World!" ); // 重载的版本

 

标准中还定义了一个重载的operator new版本;除了以size_t作为第一引数之外,该版本还接收一个void*型别作为第二引数。该实现只是简单的返回第二引数。(其中的throw()语法是一个exception-specification(异常规范),意味该函数不会传播出任何异常。在后述讨论及一般情况下,都可以安然忽略之。)

 

void *operator new( size_t, void *p ) throw()

{ return p; }

 

这就是标准的placement new,用于在特定的位置空间建构一个对象。(其不同之处在于,标准的“单引数operator new”可以被替换,而试图替换placement new则是非法的。)本质上来说,我们会将其用于“让编译器误以为调用了一个构造函数”的场合。比如说,对于一个嵌入式应用,我们或许想在某个特定的硬件地址上建构一个“status register(状态寄存器)”对象:

 

class StatusRegister {

// . . .

};

void *regAddr = reinterpret_cast<void *>(0XFE0000);

// . . .

// regAddr的位置放一个register object

StatusRegister *sr = new (regAddr) StatusRegister;

 

自然,经由placement new创建的对象必须在某个时刻被销毁。然而,由于placement new并未真正的分配内存(译注:其只是在指定位置放入对象,并未进行内存分配),因此也必须保证在销毁时没有内存被删除。回忆一下,delete operator的行为是:在调用operator delete函数(以便归还存储空间)之前,首先唤起“欲删除对象”之析构函数。对于“对象是经由placement new进行‘空间分配’”的情形,为了避免任何尝试归还内存空间的动作,我们在销毁对象时必须对析构函数进行显式的(explicit)调用(译注:这正是delete operator所做的第一步操作,第二步“调用operator delete函数”的操作就不用去做了)。

 

sr->~StatusRegister(); // 显式的调用dtor, 不调用operator delete函数

 

Placement newexplicit destruction(显式析构操作)显然是非常有用的特性,但倘若不保守并谨慎的使用它们,显然也是非常危险的。(详见Gotcha条款47中一个来自标准程序库的例子。)

 

应注意,当我们重载operator delete时,这些重载版本绝不会被“使用标准delete形式的表达式”唤起。

 

void *operator new( size_t n, Buffer &buffer ); // 重载版本的new

void operator delete( void *p,

  Buffer &buffer ); // 对应的重载版本之delete

// . . .

Thing *thing1 = new Thing; // 使用标准的operator new

Buffer buf;

Thing *thing2 = new (buf) Thing; // 使用重载版本的operator new

delete thing2; // 不对, 应该使用重载版本的delete

delete thing1; // 正确, 使用标准的operator delete

 

相应的,对于经由placement new创建的对象,我们不得不显式的(explicitly)调用该对象的析构函数,然后直接明了的调用适当的operator delete函数,以便显式的将对象的存储空间进行去配:

 

thing2->~Thing(); // 正确, 销毁Thing

operator delete( thing2, buf ); // 正确, 使用重载版本的delete

 

实际当中,经由“global operator new之重载版本”分配的存储空间经常错误的经由“global operator new之标准版本”被去配。一个避免这种错误的方法是保证:任何经由“global operator new之重载版本”分配的存储空间都是经由“global operator new之标准版本”来获取存储空间(译注:意即,在“global operator new之重载版本”的实现中,通过调用“global operator new之标准版本”来获取空间,详见本条款开头的示例)。这正是前述第一个重载实现版本(译注:指的正是本条款开头那个“以特定的字符样式(character pattern)填充新分配的存储空间”的例子)所用的方法,其能与“global operator delete之标准版本”相配合并运作正常:

 

string fill( "<garbage>" );

string *string2 = new (fill) string( "World!" );

// . . .

delete string2; // 运作正常!

 

一般来说,global operator new的重载版本要么就不分配任何存储空间,要么就应该只简单的包裹(wrapglobal operator new的标准版本(译注:如本条款开头那个例子所示,重载版本是标准版本的一个wrapper)。

 

通常情况下,最好的方案是全然避免对“处于global scope的内存管理operator functions”做手脚,代之以“operator newoperator deletearray newarray delete的成员函数版本”来定制类别或类别阶层体系的内存管理操作。

 

Gotcha条款61的结尾我们提到过,若从new表达式中的初始化操作传出一个异常,运行期系统会唤起一个“适当的”operator delete函数:

 

Thing *tp = new Thing( arg );

 

如果Thing的分配动作成功了但构造函数抛出异常,那么运行期系统将会唤起一个适当的operator delete函数来归还tp所指向的未经初始化的内存。在上例中,这个“适当的operator delete”要么是global版本的operator delete(void*),要么就是一个具有相同形式的成员函数版本。然而不同的operator new即意味着不同的operator delete

 

Thing *tp = new (buf) Thing( arg );

 

此时,适当的operator delete应该是“双引数版本”operator delete(void*,Buffer&),与Thing分配操作所使用的“operator new之重载版本”相对应;这正是运行期系统会唤起的版本。

 

C++在定义内存管理的行为方面给予了颇大的弹性,伴之以复杂性作为代价。标准的“global operator new”和“global operator delete”便足以满足多数需求。因此,我们应该仅在确实需要的情况下才采用更复杂的方案。

C++ Gotchas 条款62:替换Global New和Global Delete

Gotcha #62: Replacing Global New and DeleteGotcha条款62:替换Global New和Global Delete 将operator new、ope...
  • hejishan
  • hejishan
  • 2008年04月01日 16:35
  • 116

DLL与EXE之间的内存 new 与 delete 上的问题

一个模块一个堆,一个线程一个栈。 dll里malloc的内存,在exe里free会出错。 CRT(C运行时期库)不是使用进程缺省的堆来实现malloc(new中调用malloc)的,而是使用一个全局...
  • liujiayu2
  • liujiayu2
  • 2015年10月09日 12:02
  • 569

C++ Global Varible(全局变量)

Q: 如果想要统计一个函数被调用的次数, n
  • a130737
  • a130737
  • 2014年06月27日 18:22
  • 1079

PHP自学之路------static,global用法

1、下面是全局变量使用:
  • jsh13417
  • jsh13417
  • 2013年05月19日 16:35
  • 4996

php 引用和global

1 引用是什么 Manual手册原话:在 PHP 中引用意味着用不同的名字访问同一个变量内容。这并不像 C 的指针:例如你不能对他们做指针运算,他们并不是实际的内存地址…… 查看引用不是什么了解更多...
  • ytfrank2012
  • ytfrank2012
  • 2016年01月17日 10:14
  • 777

C++ Global(static) Object - Problems

C++中静态对象的3种类型:1、   函数范围内的静态变量;2、   全局和名字空间范围的静态变量也被称为非局部静态变量;3、   静态成员变量指那些在它们所属类的所有实例之间共享的变量。全局(静态)...
  • SJcinux
  • SJcinux
  • 2007年07月23日 16:28
  • 2002

c++ 中的重载全局new,delete

最近做一个小项目,对c++又有很多新的理解。实在不的不让人发出感叹,c++太强大了,绝对不是一朝一夕就可以领悟她的内涵的。        首先我们要清楚,为什么我们要重载new,和delete了?这...
  • u012543266
  • u012543266
  • 2013年11月04日 09:08
  • 6345

【C++】 深入探究 new 和 delete

在C++中,我们应该经常会用到new、delete,它们是C++的一个关键字,同时也是一个操作符,下面我将我对这两者的了解和学习做一个总结和探讨。new和delete的全过程首先我们定义一个对象A: ...
  • codedoctor
  • codedoctor
  • 2017年07月27日 14:29
  • 589

global flags

shao hou shang chuan
  • yinhaijing123
  • yinhaijing123
  • 2013年04月16日 15:45
  • 953

new/delete 详解

new 和 delete 是 C++ 用于管理 堆内存 的两个运算符,对应于C语言中的 malloc 和 free,但是malloc和free是函数,new 和 delete 是运算符。...
  • hihozoo
  • hihozoo
  • 2016年05月18日 14:31
  • 6898
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C++ Gotchas 条款62:替换Global New和Global Delete
举报原因:
原因补充:

(最多只允许输入30个字)