第四章 设计与声明

条款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 printNameAndDisplaywindow w

{

cout<<w.name;

w.display();//dispalywindow基类的虚函数

}

当向此函数传递一个派生类对象时,总是调用基类的display();因为派生类的对象被切割。

然而以pass by reference-to-const传递w

void printNameAndDisplayconst window& w

{

cout<<w.name;

w.display();//dispalywindow基类的虚函数

}

由在于编译器的底层,reference往往是以指针的方式实现的,因此pass by reference 通常意味着真正传递的是指针。(类似动态邦定)

一般而言,我们可以合理假设传值的成本并不昂贵的唯一对象就是内置类型,STL的迭代器和函数对象。

 

条款21:必须返回对象时,别妄想返回其reference

在坚定追求pass by reference的纯度中,人们一定会犯下一个致命的错误,返回一些reference指向其实并不存在的对象。

所谓的reference只是一个名称,代表某个已经存在的对象。任何时候看到一个reference的时候,就应该问自己,它的另一个名称是什么?

例:

一个类的函数:

const Rational operator*const Rational& lhsconst Rational & rhs

{

Rational resultlhs.n*rhs.n, lhs.d*lhs.d;

return  result;//这种做法是合理的

    }

   下面返回引用,注意错在哪???

const Rational&  operator*const Rational& lhsconst Rational & rhs

{

Rational resultlhs.n*rhs.n, lhs.d*lhs.d;

return  result;

}

这个函数有个严重的问题:返回局部变量的引用!!!

-------------

在下面在堆中建立对象!!!

const Rational & operator*const Rational& lhsconst Rational & rhs

{

Rational* result=new Rationallhs.n*rhs.n, lhs.d*lhs.d;

return  *result;

}

这也有问题谁释放堆中的资源

-------------

下面在static内存中创建对象

const Rational&  operator*const Rational& lhsconst Rational & rhs

{

static Rational result

        resultlhs.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-membernon-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& lhsconst 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命名空间的任何东西,但是可以为标准模板制造特例化。

然而这个版本无法通过编译,因为他企图访问ab内的私有指针,我们可以把此函数声明为友元,但是我们可以令Widget声明一个名为swappublic成员函数做真正的置换操作,然后将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问题

 

然而假设widgetwidgetImap都是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对象,因而调用swapC++都会根据查找规则,找到Widgetsttf中的widget专属版本。

如果我们希望我们的”class专属版本“swap在尽可能多的语境下被调用,我们还需要在该class所在的命名空间内写一个non-number版本的std::swap特化版本。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值