effective C++读书笔记(四)

4. 设计和声明(Designsand Declarations)

这个部分主要解决设计的问题,具体就是,如何让你的设计更加健壮,如何让代码更好得体现封装等面向对象的思想

 

条款18: 让接口容易被正确使用,不易被误用(Makeinterfaces easy to use correctly and hard to use incorrectly)

本条款主要讲接口设计的基本原则:好的接口很容易正确使用,不易被误用。具体的技巧包括:

1引入一些别的类,例如书中的Day, Month , Year

2促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。例如STL中涉及大小时一般都用函数size()

3阻止误用的办法包括建立新类型、限制类型上的操作(例如使用const),束缚对象值,以及消除客户对资源管理的责任(例如使用智能指针trl::shared_ptr)。

4trl::shared_ptr支持定制型删除器,这可以防范“cross-DLLproblem(即对象在一个dll中被new,在另一个dll中被delete)问题,也可以用来自动解锁互斥锁(mutexes)

 

条款19:设计class犹如设计type(Treatclass design as type design)

本条款主要讲设计class时应该考虑的一些问题。设计class的能力还要在工程实践中仔细体会。应该遵守的一些设计规范。

   设计一个良好的类,或者称作类型,考虑一下设计规范:

·        新类型的对象应该如何被创建和销毁?

·        对象的初始化和对象的赋值该有什么样的差别?

·        新类型的对象如果被passedby value(值传递),意味着什么?

·        什么是新类型的合法值

·        你的新类型需要配合某个继承图系吗?

·        你的新类型需要什么样的转换?

·        什么样的操作符和函数对此新类型而言是合理的?

·        什么样的标准函数应该驳回?

·        谁该取用新类型的成员?

·        什么是新类型的未声明接口

·        你的新类型有多少一般化?

·        你真的需要一个新类型吗?   

 

条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)

本条款主要讲函数参数传递的原则:尽量以pass-by-reference-to-const替换pass-by-value例如用下面的第一行替换第二行:

bool print(Student s);
bool print(const Student &s);

因为第二种通常比较高效(因为pass-by-value时函数参数是实参的副本。需要调用对象的拷贝构造函数和析构函数;而pass-by-reference-to-const则没有任何新对象被创建。),并可避免切割问题(slicingproblem)(所谓切割问题,是指派生类的对象传给基类的对象的参数时,派生对象中跟基类相比那些特有的性质会全被切割)。

需要注意的是,该规则并不适用于内置类型,以STL 的迭代器和函数对象。对它们而言,pass-by-value 往往比较适当。

一般而言,你可以合理假设“pass-by-value并不高贵”的唯一对象就是内置类型和STL的迭代器和函数对象。至于其他任何东西都请遵守本条款的忠告,尽量以pass-by-reference-const替换pass-by-value

 

条款21: 必须返回对象时,别妄想返回其reference(Don’ttry to return a reference when you must return an object)

当一个函数必须返回新对象时,就让它返回新对象。绝不要返回pointerreference指向一个localstack 对象,或返回reference指向一个heap-allocated对象,或返回pointerreference指向一个localstatic 对象而有可能同时需要多个这样的对象。举例说明如下:

1)如果返回pointerreference指向一个localstack 对象(函数的stack对象指函数的参数、局部变量、返回值;存放在函数stack空间):

const Rational& operator* (const Rational& lhs,const Rational& rhs)
{
 Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //警告!糟糕的代码!
 return result;
}

解释:local对象在函数退出前被销毁了

2返回reference指向一个heap-allocated对象(Heap-based对象指有new malloc之类的函数分配的对象,存放在heap空间)

const Rational& operator* (const Rational& lhs,const Rational& rhs)
{
 Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
 return *result;
}

解释:容易导致内存泄露,因为没有谁对new出来的对象实施delete操作。

3)返回pointerreference指向一个localstatic

const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
 static Rational result;
 result = ... ;
 return result;
}

在下面的调用中,将出现错误:

if((a * b) == (c * d)){
 //当乘积相等时,做适当的相应动作;
}
else {
 //当乘积不等时,做适当的相应动作;
}

解释:(a* b) == (c * d)永远为true,因为两次operator*调用的确调用了各自的staticRational对象值,但他们返回的都是Reference,因此调用端看到的永远是static Rational现值

因此,最终结论:别返回local对象的reference,还是返回对象,虽然要调用拷贝和析构函数,但是,没办法,该花的还得花

一个必须返回新对象的函数的正确写法是:就让那个函数返回一个新对象。

Inlineconst Rational operator * (const Rational& lhs, constRational& rhs)

{

       Return Rational (lhs.n * rhs.n, lhs.d *rhs.d);

}

 

条款22: 将成员变量声明为private(Declaredata members private)

本条款建议将类的成员变量声明为private;并且说明了protected并不比public更具封装性

1、可以实现对变量权限的精确控制(只读,只写,可读可写)

2、只要保证客户端接口不变,我们可以讲接口实现的细则进行任意的改变,并且,我们在接口中可以实现对成员变量值的有效性的检测

3、有点哲学的就是,更好的封装,因为越少的东西被外界看到,越封装。如果一个成员是public的,那么撤掉他会改动很多代码的。所以public没有封装,同样,protected也没有封装,只有private不被外键看见,才能算是封装。

 

条款23: 宁以non-membernon-friend 替换member 函数(Prefernon-member non-friend functions to member functions)

有时候,可以用non-membernon-friend替换member 函数,因为这样可以增加封装性class的成员函数少了,可以访问private成员的函数少了,封装性提高了);包裹弹性和机能扩充性。

后面两点是因为:可以把non-membernon-friend函数和class放在同一个namespace,给namespace内添加函数很方便。并且,namespace可以跨越多个源文件而class不可以。C++标准库就是在10多个头文件(<vector><algorithm><memory>等)中,每个头文件实现std的某些机能。

参考文中示例的命名空间。

 

条款24:若所有参数皆需类型转换,请为此采用non-member 函数(Declarenon-member functions when type conversions should apply to all parameters)

如果需要对某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member的。举个例子:将运算符重载函数定义为non-member的,这样可以让所有参数被列于参数列内,对所有参数实行隐式类型转换。

书上两句核心的话:只有当参数被列于参数列内,这个参数才是隐式转换的合格参与者,地位相当于被调用之成员所隶属的那个对象”---this对象,绝不是隐式转换的合格参与者

例子:

classRational{

   const Rational operator* (const Rational & rhs) const ;

}

RationaloneHalf(1 ,2) ;

result= oneHalf * 2  // OK

result= 2*oneHalf  // error

如果Rationalexplicit单参数构造函数,那么上面两个都错

 

如果希望上面两个形式都对,应该这么定义:

constRational operator* (const Rational & lhs , const Rational & rhs)

{

   ....

}

 

结论:如果你需要为某个函数的所有参数(包括this)进行类型转换,那么这个函数必须是non-member

 

条款25:考虑写出一个不抛异常的swap函数(Considersupport for a non-throwing swap)

如果class内运用了“pimplpointerto implementation手法,std中的默认swap函数效率会比较低,因为默认的swap需要调用copy构造函数和copyassignment操作符。解决方法是:自己提供一个member swap,再提供一个non-memberswap来调用前者

标准库有一个swap用于交换两个对象值

namespacestd{

   template<typename T>

   void swap(T& a , T& b)

   {

       T temp(a) ;

       a = b ;

       b = temp ;

   }

}

所以,只要对象的类型T支持copyingcopyctor copy assignment),那么你就可以用这个标准库的swap,但是,你必须保证,你swap的时候,上述操作是你需要的,有的时候,没有必要,尤其是pimplpointerto implementation)手法。

 

例如下面这个例子:

classWidgetImpl{

public:

   ....

private:

   int a , b , c  ;

   ....   //好多成员

};

 

classWidget

{

public:

  Widget(const Widget & rhs) ;

   Widget& operator=(const Widget & rhs)

   {

        *pImpl = *rhs.pImpl // 复制所指的对象

   }

private:

   WidgetImpl * pImpl ;

};

一旦要交换两个Widget,我们其实就让两个指针换一下就可以了,没有必要把指针所指的内容互换,那样效率太低。

 

想法1

namespacestd{

   template<>  // 全特化

   void swap<Widget> (Widget & a,  Widget & b)

   {

       swap(a.pImpl , b.pImpl) ;

   }

}

上面这个想法编译不过,因为pImplprivate的,是不能被外界访问的。

 

想法2 加入类的成员函数swap,让模板函数去调用

classWidget{

public:

   void swap(Widget & other)

   {

        usingstd::swap ;  // 后面讲

       swap(p.Impl , other.pImpl) ;

   }

   ...

};

namespacestd{

   template<>  // 全特化

   void swap<Widget> (Widget & a,  Widget & b)

   {

       a.swap(b) ;

   }

}

通过上面的写法,我们就能得到正确的,针对类别Widgetswap函数。这就叫为你的class特化std::swap,并令它调用你的swap函数。

但是,问题,如果我现在是类模板,怎么办?

 

现在的类是:

template<typenameT>

classWidgetImpl{...}

template<typenameT>

classWidget{...}

如果你现在这么做:

namespacestd{

   template<typename T>  // 偏特化 

   void swap<Widget> (Widget<T> & a,  Widget<T> &b)

   {

       a.swap(b) ;

   }

}

sorry不行了,原因有以下2点:

1、因为C++只允许对classtemplate进行偏特化,不对能functiontemplate进行偏特化。

2、在std这个命名空间,不可以添加新的templatestd里。上面,我们能加东西到std是因为,为标准的templates(eg: swap)制造特化版本。

为了解决上述2两个问题,引入下面的解决方法

 

想法3:转为classtemplate量身打造

定义一个专门的命名空间,放入我们所有的东西

namespaceWidgetStuff{

   template<typename T>

   class WidgetImpl{...}

   template<typename T>

   class Widget{...} // 这个里面有一个swap函数

 

   template<tyoename T>

   void swap(Widget<T>& a , Widget<T>& b)

   {

         a.swap(b);  

   }

}

 

我们在用swap函数的时候,应该这么用:

template<typenameT>

voiddoSomething(T& obj1 , T& obj2)

{

   using std::swap ;  // 这样即使没有T的专属版本的swap函数 

    swap(obj1, obj2)  ;

}

 

上面就结束了我们swap函数的制作过程,那么为什么可行呢?这个都是利用了C++的名称查找规则

编译器用实参取决之查找规则,找出WidgetStuff中的swap函数,如果命名空间中没有T的专属swap函数,编译器就用std::swap,即使如此,编译器还是喜欢std::swapT的专属版本,就是想法2会选中。所以,优先级顺序是想法3--》想法2--》标准库自己的swap

 

总结:

1、弄清楚什么时候我们需要自己写swap函数

2、在class 或者 classtemplate提供一个publicswap函数

3、在class 或者 classtemplate的命名空间中提供一个non-memberswap ,调用2中的swap

4、如果你正在编写一个class(不是template),为class特化std::swap。并令他调用2中的swap

5、当你实际用swap时,先usingstd::swap,然后不加任何修饰符,赤裸裸调用swap

成员版swap决不能抛出异常,因为很多异常安全性代码都是通过类的swap成员函数保证的,并且,我们自己去写swap,一般都是用指针,内置类型,这些类型不会抛异常,写swap是为了高效

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值