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

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

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

class Person{

public:

      Person();

      virtual~Person();

      …

private:

      std::stringname;

      std::stringaddress;

};

class Student:public Person{

public:

      Student();

      ~Student();

      …

private:

      std::stringschoolName;

      std::stringschoolAddress;

};

bool validateStudent(Student s);

Student plato;

bool platoIsOK = validateStudent(plato);

当上述函数被调用时,发生什么事?

无疑地Student的copy构造函数会被调用,以plato为蓝本将s初始化.同样明显地,当validateStu7dent返回s会被销毁.因此,对此函数而言,参数的传递成本是”一次Student
copy构造函数调用,加上一次Student析构函数调用”.

Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象.此外Student对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象.一个Person对象又有两个string对象在其中,因此每一次Person构造动作又需承担两个string构造动作.最终结果是,以by
value方式传递一个Student对象会导致调用一次Student copy构造函数,一次Person copy构造函数,4次string
copy构造函数.当函数内的那个Student复件被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作.因此,以by
value方式传递一个Student对象,总体成本是”六次构造函数和六次析构函数”.

bool validateStudent(const Student &s);

      上述传递方式的效率就高得多:没有任何构造函数或析构函数被调用,因为没有任何新的对象被创建.

以by reference方式传递参数也可以避免slicing(对象切割)问题.当一个derived class对象以by
value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而”造成此对象的行为像个derived
class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象.如下例:

class Window{

public:

      …

      std::stringname() const; //返回窗口名称

      virtualvoid display() cosnt;   //显示窗口和其内容

};

class WindowWithScrollBars:public Window{

public:

      …

      virtualvoid display() const;

};

void printNameAndDisplay(Window w){   //参数可能被切割

      std::cout<<w.name();

      w.display();

}

WindowWithScrollBars wwsb;

printNameAndDisplay(wwsb);

你会发现,在
printNameAndDisplay()函数内无论传递过来的对象原来是什么类型,参数w就像是个Window对象.即在printNameAndDisplay内调用display总是Window::display,而不会是WindowWithScrollBars::display.

解决切割问题方案:

void printNameAndDisplay(const Window &w){

      std::cout<<w.name();

      w.display();

}

现在,传进来的窗口是什么类型,w就表现出来那种类型.

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

     
一旦程序员领悟了pass-by-value的效率牵连层面,往往变成十字军战士,一心一意根除pass-by-value带来的种种邪恶.在坚定追求pass-by-reference的纯度中,他们一定会犯下一个致命错误:开始传递一些references指向其实并不存在的对象.考虑如下:

class Rational{

public:

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

      …

private:

      intn, d;

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

};

这个版本的operator *系以by value方式返回其计算结果(一个对象).

如果改为传递reference,任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称,以上述operator
*为例,如果它返回一个reference,后者一定指向某个既有的Rational对象,内含两个Rational对象的乘积.

我们当然不可能期望这样一个(内含乘积的)Rational对象在调用operator *之前就存在.也就是说,如果你有:

Rational a(1,2);       //a = 1/2

Rational b(3,5);       //b = 3/5

Rational c = a * b;        //c应该是3/10

期望”原本就存在一个其值为3/10的Rational对象”并不合理.如果operator
*要返回一个reference指向如此数值,它必须自己创建那个Rational对象.

函数创建新对象的途径有二:在stack空间或heap空间创建之.如果定义一个local变量,就是在stack空间创建对象.根据这个策略试写operator
*如下:

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

      Rationalresult(lhs.n*rhs.n, lhs.d*rhs.d);

      returnresult;

}

你可以拒绝这种做法,因为你的目标是要避免调用构造函数,而result却必须像任何对象一样地由构造函数构造起来.更严重的是:这个函数返回一个reference指向result,但result是个local
对象,而local对象在函数退出前被销毁了.因此,这个版本的operator
*并未返回reference指向某个Rational,它返回的reference指向一个”从前的”Rational;一个旧时的Rational,因为它已经被销毁了.任何函数如果返回一个reference指向某个local对象,都将一败涂地.(如果函数返回指针指向一个local对象,也是一样)

考虑在heap内构造一个对象,并返回reference指向它:

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

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

      return*result;

}

上述还是必须付出一个”构造函数调用”代价,因为分配所得的内存将以一个适当的构造函数完成初始化动作.此外又有另一个问题:谁该对着被你new出来的对象实施delete?

如:

Rational w,x,y,z;

w = x * y * z;

这里同一个语句调用了两次operator*,因而两次使用new,也就需要两次delete.但却没有合理的办法让operator
*使用者进行那些delete调用,因为没有合理办法让他们取得operator*返回的reference背后隐藏的那个指针.这绝对导致内存泄漏.

一个”必须返回对象”的函数的正确写法是:就让那个函数返回一个新对象呗.对Rational的operator *而言意味以下写法:

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

      returnRational(lhs.n*rhs.n, lhs.d*rhs.d);

}

当然,你需得承受operator *返回值的构造成本和析构成本,然而长远来看那只是为了获得正确行为而付出的一个小小代价.

提示:绝不要返回pointer或reference指向一个local
stack对象,或返回一个reference指向一个heap-allocated对象,或返回pointer或reference指向一个local
static对象.

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

如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数.这样就能对成员变量的处理有更精确的控制.如:

class AccessLevels{

public:

      intgetReadOnly() const { return readOnly; }

      voidsetReadWrite(int value) { readWrite = value; }

      intgetReadWrite() const { return readWrite; }

      voidsetWriteOnly(int value) { writeOnly = value; }

private:

      intnoAccess;          //对此int无任何访问动作

      intreadOnly;           //只读访问

      intreadWrite;          //读写访问

      intwriteOnly;          //只写访问

};

提示:如果你通过函数访问成员变量,日后可以以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化.

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

class WebBrowser{

public:

      voidclearCache();

      voidclearHistory();

      voidremoveCookies();

};

也许你想整合上述所有动作,因此WebBrowser也提供这样一个函数:

class WebBrowser{

      …

      voidclearEverything() {

            clearCache();

           clearHistory();

           removeCookies();

}

};

当然,这一机能也可由一个non-member函数调用适当的member函数而提供出来:

void clearBrowser(WebBrowser & wb){

      wb.clearCache();

      wb.clearHistory();

      wb.removeCookies();

}

那么,哪一个比较好呢?

面向对象守则要求,数据以及操作数据的那些函数应该被捆绑在一块,这意味它建议member函数是较好的选择.不幸的是这个建议不正确.这是基于对面向对象真实意义的一个误解.面向对象守则要求数据应该尽可能被封装,然而与直观相反地,member函数clearEverything带来的封装性比non-member函数clearBrowser低.此外,提供non-member函数可允许对WebBrowser相关机能有效大的包裹弹性,而那最终导致较低编译相依度,增加WebBrowser的可延伸性.

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

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

所谓swap两对象值,意思是将两对象的值彼此赋予对方.缺少情况下swap动作可由标准程序库提供的swap算法完成,其典型实现如你所预期:

namespace std{

      template<typenameT> void swap(T & a, T & b){

  T temp(a);

  a = b;

  b = temp;

}

}

只要类型T支持copying,缺省的swap实现代码会帮你置换类型为T的对象,你不需要为此另外再做任何工作.缺省的swap版本涉及三个对象的复制:a复制到temp,b
复制到a,以及temp复制到b.

class WidgetImpl{

public:

      …

private:

      inta, b, c;

      std::vector<double>v;  //可能有许多数据,意味复制时间很长

};

class Widget{

public:

      Widget(constWidget & rhs);

      Widget& operator=(const Widget & rhs){

  …

  *pImpl = *(rhs.pImpl);

  …

}



private:

      WidgetImpl* pImpl;

};

一旦要置换两个Widget对象值,我们唯一需要做的是置换其 pImpl指针,但缺省swap算法不知道这一点.

我们希望告诉std::swap,当Widgets被置换时真正做的是置换其内部的pImpl指针.确切实践这个思路的一个做法是:将std::swap针对Widget特化.

class Widget{

public:

      …

      voidswap(Widget & other){

           usingstd::swap;

           swap(pImpl,  other.pImpl); //只置换其pImpl指针

      }

      …

};

namespace std{

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

            a.swap(b);

}

}

Widget和WidgetImpl都是class templates而非classes的情况呢,又如何?

namespace WidgetStuff{

      …

      template<typenameT> class Widget{…};

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

    a.swap(b);

      }

}

技术
©2019-2020 Toolsou All rights reserved,
Vue常用特性(一)百度、阿里、腾讯内部岗位级别和薪资结构,附带求职建议!【虚拟机踩坑记】此主机支持 Intel VT-x,但 Intel VT-x 处于禁用状态。苹果与日产对话暂停,Apple Car进展如何?一文揭秘阿里、腾讯、百度的薪资职级Java基础(冒泡排序)TP6验证器的使用示例及正确验证数据一年半JAVA工作经验的总结谷歌称居家办公影响工作效率!2021 年将回归线下办公这些歌,程序员千万万万万别听!