effective C++(五)

智能指针是行为像指针的对象,而真实指针做的很好的一件事是,支持隐式转换,派生类指针可以转换为基类指针等

在同一个template的不同具现体之间并不存在什么与生俱来的固有关系,如果以带有base-derived关系的B,D两类型分别具现化某个template,产生出来的两个具现体并不带有base-derived关系,因此它们之间并不能相互转换,要让它们之间能转换我们必须将它们的转换函数明确地编写出来

template<typename T>
class SmartPtr{
 public:
 template<typename U>
 SmartPrt(const SmartPtr<U>& other)
  :heldPtr(other.get()){...}
 T* get() const{ return heldPtr;}
 ...
 private:
 T* heldPtr;
};

以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>,这个构造函数根据对象U创建对象T,而U和v的类型是同一个template的不同具现体,有时我们称之为泛化复制构造函数,上面的泛化构造函数没有被声明为explicit,因为原始指针类型之间的转换时隐式的,之所以需要一个T类型的指针是因为让这个行为只有当存在某个隐式转换可将一个U*指针转为一个T*指针时才能通过编译,也即更像真实指针只有当转换合理时才允许转换。

在class内声明泛化复制构造函数并不会阻止编译器生成它们自己的复制构造函数。

模板化的类和非模板化的类有所不同

template<typename T>
class Rational{...}
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{...}
Rational<int> oneHalf(1,2);
Rational<int>result=oneHalf*2;      //无法通过编译

或许我们期望编译器使用构造函数将2转换为Rational<int>,进而将T推导为int,但它们不那么做,因为在template实参推导过程中从不讲隐式类型转换函数纳入考虑,因为在能够调用一个函数之前,首先必须知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型,然后才可将适当的函数具现化出来。解决方式是声明为firend函数

template<typename T>
class Rational{
 public:
 ...
 friend const Rational operator* (const Rational& lhs,const Rational& rhs); //在一个类模板内,template名称可被用来作为template和其参数的
};                                                                              简略表达方式,所以在Rational<T>内我们可以只写Rational
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{...}

为了将inline声明所带来的冲击最小化,做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数

template<typename T> const Rational<T> domultiply(const Rational<T>& lhs,const Rational<T>& rhs);
....
friend const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{ return domultiply(lhs,rhs);}

当编写类模板,而它所提供之与此模板相关的函数支持所有参数之隐式类型转换时,将那些函数定义为类模板内部的friend函数

input迭代器只能向前移动,一次一步,客户只能读取且只能读取一次,它们模仿指向输入文件的阅读指针,代表为istream_iteraotrs,out迭代器情况类似,模仿指向输出文件的涂写指针,ostream_iterators是代表,forward迭代器可以做前两种分类所能做的每一件事,而且可以读或写所指物一次以上,Bidirectional迭代器比上一个分类威力更大,除了可以向前移动,还可以向后移动,stl的list迭代器就属于这一分类,之后最强大的就是随机访问迭代器了。对它们,c++标准库分别提供专属的卷标结构(tag struct)加以确认:

struct input_iterator_tag{};
sturct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public input_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};

这些structs之间的继承关系是有效的is-a关系。所有forward迭代器都是Input迭代器等。

traits,它允许你在编译期间取得某些类型信息,Traits并不是C++关键字或一个预先定义好的构件,它们是一种技术,也是一个C++程序员共同遵守的协议,类型的traits信息必须位于类型自身外,标准技术是把它放进一个template及其一或多个特化版本中,这样的template在标准程序库中有若干个,其中针对迭代器者被命令为iterator_traits;

template<typename IterT>
struct iterator_traits;    //习惯上,traits总是被实现为structs
template<..>
class deque{
 public:
 class iterator{
 public typedef random_access_iterator iterator_category;//iterator_traits的运作方式是,针对每一个类型IterT,在内声明一个typedef名为
 ...                                                        iterator_category用来确认适当IterT的迭代器分类
};
template<...>
class list{
 public:
 class iterator{
 public:
 typedef bidirectional_iterator_tag iterator_category;    //类型IterT的iterator_category其实就是用来表现IterT说它自己是什么
 ...
};

为了支持指针迭代器,我们需要特别针对指针类型提供一个偏特化版本

template<typename IterT>
struct iterator_traits<IterT*>
{
 typedef random_access_iterator_tag iterator_category;
};

iterator_traits<IterT>::iterator_category可在编译期间确定,但是if语句是在运行期才会核定,这不仅浪费时间,也造成可执行文件膨胀,如果想判断编译器核定成功类型,可以使用重载

template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::random_access_iterator_tag)
{
 iter+=d;
}
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::bidirectional_iterator_tag)
{
 if(d>=0){while(d--) ++iter;}
 else {while (d++) --iter;}
}
template<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::input_iterator_tag)
{
 if(d<0){
 throw std::out_of_range("Negative distance");}
 while(d--) ++iter;
}
template<typename IterT,typename DistT>
void advance(Iter& iter,DistT d)
{
 doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
};

使用traits class,建立一组重载函数或函数模板,彼此之间的差异只在于各自的traits参数,令每个函数实现码与其接受之traits信息相应和,建立一个控制函数或函数模板,它调用上述那些重载函数并提供traits class 所提供的信息。

TMP模板元编程,执行于C++编译器,因此可将工作从运行期转移到编译期,这导致一个结果是,某些错误原本通常在运行期才能侦查到,现在可在编译器找出来,另一个结果是,使用TMP的C++程序可能在每一个方面都更高效。traits就是TMP形式的

TMP并没有真正的循环构件,所以循坏效果由递归完成,TMP主要是个函数式语言,TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及递归模板具现化。一个计算阶乘的例子递归表现如下:

template<unsigned n>
struct Factorial{
 enum{ value=n* Factorial<n-1>::value};
};
template<>
struct Factorial<0>{
 enum{value=1};
};

STL容器所使用的堆内存是由容器所拥有的分配器对象(allocator objects)管理

当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数

namespace std{
 typedef void (*new_handler)();
 new_handler set_new_handler(new_handler p) throw();
}

new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler则是获得一个new_handler并返回一个new_handler的函数,set_new_handler声明式尾端的throw()是一份异常明细,表示该函数不抛出任何异常。当operator new无法满足内存申请时,它会不断调用new handler函数,直到找到足够内存,一个良好设计的new-handler函数必须做下事情:1.让更多内存可被使用,2.安装另一个new-handler,3.卸除new-handler(也就是将null指针传给set_new_handler,一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常)4.抛出bad_alloc的异常,5.不返回,直接调用abort。

在new的使用场合用了nothrow对象,widget* pw2=new (std::nothrow) widget;表达式说明当operator new被调用,用以分配足够内存给Widget对象,如果分配失败便返回Null指针,虽然它不会抛出异常,但是类的构造函数却会。

考虑替换operator new或operator delete一般出于下面三个常见理由:用来检测运用上的错误,为了强化效果(当内存被分得太小被无法满足大内存需求,或因为分得太大无法满足小需求),为了收集使用上的统计数据。为了增加分配和归还得速度,为了降低缺省内存管理器带来的空间额外开销,为了弥补缺省分配器中的非最佳齐位,为了将相关对象成簇集中。

即使客户要求0bytes,operator new也得返回一个合法指针。operator new成员函数会被派生类继承!

当定义自己的new和delte时需固守常规,如果你决定写个operator new[],记住唯一需要做的事情就是分配一块未加工内存

class Base{
 public:
 static void* operator new(std::size_t size) throw(std::bad_alloc);
 static void operator delete(void* rawMemory.std::size_t size) throw();
 ...
};
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
 if(size!=sizeof(Base))          //如果大小错误
    return ::operator new(size); //令标准的operator new来处理
 ...                              //否则在这里处理
}
void Base::operator delete(void* rawMemory,std::size_t size) throw();
  if(rawMemory==0) return;            //检查空指针,什么都不用做直接返回       C++保证,删除null指针永远安全
 if(size!=sizeof(Base)){                 //如果大小错误,令标准版本来处理
 ::operator delete(rawMemory);
 return ;
} return;              //现在,归来rawMemory所指内存
};

如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是所谓的placement new,众多版本中,有一个特别有用的是接受一个指针指向对象该被构造之处,长相为

void* operator new(std::size_t,void* pMemory) throw();
//这个new的用途之一是负责在vector的未使用空间上创建对象

考虑如下

Widget* pw=new (std::cerr) Widget;

如果内存分配成功,而Widget构造函数抛出异常,运行期系统有责任取消operator new分配并恢复原样,然后运行期系统无法知道真正被调用的那个operator new如何运作,因此它无法取消分配并恢复原样,运行期系统寻找参数个数和类型都与operator new相同的某个opereator delete,如果找到就调用它,所以写了placement new也要写placement delete,如果没有写,那么当placement new调用失败时就没有operator delete被调用

缺省情况下C++在global作用域内提供以下形式的operator new

void* operator new(std::size_t) throw(std::bad_alloc);   //normal new
void* operator new(std::size_t,void*) throw()          //placement new
void* operator new(std::size_t,const std::nothrow_t&) throw();  //nothrow new

除非你的意识是要阻止class的客户使用这些形式,否则请确保它们在你所生成的任何定制型operator new之外还可以使用

class Base{
 ...
 static void* operator new(std::size_t size,std::ostream& logStream)
 throw(std::bad_alloc);
 ...
};
Base* pb=new Base;             //错误,正常形式的operator new被掩盖
Base* pb=new(std::cerr) Base;   //正确调用Base的placement new
class Dervied:public Base{
 public:
 ...
 static void* operator new(std::size_t size) throw (std::bad_alloc); //重新声明正常形式的New
...
};
Derived* pd=new (std::clog) Derived;  //错误,Base的placement new被掩盖了
Derived* pd=new Derived;           //没问题,调用Derived的operator new

解决方法为,在类里声明所有正常形式的new和delete,或者继承标准形式,然后使用using,让基类函数在派生类中可见

需要多注意TR1中详细叙述了的14个新组件。

猜你喜欢

转载自blog.csdn.net/weixin_38893389/article/details/79533845