条款18:让接口容易被正确使用,不易被误用
(1)“促进正确使用”的办法包括建立接口的一致性,以及与内置类型的行为兼容。
(2)“阻止误用”的办法包括建立型类型,限制类型上的操作,束缚对象的值,以及消除客户的资源管理责任;
(3)智能指针的定制删除器。
条款19:设计class犹如设计type
每个class都需要面对的问题
(1)新type的对象应该如何创建和销毁?
(2)对象的初始化和对象的赋值的区别应该是什么样的?
(3)新type的对象如果被依值传递,着意味什么?
(4)什么是新type的“合法值”?
(5)新的type需要配合某个继承图系吗?
(6)新的type需要什么样的转换?
(7)什么样的操作符和函数对此新type而言是合理的?
(8)什么样的标准函数应该驳回?
(9)谁该取用新的type成员?
(10)什么是新的type的“未声明接口”?
(11)新type有多一般化?
(12)新type真的被需要吗?
条款20:宁以pass-by-reference-to-const替换psas-by-value
一般情况下pass by reference-to-const的效率比psss-by-value的高,原意传值设计到对象的构造与析构。
而且常量引用可以避免对象切割问题。
例:
void printNameAndDisplay(window w)
{
cout<<w.name;
w.display();//dispaly是window基类的虚函数
}
当向此函数传递一个派生类对象时,总是调用基类的display();因为派生类的对象被切割。
然而以pass by reference-to-const传递w
void printNameAndDisplay(const window& w)
{
cout<<w.name;
w.display();//dispaly是window基类的虚函数
}
由在于编译器的底层,reference往往是以指针的方式实现的,因此pass by reference 通常意味着真正传递的是指针。(类似动态邦定)
一般而言,我们可以合理假设传值的成本并不昂贵的唯一对象就是内置类型,STL的迭代器和函数对象。
条款21:必须返回对象时,别妄想返回其reference
在坚定追求pass by reference的纯度中,人们一定会犯下一个致命的错误,返回一些reference指向其实并不存在的对象。
所谓的reference只是一个名称,代表某个已经存在的对象。任何时候看到一个reference的时候,就应该问自己,它的另一个名称是什么?
例:
一个类的函数:
const Rational operator*(const Rational& lhs,const Rational & rhs)
{
Rational result(lhs.n*rhs.n, lhs.d*lhs.d);
return result;//这种做法是合理的
}
下面返回引用,注意错在哪???
const Rational& operator*(const Rational& lhs,const Rational & rhs)
{
Rational result(lhs.n*rhs.n, lhs.d*lhs.d);
return result;
}
这个函数有个严重的问题:返回局部变量的引用!!!
-------------
在下面在堆中建立对象!!!
const Rational & operator*(const Rational& lhs,const Rational & rhs)
{
Rational* result=new Rational(lhs.n*rhs.n, lhs.d*lhs.d);
return *result;
}
这也有问题谁释放堆中的资源
-------------
下面在static内存中创建对象
const Rational& operator*(const Rational& lhs,const Rational & rhs)
{
static Rational result;
result(lhs.n*rhs.n, lhs.d*lhs.d);
return result;
}
这个用一个隐藏很深的问题
当出现下面的情况时:
if((a*b)==(c*d))
{ ...... }
else
{ ...... }
这个条件在if中永远是true,原意是返回的是static,由于返回的是static的引用,
每次都返回当前的最新值,所以两者永远相等。
所以如果“必须返回体格对象”就返回对象吧,正确才是第一位。
条款22:将成员变量声明为private
原意:(1)语法一致性
(2)使用函数可以让成员变量的处理更加精确
(3)封装
将成员变量隐藏在函数接口的背后,可以为“所有可能实现”提供弹性。
其实只有两种访问权限:private(提供封装)和其他(不提供封装),protected并不比public更具封装性。
条款23:宁以non-member,non-friend替换number函数
面向对象守则要求数据应该尽可能的被封装,然而与直观相反,number函数的封装性比non-number函数低。
所以(1)封装的原因:他时我们能够改变事物而只影响有限客户。
(2)对象的内存:越少的代码可以看到数据,封装性越高。
在C++中,较自然的做法就是把类和类相关的non-number函数位于相同的namespace中,这样可以增加封装性,包裹弹性和机能扩充性。
条款24:若所有参数皆需要类型转换,请为此采用non-number函数
例:
class Rational
{
......
const Rational operator*(const Ration& rhs ) const;
};
result=oneHalf*2;//对
result=2*oneHalf;//错
原因result=oneHalf.operator*(2);//T
result=2.operator*(onHalf);//F
只有当参数被列于参数列内,这个参数才才可以发生隐式类型转换
然而如果想支持混合算数运算可以让类提供类型转换运算符,
或者让operator*成为一个non-member函数:
const Ration operator*(const Rational& lhs,const Rational& rhs)
{
return Ration(... ,... );
}
所以如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是一个non-number。
条款25:考虑写出一个不抛出异常的swap函数
所谓swap(置换)两对象值,意识是将两个对象的值彼此赋予对方。缺省状态下swap动作可由标准程序库提供的swap算法完成。其典型的预期是:
namespace std{
template<typename T> void swap(T& a, T& b)
{
T temp(a) ; a=b; b=temp;
}
}
只要类型T支持copying,缺省的swap 实现代码就会帮我们置换类型为T的两个对象,我么不在需要做任何操作。
然而这种效率非常低,我们常在类中以指针交换的方式交换数据:
例:class WidgetItmp{
public:
......
private:
int a,b,c;
vector<double> v;
......
};
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
.......
*pImpl=*(rhs.pImpl);
}
......
private:
WidgetImpl* pImpl;
};
一旦要置换两个widget对象的值,我们唯一需要做的就是置换其pImap指针,但是缺省的swap算法不知道这一点。他不至赋值三个Widget,还复制了三个指针,效率非常的低。
我们希望告诉std::swap:当Widget被置换时真正该做的是置换其内部的pImap指针。实践这个思路的做法是:将std::swap针对Widget进行特化。下面是基本构思,单边一起无法通过:
namespace std{
template<> void swap<Widget>(Widget& a,Widget& b)
{
swap(a.pImpl ,b.pImpl);//这是针对Widget的特例化版本,目前还不能通过编译
}
}
这个函数一开始的“template<>”表示他是std::swap的一个全特例化版本,函数名称之后的<Widget>表示着是一个针对Widget而设计的。换句话述只要一般性的swap template施行于Widget身上便会启动这个版本。通常我们不允许改变在std命名空间的任何东西,但是可以为标准模板制造特例化。
然而这个版本无法通过编译,因为他企图访问a和b内的私有指针,我们可以把此函数声明为友元,但是我们可以令Widget声明一个名为swap的public成员函数做真正的置换操作,然后将std::swap特例化,令他调用该成员函数:
class widget{
......
void swap(Widget& other)
{
using std::swap;//思考这里为何有这个声明,将一般的swap暴露于此,供 swap重载,选择最佳的swap
swap(pImp1, other.pImap1);
}
.....
};
namespace std{
template<> void swap<Widget>(Widget& a,Widget& b)
{
swap(a ,b );//此std::swap模板全特例化的作用是将置换操作转移至自定义的类,而不是std::swap,然后让自定义的类选择到底以什么方式置换
}
}
这种做法做法能通过编译,而且还与STL保持一致性。
=====================================================下面是模板类的swap问题
然而假设widget和widgetImap都是class templates而非classes,也许我们可以试试将widgetImpl内的数据类型加以参数化:
template<typename T> class WidgetImpl {........};
template<typename T> class Widget {........};
在widget内放个swap成员函数可以向以往一样简单,但是我们却在特例化std::swap是遇到了乱流:我们打算写成这样:
namespace std{
template<typemame T>
void swap<Widget<T>>(widget<T>& a,widget<T>& b)
{
a.swap(b);
}
}
这段代码看似合情合理,却不合法。原因,我们企图偏特化一个函数模板,但是C++只允许对类模板偏特化,在函数模板上偏特化是行不通的。
当你打算偏特化一个函数模板时,惯常的做法是添加一个重载版本:像这样
namespace std{
template<typemame T>
void swap (widget<T>& a,widget<T>& b)//这是std::swap的一个重载版本 (swap之后无<>)
{
a.swap(b);
}
}
重载函数模板没问题,但是std是以特殊的命名空间,用户可以全特化std内的模板,但是不允许添加新的template到里面(如模板重载,函数,类等);
解决方法:我们可以声明一个non-number swap让他调用number swap,但不是将non-number swap声明为std的特化或重载,而是和类放在同一个自定义的命名空间。
namespace Widgetsttf{
template<typemame T>
void swap (widget<T>& a,widget<T>& b)
{
a.swap(b);
}
}
现在,任何地点的任何代码如打算置换两个widget对象,因而调用swap,C++都会根据查找规则,找到Widgetsttf中的widget专属版本。
如果我们希望我们的”class专属版本“swap在尽可能多的语境下被调用,我们还需要在该class所在的命名空间内写一个non-number版本的std::swap特化版本。