Effective C++ 读书笔记(四)

4 设计与声明

条款18:让接口容易被正确使用,不易被误用

n  好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质 。

n  促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。

n  阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任

n  tr1::shared_ptr支持定制型删除器。这可防范dll问题,可被用来自动解除互斥锁。

条款19:设计class犹如设计type

当你定义一个新class,你也就定义了一个新type。几乎每一个class都要求回答如下提问:

  1. 新type的对象应该如何创建和销毁。
  2. 2.         对象的初始化和对象的赋值有什么样的差别?
  3. 新type的对象如果被pass-by-value意味着什么? copy构造函数用来定义一个type的pass-by-value该如何实现
  4. 什么是新type的合法值?
  5. 5.         你的新type需要配合某个继承图系吗?

如果你继承自某些已有的classes,你就受到那些classes的束缚,特别是它们的函数是virtual还是non-virtual。

如果你允许其他class继承你的class,会影响你所声明的函数——是否为virtual。

  1. 6.         你的新type需要什么样的转换?

允许类型T1隐式转换为T2,就必须在T1内写一个类型转换函数(operator T2)或在T2中写一个non-explicit-one-argument的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数。

  1. 什么样的函数和操作符对于新type是合理的。
  2. 什么的标准函数应该被驳回?那些正是你必须声明为private者(条款6)。
  3. 该谁取用新type的成员?
  4. 什么是新type的未声明接口?
  5. 你的新type有多么一般化?(或许你并非定义一个新type,而是定义一整个types,考虑定义一个新的class template)。
  6. 你真的需要一个新的type吗?

 

条款20:宁以pass-by-ref-const替换pass-by-value

n  尽量以pass-by-ref-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。

class Person{

public:

  Person();

  virtual ~Person();

  …

};

class Student: public Person{

public:

  Student();

  ~Student();

  …

}

bool validateStudent(Student s);

Student plato;

bool platoIsOk = validateStudent(plato);

//Student的copy构造函数会被调用,以plato为蓝本将s初始化。

替代方案:bool validateStudent(const Student&  s);

以引用为传递方式效率高。修订后的参数中的const是重要的。原先的是by value传递Student,因此调用者知道他们受到保护,不会对传入的plato作任何改变,只能对它的复件s作改变。因此const是必要的。

 

by ref传递的另一个好处是可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些物化性质全被切割掉了,仅留下一个base class对象。

 

n  以上规则并不适用内置内型,以及 STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

 

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

n  绝不要返回pointer或ref指向一个local stack对象,或返回 ref反射一个heap-allocated对象,或返回pointer或ref指向一个local static对象。

一个有理数的class内含一个函数用来计算有理数的乘积:

class Rational{

public:

        Rational(int numerator = 0, int denominator = 1);

         …

private:

         int n,d;

         friend Rational operator* (const Rational& lhs, const Rational& rhs); //以by-value返回结果

};

why要以by value返回结果,by ref不可以吗?

ref只是一个名称,代表一个既有对象。当看到一个ref时,定要想到它的另一个名称是什么?

在调用operator *之前,内含乘积的Rational对象不可能存在。例如:

Rational a(1, 2), b(3, 5);

Rational c = a*b; //原本存在一个3/10的对象是不合理的

创建新对象在stack空间或heap空间上。

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

{

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

         return result; //函数调用一结束,result就会被释放,ref指向一个已释放的对象,Danger!

}

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

{

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

         return *result; //如何释放result??会造成资源泄漏

}

例如:Rational w, x, y, z;

           w =x*y*z;

 

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

         static Rational result;

         …

         return result;

}

情况更差:Rational a, b, c, d;

if( a*b == c*d)//总为true;

{…}

 

条款22:将成员变量声明为private

n  将成员变量声明为private。 这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。

n  protected并不比public更具封装性。

假设我们有一个protected成员变量,而我们最终取消了它?有多少代码被破坏?所有使用它的derived class都会被破坏,那往往是个不可知的大量。一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。

 

条款23:宁以non-member、non-friend替换member函数

举例来说:

class WebBrowser{

public:

         …

         void clearCache();

         void clearHistory();

         void removeCookies();

         …

};

class WebBrowser{

public:

void clearEverything();//调用上面的三个函数

};

void ClearBrowser(WebBrower& wb)

{…

//通过wb调用上面的三个函数。

}

哪一个较好?member函数?还是non-member函数?

面向对象守则要求,数据以及操作数据的函数应该捆绑在一起,这意味着member函数较好。

但clearEverything实际上只是一个便利函数,一个像WebBrowser这样的 class有大量的便利函数,某些与书签有关,某些与打印有关,还有与cookie有关,通常用户只对其中的部分感兴趣。没道理只对cookie便利感兴趣的客户也要与书签发生关系。

//头文件webbrowser.h

namespace WebBrowserStuff{

class WebBrowser{ … };

//核心机能,所有客户都需要

}

//头文件browserbookmarks.h

namespace WebBrowserStuff{

//与书签相关的便利函数

}

//头文件browsercookie.h

namespace WebBrowserStuff{

//与cookie相关的便利函数

}

         这正是C++标准程序库组织的方式。标准程序库并不是拥有单一头文件并在其内含std命令空间内的每一个东西,而是有数十个头文件<vector> <algorithm>等,每个头文件声明为std的某些机能。

         将所有便利函数放在多个头文件中但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。

 

条款24:若所有参数皆需类型转换,请为此采用non-member函数

class Rational{

public:

         Rational(int numerator = 0, int denominator = 1);

//构造函数不为explicit,允许int-to-Rational隐式转换

         int numerator( ) const;

         int denominator( ) const;

private:

         …

};

乘法:

class Rational{

public: …

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

};

Rational oneEighth(1, 8), oneHalf(1, 2);

Rational result = oneHalf * oneEihth; //OK

result = oneEighth * oneHalf; //OK

 

但result = oneHalf  * 2; //OK, oneHalf.operator*(2)

result = 2 * oneHalf; //Error, 2.operator*(oneHalf)

编译器也会尝试寻找operator*(2, oneHalf),但没有定义这样的函数为什么2*oneHalf没有发生隐式转换?只有参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。

解决办法

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

{

         return Rational(lhs.numerator() * rhs.numerator(), lhs.deominator() * rhs.deominator());

}

result = oneHalf  *  2; //OK

result = 2 * oneHalf; //OK

 

无论何时如果你可以避免friend函数就该避免,因为就像真实世界一样,朋友带来的麻烦往往多过其价值。当然有时候friend有其正当性,但这个事实依然存在:不能够只因函数不该成为member,就自动让它成为friend。

 

条款25:考虑写出一个不抛异常的swap函数

STL标准的swap实现如下:

namespace std{

template<typename T>

void swap(T& a, T& b)

{

     T temp(a);

     a = b;

     b = temp;

}

}

 

n  如果你提供一个member swap,也请提供一个non-member swap用来调用前者。对于 classes非templates,请特化std::swap。

class WidgetImpl{

public:…

private:

int a, b, c;

std::vector<double> v;

};

class Widget{

public:

Widget(const Widget& rhs);

Widget& operator=(const Widget& rhs)

{ …

*pImpl = *(rhs.pImpl);

}

private:

WidgetImpl*  pImpl;

};

 

class Widget{

public:

         …

         void swap(Widget& other)

{        using std::swap;

         swap(pImpl, other.pImpl);

}

};

namespace std{

template<>

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

{ a.swap(b);}

}

 

对于class templates而非class,如下:

template<typename T> class WidgetImpl{ }

template<typename T> class Widget{ }

namespace std{

         template<typename T>

         void swap<Widget<T> >(Widget<T> & a, Widget<T>& b){a.swap(b);}

         //Error,std是特殊的命令空间,其管理规则比较特殊。客户可以全物化 std内的 template,但不可以添加新的template

}

 

解决方案

namespace Widget Stuff{

template<typename T>class Widget{ }

 

template<typename T>

void swap (Widget<T> & a, Widget<T>& b){a.swap(b);} //OK

}

 

posted on 2012-11-21 22:39  ArcherXu  阅读(180)  评论(0编辑  收藏  举报

导航