`
evasiu
  • 浏览: 165478 次
  • 性别: Icon_minigender_2
  • 来自: 广州
博客专栏
Fa47b089-e026-399c-b770-017349f619d5
TCP/IP详解卷一>阅读...
浏览量:12264
社区版块
存档分类
最新评论

effective c++ -- 设计与声明

 
阅读更多

本章对良好C++接口的设计与声明提出了一些建议,提供了错误接口可能带来的后患的一些例子。总的来说,良好的设计就是“让接口容易被使用,不容易被误用”。

 

Item 18: 让接口容易被使用,不容易被误用
要做到这一点,首先必须考虑客户(即使用该接口的人)可能做出什么样的错误。例如一个日期class的构造函数:

class Date{
  public:
    Date( int month, int day, int year );
    ...
};

 用三个int来表达日期的年月日,看起来似乎合情合理,但是,习惯用日/月/年来表达一个日期的用户,很可能就会犯这样的错误,在日期的构造函数中使用错误的次序传递参数,又或者传递一个无效的月份或天数。解决些类问题比较好的方案是引入新类型(wrapper types)来区分年、月、日:

struct Day{
  explicit Day( int d ):val(d){}
  int val;
};

struct Month{
  explicit Month( int m ):val(m){}
  int val;
};

struct Year{
  explicit Year( int y ):val(y){}
  int val;
};

class Date{
  public:
    Date( const Month& m, const Day& d, const Year& y );
    ...
};

 可以通过这种方法来限制月份的取值:

class Month{
public:
  static Month Jan() { return Month(1) };
  static Month Feb() { return Month(2) };
  ...
  static Month Dec() { return Month(12) };
private:
  explicit Month( int m ); //阻止生成新的月份
  ...
};

 为什么不使用enum来枚举月份呢?书说提到,因为enum不具备我们希望拥有的类型安全性,例如enum可被拿来当一个int使用。
预防用户错误的另一个办法是,限制类型内什么事可以做,什么事不能做。常见的限制是加上const,例如我们前面提到的对operator*操作符的返回结果加上const。
还有另外一个准则,就是“尽量让自定义的type的行为与内置type一致”,例如上面提到的operator*操作符。
任何接口如果要求用户必须记得做某些事情,就是有着“不正确使用”的倾向。例如前面的createInvestment,它返回一个Investment指针,为了良好的资源管理,用户必须记得马上将返回来的指针放入shared_ptr中,因此,最好就是让createInvestment直接返回一个包含有Investment对象的shared_ptr指针。
shared_ptr支持定制型删除器,它会自动使用它的“每个指针专属的删除器”,因而消除“跨DLL之new/delete成对运用”可能带来的问题。

 

Item 19: 设计class犹如设计type
设计一个良好、高效的class应该考虑到的问题:
(1)新type的对象应该如何被创建和销毁?即其class的构造函数和析构函数以及内存分配函数和释放函数(operator new/delete, operator new[]/delete[])的设计。
(2)对象的初始化和对象的赋值该有什么样的差别?这决定了构造函数和赋值操作符的行为,以及其间的差异。
(3)新type的对象如果被passed by value,意味着什么?也就是copy构造函数。
(4)什么是新type的合法值?这决定了其成员函数必须进行的错误检查工作,也影响函数抛出的异常,以及函数异常明细列。
(5)新的type需要配合某个继承图系吗?主要是函数的virtual和non-virtual设定。如果允许其他class继承该class,则某些函数,尤其是析构函数,应该设为virtual。
(6)新的type需要什么样的转换?如果你希望允许类型T1之物被隐式转换为类型T2之物,就必须在class T1内写一个类型转换函数,或在class T2内写一个non-explicit-one-argument的构造函数。如果只允许explicit构造函数存在,就得写出专门负责执行转换的函数。
(7)什么样的操作符和函数对此新type而言是合理的。也就是,该type是要做什么呢?
(8)什么样的标准函数是不允许的?那些正是你必须声明为private者。
(9)谁该使用新type的成员?这将帮助你决定哪个成员为private,哪个为protected,哪个为public,以及哪些class和/或function应该是friends,以及将它们嵌套于另一个之内是否合理。
(10)什么是新type的未声明接口?它对效率、异常安全性以及资源运用提供何种保证?
(11)你的新type有多么一般化?即你只是需要一个class,还是一个class template?
(12)你真的需要一个新type吗?如果只是定义新的derived class以便为既有的class添加机能,那么说不定一或多个non-member函数或template更能够达到目标。

 

Item 20: 宁以passed-by-reference-to-const替换pass-by-value
首先,pass-by-value方式传递到函数,函数使用的其实是实参的副本。对于用户自定义类而言,这个过程使用了类的构造函数,其缺陷主要有:
(1)调用类的构造函数可能是一个昂贵的操作;
(2)在继承体系下,如果函数的形参声明的是一个基类,而传入该函数的形参是继承类,那么实参将被切割而变成一个基类,从而失去我们期望的多态性。
不过,对于内置类型及STL的迭代器和函数对象,pass-by-value往往比较高效。

Item 21: 必须返回对象时,别妄想返回其reference
所谓reference只是个名称,代表某个既有的对象。任何时候看到一个reference的声明式,我们应该立刻想到,它的另一个名称是什么?
函数创建对象的途径有二:在stack空间或在heap空间创建之。如果定义一个local变量,就是在stack空间创建。例如:

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

 result是operator*的local对象,当operator执行结束返回后,result将不复存在,这便是函数返回reference的第一种结果。
那如果operator*在heap上创建对象呢?result总不会随着operator*的执行结束而不存在了吧?我们来看:

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呢?
除了on-stack跟on-heap,另一个存放对象的地方便是数据段,这里的数据要等程序运行完了才会被释放?让operator*返回的reference指向一个被定义于函数内部的static对象?我想它根本就不是我们想要的。

 

Item 22: 将成员变量声明了private
1. 将成员变量声明为private可赋予客户访问数据的一致性,可细微划分访问控制(可读,可写),允诺约束条件获得保证,并提供class作者充分的实现弹性。
2. protected并不比public更具封装性。

 

Item 23: 宁以non-member、non-friend函数替换member函数
以non-member、non-friend函数替换member函数,这一节讨论的主要原因是添加封装性。越多的东西被封装,即越少的代码可以看到数据,我们就越能自由地改变对象数据。我们通过能够访问该数据的函数数量来估量有多少代码可以看到某一块数据。如果成员变量是public,那么就有无限量的函数可以访问他们,而能够访问private成员变量的函数只有class的member函数加上friend函数而已。这也就是,宁以non-member、non-friend函数替换member函数的起因。当然,前提是存在这样的non-member non-friend函数。
这一节还讨论了namespace的对代码组织起的作用。namespace是跨越多个源码的。将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多non-member、non-friend函数到些命名空间。

 

Item 24: 若所有参数皆需要类型转换,请为此采用non-member函数
这里的non-member函数对应的另一个函数是member函数,member函数暗含着一个*this参数,所谓的“若所有参数比需要类型转换,必须采用non-member函数”的原因就在于,member函数暗含的*this参数是转换不了的!例如我们前面提到的operator*应用于Rational,假如operator*是Rational的一个member函数:

class Rational{
  public:
    Rational( int n = 0, int d = 1 ):num(n), den(d){
    }
    int numerator() const;
    int denumerator() const;
    const Rational operator* ( const Rational& lhs, const Rational& rhs ) const;
  private:
    int num;
    int den;
};
Rational oneEighth( 1, 8 );
Rational oneHalf( 1, 2 );
Rational result = oneHalf * oneEighth;  //调用oneHalf.operator*( oneEighth );
Rational result2 = oneEighth * oneHalf;  //调用oneEighth.operator*( oneHalf );
result = oneHalf * 2;    //调用oneHalf.operator*( 2 ); 2隐式转换。
result2 = 2 * oneHalf;    //错误,没有函数可调用

 从Rational类设计者的角度来看,拿一个Rational对象跟一个整数相乘应该是被允许的,这比较符合我们对它的期望,前面我们也提到,尽量让类的设计与内置类型的行为相一致。若函数设计成non-member函数,将不再受*this参数限制,所以参数都可能被隐式转换:

const Rational operator* ( const Rational& lhs, const Rational& rhs );
result = 2 * oneHalf;    //2首先被隐式转换为Rational,成功。

 至于该operator*是否应该被设为Rational的friend函数,在这里显然是不需要的。别忘了,让越少的代码接触到数据,封装性越高,我们对private部分能做的改动的弹性便越高。

 

Item 25: 考虑写出一个不抛异常的swap函数
我们在处理自我赋值那里看到了swap函数的作用。swap原本只是STL的一部分,而后成为异常安全性编程的脊柱。缺省情况下swap动作可由标准程序库提供的swap算法完成,其典型实现如下:

namespace std{
  template<typename T>
    void swap( T& a, T& b ){
      T temp(a);
      a = b;
      b = temp;
    }
}

 swap复制a到temp, b到a, 然后是temp到b。问题在于,对于某种特殊类型的类,“以指针指向一个对象,内含真正数据”类型,即用pimpl手法,其典型实现可能如下:

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;
};

 一旦要转换两个Widget对象值,我们唯一需要做的就是置换其pImpl指针,但缺省的swap调用了operator=,它不只复制了三个Widget,还复制了三个WidgetImpl!
当默认的版本并不符合我们的期望时,试着做以下事情:
(1)提供一个public swap成员函数,让它高效地置换你的类型的两个对象值:

class Widget{
  public:
    void swap( Widget& other ){
      using std::swap;
      swap( pImpl, other.pImpl );
    }
    ...
  private:
      ...
};

 (2)在你的class或template命名空间内提供一个non-member swap函数,并令它调用上述swap成员函数。

namespace WidgetStuff{
  ...
    template<typename T>
    class Widget{ ... };
  ...
    template<typename T>
    void swap( Widget<T>& a, Widget<T>& b ){
      a.swap(b);
    }
}

 (3)如果编写的是一个class(而非class template),为该class特化std::swap,并令它调用swap成员函数。
 最后,调用class的时候,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace,赤裸裸地调用swap。

template<typename T>
 void doSomething( T& object1, T& object2 ){
 	using std:swap;
	...
	swap( object1, object2 );
	} 


 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics