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

news/2024/12/14 18:47:41

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 (::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( " " );

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 (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( " " );

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”便足以满足多数需求。因此,我们应该仅在确实需要的情况下才采用更复杂的方案。





http://www.niftyadmin.cn/n/3655126.html

相关文章

导入Excel遇到的格式问题

1. Excel表格导入时&#xff0c;将中文key转为英文key Excel表格中读入的是姓名,而后端需要的是username [{姓名&#xff1a;小张&#xff0c; 手机号: 13712345678}{.....}]我们需要将他转换为[ {username&#xff1a;小张&#xff0c;mobile: 13712345678}, {.....} ] 复制…

Kingofark关于学习C++和编程的50个观点2003修订版

<Kingofarks 50 Points of View About Learning C And Programming>Kingofarks50 Points of View About Learning C And ProgrammingK ][ N G of A R K™关于学习C和编程的50个观点2003修订版Revision 2.0by K ][ N G of A R K ™前 言Long long time ago, on a topic fa…

history路由模式和hash路由模式的区别

hash模式 hash模式 &#xff1a;使用 URL 的 hash 来模拟一个完整的 URL, 其显示的网络路径中会有 “#” 号http://www.abc.com/#/hello hash 虽然出现URL中&#xff0c;但不会被包含在HTTP请求中&#xff0c;对后端完全没有影响&#xff0c;因此改变hash后刷新, 也不会有问题…

写项目时简单封装了一个点击弹出视频播放的组件(需要可以拿去玩玩)

效果图&#xff1a; 因为封装的时候主要是为了让代码更简洁 没想复用这一块 所以使用起来不是很灵活 可以参考一下 具体如下&#xff1a; 父组件&#xff1a; 给点击弹出视频的元素添加一个点击事件把视频的url地址传过来 我这里添加的是move src属性是视频的播放地址 //父组件…

防抖的使用

防抖(debounce) 触发同一事件太快&#xff0c;以致于发送了多次请求&#xff0c;所以需要防抖 减少一段时间内事件发送的次数 减轻服务器压力 一段时间内用户无操作则发送请求 这段时间内又被触发&#xff0c;则重新计时。 实现原理&#xff1a; 设置一个定时器&#xff0c;通…

VC编程经验汇总(一)

1. 窗口最大化、最小化的实现当我们不能用标题栏的最大化、最小化及恢复按钮而又需在其他的地方实现这些功能&#xff0c;可以在指定的消息处理函数里添加&#xff1a;WINDOWPLACEMENT wndpl;WINDOWPLACEMENT *pwndpl;pwndpl &wndpl;GetWindowPlacement(pwndpl);pwndpl-&g…

当输入框的绑定了失去焦点事件但是又有模糊查询的问题

当我们的input输入框绑定了失去焦点 模糊查询的推荐框应该隐藏 正常点击空白处是没有问题的 但是当我们点击了模糊搜索的选项的时候 也相当于触发了input的失去焦点事件 模糊搜索框会直接关闭 且选中的值不会回填到输入框 这时我们需要将原本绑定在模糊搜索框上click&#xff…

VC编程经验汇总(二)

6. 如何创建可伸缩的对话框在进行对话框的设计时&#xff0c;有时候我们需要设计可伸缩的对话框&#xff0c;当用户按下某个按钮时弹出或隐藏对话框的下半部分。&#xff08;1&#xff09;、首先在对话框中建立一个图片控件把ID设为IDC_DIVIDER&#xff0c;Type设置为矩形&…