您的位置:首页 > 编程语言 > C语言/C++

Effective C++(条款51-55)

2016-01-27 18:35 615 查看
条款51:编写new和delete时需固守常规

条款50已经解释什么时候你会想要写个自己定制的operator new和operator delete,这个条款告诉你编写定制的内存管理器时必须遵守什么规则。

operator new应该内含一个无穷循环,并在其中尝试分配内存。实现一致性operator new必须返回正确地值,内存不足时必须调用new-handler函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new。因为operator new实际上不止一次尝试分配内存,并在每次失败后调用new-handler函数。只有当指向new-handler函数的指针是null,operator new才会抛出异常。

下面是一个non-member operator new伪码:

void* operator new(std::size_t size) throw(std::bad_alloc)//你的版本可能接受额外参数
{
using namespace std;
if(size==0){
size=1;
}
while(true)
{
尝试分配size bytes;
if(分配成功)
return(一个指针,指向分配得来的内存);
//分配失败,找出目前的new-handler函数
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)  (*globalHandler)();
else throw std::bad_alloc();
}
}


许多人没有意识到,operator new成员呢函数会被derived class继承。写出定制型内存管理器的一个常见理由是为了针对特定class对象分配行为提供 最优化,却不是为了该类的任何derived class。如果base class专属的operator new不是设计针对其继承类使用提供最优化,处理这种情形的最佳做法是将“内存申请量错误”的调用行为改为采用标准operator new 。像这样:

void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if(size!=sizeof(Base))
return::operator new(size);//令标准operator new处理
...
}


如果你决定写一个operator new[],记住,唯一需要做的事情就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。包括假设array内的每个对象的大小是sizeof(base),或元素个数是byte申请数/sizeof(base)等等。

operator delete的情况更简单,你需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以你必须兑现这个承诺。

下面是non-member operator delete的伪码:

void operator delete(void* rawMemory) throw()
{
if(rawMemory==0) return;//如果被删除的是一个null指针,那就什么都不做
现在,归还rawMemory所指的内存;
}


这个函数的member版本也很简单,只需要多加一个动作检查删除数量。万一你的class专属的operator new将大小有误的分配行为转交标准new执行,你也必须将大小有误的删除行为转交::operator delete执行:

void Base::operator delete(void* rawMemory,std::size_t size) throw()
{
if(rawMemory==0) return;
if(size!=sizeof(Base)){
::operator delete(rawMemory);//令标准版本处理此申请
return;
}
现在,归还rawMemory所指的内存;
return;
}


如果你的base class遗漏了virtual析构函数,operator delete可能无法正常工作,因为c++传给operator delete的size_t数值可能不正确。

条款52:写了placement new也要写placement delete

重要概念:如果operator new接受的参数除了一定会有的那个size_t之外该有其他,这便是个所谓的placement new。类似于new的placement版本,operator delete如果接受额外参数,便称为placement deletes。

placement new名称的来源:operator new接受的参数除了一定会有的那个size_t之外该有其他,其中最特别的一个是“接受一个指针指向对象被构造之处”,那样的operator new长相如下:

void* operator new(std::size_t size,void* pMemory) throw();


这个版本的new已被纳入C++标准程序库,你只要#include<new>就可以使用它。实际上它正是placement new的命名根据:一个特定位置上的new。当人们说到属于placement new时,一般有两种含义:上述的operator new特定版本,另一个是带有任意额外参数的operator new。往往能够根据上下文推测出其含义。

引入:当你写y一个new表达式像这样:

Widget* pw=new Widget;//公有两个函数被调用,一个是用来分配内存的operator new,另一个是Widget的默认构造函数
//假设第一个函数调用成功,第二个函数却抛出异常,步骤一的内存分配所得必须取消并恢复旧观
//取消步骤一并恢复旧观的责任客户无法做到,这就要落在C++运行期系统身上
//运行期系统要调用步骤一中所调用的operator new相应operator delete版本


规则:当你只是用正常形式的operator new和delete时,运行期系统毫无疑问可以找到相对应的delete。但是当你使用一个非正常形式的new时,问题就出现了,系统不知道要调用哪一个delete版本。

当你写一个placement operator new,请确定写出相对应的placement operator delete,即两者带有相同额外参数。那么当new的内存分配动作需要取消并恢复旧观时就可以调用相对应的operator delete,消除代码中可能存在的内存泄露

class Widget{
public:
...
static void* operator new(std::size_t,size,std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory)throw();//正常版本
static void operator delete(void* pMemory,std::ostream& logStream) throw();//placement delete
//static成员要在类外定义
...
};
//这样声明定义后,如果以下语句引发Widget构造函数抛出异常,则调用placement delete,不会引发内存泄露
Widget* pw=new Widget;
delete pw;//调用正常的operator delete


由于成员函数的名称会掩盖其外围作用域中的相同名称,你必须小心避免让class专属的new和delete掩盖客户期望的其他版本(包括正常版本)。同样的道理,derived class中的operator new和operator delete会掩盖global版本和继承而来的base class 版本

对于撰写内存分配函数,你需要记住的是,缺省状态下C++在global作用域内提供一下形式的operator new:

void* operator new(std::size_t size) throw(std::bad_alloc);//normal new
void* operator new(std::size_t size,void*)throw();//placement new
void* operator new(std::size_t size,const std::nothrow_t&)throw();//nothrow new,内存分配失败时返回null,并不抛出异常


如果你在class内声明任何的operator new,它会遮掩上述这些标准形式。对每一个可用的operator new也请确定提供对应的operator delete。如果你希望这些函数有着正常的行为,只要令你的class专属版本调用global版本即可。

做法很简单,建立一个base class,内含所有正常行驶的new和delete:

class StandardNewDeleteForms{
public:
//normal new/delete
static void* operator new(std::size_t size)throw(std::bad_alloc)
{return ::operator new(size);}
static void operator delete(void* pMemory)throw()
{::operator delete(pMemory);}

//placement new/delete
static void* operator new(std::size_t size,void* ptr)throw()
{return::operator new(size,ptr);}
static void operator delete(void* pMemory,void* ptr)throw()
{return::operator delete(pMemory,ptr);}

//nothrow new/delete
static void* operator new(std::size_t,const std::nothrow_t& nt) throw()
{ return::operator new(size,nt);}
static void operator delete(void* pMemory,const std::nothrow_t& nt) throw()
{::operator delete(pMemory);}


凡是想以自定形式扩充标准形式的客户,可利用继承机制及using 声明式取得标准形式:

class Widget:public StandardNewDeleteForms{
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;//让这些形式可见
static void* operator new(std::size_t size,std::ostream& logstream)throw(std::bad_alloc)//自定义placement new
static void operator delete(void* pMemory,std::ostream& logstream)throw()//添加一个对应的placement delete
...
}


条款53:不要轻忽编译器的警告

问题:许多程序员习惯性忽略编译器的警告,他们认为如果问题很严重,编译器应该给出一个错误信息而不是警告信息。

引子:举个例子,下面是或多或少会发生在每个人身上的一个错误

class B{
public:
virtual void f() const;
};

class D:public B{
public:
virtual void f();
};
//这里希望以D::f重新定义virtual函数B::f,但是B中的f()是const成员,而在D中它没有被声明为const


于是有的编译器发出一个警告:f() hides virtual B::f()

这个编译器试图告诉你声明于B中的f()并没有在D中被重新声明,而是整个被遮掩了。如果忽略这个警告,几乎肯定导致错误的程序行为。

结论:所以,在你打发某个警告信息之前,请确定你了解它意图说出的精确意义。

严肃对待编译器发出的警告信息,努力在你的编译器的最高警告级别下争取“无任何警告”的荣誉。

不要过度依赖编译器的报警能力,因为不同的编译器对待事物的态度并不相同。一旦移植到另一个编译器上,你原本的警告信息可能会消失

条款54:让自己熟悉包括TR1在内的标准程序库

TR1代表“Technical Report 1",是一份文档,TR1自身只是一份规范,它宣告了一个新版C++的来临,TR1提供的机能几乎对每一种程序库和每一种应用程序都带来利益。

在概括论述TR1有些什么之前,先回顾下C++标准程序库有哪些主要成分

STL(标准模板库),覆盖容器(如vector,string,map)、迭代器、算法(如find,sort,transform)、函数对象(如less,greater)、各种容器适配器和函数适配器
Iostreams,覆盖用户自定缓冲功能、国际化I/O,以及预先定义好的对象cin ,cout,ceer,clog
国际化支持,包括多区域能力,像wchar_t和wstring等类型都对促进uniclde有所帮助
数值处理,包括复数模板和纯数值数组
异常阶层处理,包括base class exception及其derived class logic_error和runtime_error,以及更深继承的各个classes
以前版本的标准库程序也包含在内

TR1详细论述了14个新组件(也就是程序库机能单位),统统放在std命名空间内,更正确地是在其嵌套命名空间tr1内。因此,TR1组件shared_ptr全名是std::tr1::shared_ptr

智能指针:tr1::shared_ptr和tr1:;weak_ptr。
tr1::function,此物得以表示任何可调用物(也就是任何函数或函数对象),只要其签名符合目标。
tr1::bind,它可以和const和non-const成员函数协同运作,可以和by-reference参数协同运作,而且不需特殊协助就可以处理函数指针。它是第二代绑定工具
hash tables,用来实现set,multiset,map,mutimap。以hash为基础的这些TR1容器内的元素并无任何可预期的次序
正则表达式,又称常规表示法、正规表示法,使用单个字符串来描述、匹配一系列符合某个句法规则的字符串
tuples(变量组),pair只能持有两个对象,tr1::tuple可持有任意个数的对象
tr1::array,大小固定,并不使用动态内存,是一个支持成员函数如begin何end的数组
tr1::mem_fn,这是个语句构造上与成员函数指针一致的东西
tr1::reference_wrapper,一个”让reference的行为更像对象“的设施,实际上容器只能持有对象或指针,它可以造成容器”犹如持有引用“
随机数生成工具
数学特殊函数,包括Laguerre多项式、Bessel函数等
C99兼容扩充
Type Traits,一组traits class,用以提供类型的(type)编译器信息
tr1::result_of,这是个template,用来推导函数调用的返回类型

虽然若干TR1成分纳入了某些”前TR1“组件能力,但其实TR1是对标准程序库的纯粹添加,没有任何TR1组件用来替换既有组件,所以早期(写于TR1之前)代码仍然有效。

TR1自身只是一份规范,为获得TR1提供的好处,你需要一份实物,一个好的实物来源是Boost。

条款55:让自己熟悉Boost
Boost是一个高质量、源码开放、平台独立、编译器独立的程序库。
它的网址是http://boost.org
Boost提供许多TR1组件实现品,以及其他许多程序库。

Boost程序库大概包含以下几种主题:
字符串与文本处理,容器,函数对象和高级编程,泛型编程,模板元编程,数学和数值,正确性与测试,数据结构,语言间支持,内存,杂项等。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: