PS,1880后程序员

看不完的牙,写不完的程序,跑不完的步。
随笔 - 97, 文章 - 34, 评论 - 10, 引用 - 0
数据加载中……

Effecitive C++读书笔记 Chapter4 设计与声明

 

Item18 Make interfaces easy to use correctly and hard to use incorrectly.让接口容易被正确使用,不易被误用

好的接口设计应该是既容易被正确使用,又不易被误用。

就例如书中的Sample,关于时间的,我们一般的做法就是在创建Day对象时,追加校验函数来判断年月日是不是有效。

建议的做法一是:创建新的类型

定义:

class Month {

public:

   static Month Jan() {return Month(1); }

   static Month feb() {return Month(2); }

private Month(int m);

};

Date d(Month::Mar(), Day(30), Year(1995) );

为啥不直接使用静态变量?

参考Item4 P30,简单说就是不能保证在使用non-local static objects时,这个对象就已经初始化了。如果non-local static objects在另一个文件了,又恰巧没有初始化,系统当然就会翘辫子了。

另一种方法:加上const来限制type可以做的事情。

先参考Item3 P19

class Rational { };

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

之所以强制设置为const就是为了避免client在使用时出错。因为如果没有clien,那么:

Rational a,b,c;

(a*b)=c

这种写法是对的,但是如果ab是内置类型,这种写法就是错误的。

除非有必要,否则就要保证你的类型type的行为和内置类型一致。

一致性导致接口容易被正确使用。

STL是榜样,java在这里成了反面教材,因为如果想知道容器内对象的数量,用Array,要访问属性lengthString要用length函数,ArrayList要用size函数,这就是不一致性。

使用std::tr1::shared_ptr,消除client对资源管理的责任

找出以下写法的两个易错的地方:

Investment* createInvestment();

1 忘记删除createInvestment()返回的指针

2 删除这个指针多次

so,修改定义:

std::tr1::shared_ptr< Investment > createInvestment();

如果出现这种情形:从createInvestment得到Investment*的函数要把这个指针传递个给叫做getRidOfInvestment,由getridOfInvestment取代使用delete

这里就出现了一个新的client易错的点,用户或许会使用错的资源释放机制。因为deletegetRidOfInvestment取代了。

std::tr1::shared_ptr< Investment >

pInv(static_cast<Investment*>(0), getRidOfInvestment);

那么定义就应该是这样的:

std::tr1::shared_ptr< Investment > createInvestment()

{

   std::tr1::shared_ptr< Investment >

retVal(static_cast<Investment*>(0), getRidOfInvestment); //这不能让client来做

retVal = ;

return retVal;

}

tr1::shared_ptr的优点是允许在一个DLL创建对象,在另一个DLL里删除对象。

牢记

  • Good interfaces are easy to use correctly and hard to use in correctly.
  • 接口一致性,于内置数据类型的行为兼容
  • 阻止错误的方式还包括创建新的类型(例如Month),限制类型上的操作,束缚对象值,以及消除客户的资源管理的责任
  • tr1::shared_ptr支持定制类型的删除器deleter,允许在一个DLL创建对象,在另一个DLL里删除对象。

Item19Treat class design as type design.设计class犹如设计type

在设计一个类的时候,要回答一系列的问题哦。

参考大师在P85-P86之间给出的常常的清单吧,其实实际上,我在设计类的时候的确没有想过这么多,问过自己这么的为什么,所以这也是我总是在追求代码重用,却总是发现自己写的代码重用度很低的一个原因把。

Item20Prefer pass-by-reference-to-const to pass-by-value.宁以pass-by-reference-to-const替换pass-by-value

But先学习一个单词,characteristicKAO,这竟然是个名词。

再学一个地道的说法:解决问题的方法:The way around the slicing problem is…

函数都是值传递。pass by-valuefunction parameters are initialized with copies of the actual arguments, and function callers goes back a copy of the value returned by the function.这样当然开销就大了,每次都先copy一份进来,完事以后,再copy一份出去。

假设函数的参数是一个Student对象,bool validateStudent(Student s);调用这个函数,额外的隐性开销包括要先调用copy constructor创建一个Student对象用于函数内部,函数执行结束再调用析构函数释放这个对象。

开销太大了,改进一下:pass by reference-to-const

bool validateStudent(const Student& s);

引用是通过指针来实现实现的,因此传递引用实际上就是在传递指针。references are typically implemented as pointers.

但是这个规则对于内置数据类型不适用,也不是适用STL iterator和函数对象function objects

即使再小的对象也应该不要使用值传递,而是要使用pass by reference-to-const

关于slicing problem的另一种描述

slicing problem是在多态规则里面容易产生的。

看一个简单的基类、派生类的定义

class Window

{

public:

int height;

int width;

};

class TextWindow : public Window

{

public:

int cursorLocation;

};

Window win;

TextWindow *tWinPtr;

tWinPtr = new TextWindow;

win = *tWinprt;

win是一个Window对象,C++规定:给win分配的内存看见的大小,由其静态类型决定。就是说默认的拷贝函数导致信息会出现丢失。这就是slicing problem

试想一下这要是通过值传递的方式传递参数,实参一copy就已经丢失信息了。

牢记

  • Prefer pass-by-reference-to-const over pass-by-value.这样既有效,又可以避免slicing problem
  • 但是这个规则对于内置数据类型,STL iterator和函数对象function objects不适用。对于它们传递值就好了。

Item 21Don't try to return a reference when you must return an object.必须返回对象时,别妄想返回其reference

heap and stack

堆和栈这是2个不同的概念,哎哟,我一直以为是一个词。

heap:堆

  • 栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;
  • 堆是函数库内部数据结构,不一定唯一。
  • 堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。
  • 使用new创建的对象是在heap上分配内存空间。

stack:栈

  • 而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。
  • 栈是系统数据结构,对于进程/线程是唯一的;不同堆分配的内存无法互相操作。
  • 栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!
  • 定义的局部变量是在stack上分配内存空间的。

牢记

  • 简单一句话就是必须要返回对象。

Item22Declare data members private.将成员变量声明为private

why data members shouldn’t be public?

argument

(against) 争论,意见

实参

形参是parameters

protected data member is mot more encapsulated than public one.

牢记

  • data member一定要封装。
  • protected不必public有更好的封装。

Item 23Prefer non-member non-friend functions to member functions.宁以non-membernon-friend替换member函数

这是一个典型的例子:

class WebBrowser {

public:

void clearCache();

void clearHistory();

void removeCookies();

};

为了提供一个执行所有操作的函数,所以就在WebBrowser里面追加定义:

void clearEverything();

哎,我一直就是这么写的,并自以为有很好的封装,But

void clearBrowser(WebBrowser wb)

{

wb.clearCache();

wb.clearHistory();

wb.removeCookies();

}

第一:前者并不比后者有很好的封装

这就要解释一下什么叫做“封装”?以及封装的判别标准。

封装的判别标准:可以通过统计能够访问这个data的函数的数目来计算,函数越多,这个data封装也就月不好,因此前一种写法的封装就没有后者好。这也可以用来解释Item22里面,为什么要求数据成员不能定义为public。另外增加clearEverything()作为member function,实际上是降低了封装性。而后面的non-member non-friend functions的定义就没有改变WebBrowser的封装性。

第二:后者还能提供更加灵活的打包package,增加扩展性。

put all convenience functions in multiple header files, but one namespace.

第三:增加函数的可扩展性。

你可以定义自己的convenience functions,写到一个header file里面,放到同一个namespace里面。这是member function做不到的。

牢记

  • 优先使用non-member non-friend函数来替换member函数。

Item 24Declare non-member functions when type conversions should apply to all parameters.若所有参数皆需类型转换,请为此采用non-member函数

原因:

Parameters are eligible for implicit type conversion only if they are listed in the parameter list.

结论:

make operator* a non-member function, thus allowing compilers to perform implicit type conversions on all arguments.

class Rational {

};

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

{

   return Rationan(lhs.numerator()*rhs.numerator(),

lhs.denominator()*rhs. denominator () );

}

一个误区:

如果一个函数,和某个类相关,而又不能定义成member,那么这个函数就一定要定义成friend

上面这个例子就说明这个说法并不正确。真爱生命,慎用friend functions

Item 25Consider support for a non-throwing swap.考虑写出一个不抛异常的swap函数

  1. default swap

就是指std里面定义的swap

  1. member swap
  2. nonmember swap
  3. specializations of std::swap

member swap

Widget:我们希望的是交换指针,但swap实际做的是不仅copy3Widget对象,而且还copy3WidgetImpl对象。太浪费了!都低碳时代了。

class Widget{

void swap(Widget& other)

{

using std::swap;

swap(pImpl, other.pImpl;);

}

};

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

{

   a.wap(b);

}

nonmember swap

接下来要讨论的是如果WidgetWidgetImpl不是类而是类模板会怎么样?

约束条件:不能在std里面增加新的template,只能特化(specializestd内的template

如果非要定义,say sorrybehavior is undefinedKAO,其实这比异常还讨厌。

解决方法是把它定义到一个自己的namespace里面,而不要定义到std里面。

namespace WidgetStuff {

template<typename T>

class Widget{…};

template<typename T>

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

{

a.swap(b);

}

}

specializations of std::swap

如果仅仅是针对一个class,那就特化std::swap

If you want to have your class-specializing version of swap called in as many contexts as possible, you need to write both a non-member version in the same namespace as your class and a specialization of std::swap.

这部分十分绕,P111还对于C++name lookup的规则进行了详细的描述。值得重新温习。

posted on 2010-03-22 11:28 amenglai 阅读(359) 评论(0)  编辑  收藏 所属分类: Effecitive C++读书笔记


只有注册用户登录后才能发表评论。


网站导航: