Effective C++ 第四章——设计与声明

介绍了条款18~25

Posted on May 8, 2017 in Effective_Cpp, ReadBook


条款18:让接口容易被正确使用,不易被误用
条款19:设计class犹如设计type
条款20:宁以pass-by-reference-to-const替换pass-by-value
条款21:必须返回对象时,别妄想返回其reference
条款22: 将成员变量声明为private
条款23:宁以non-member、non-friend替换member
条款24:若所有参数皆需类型转换,请为此采用non-member函数
条款25:考虑写出一个不抛异常的swap函数


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

C++中接口值的是:

function接口

class接口

template接口

每一种接口的参数传递必须被准确限制,不易被误用 
例如 
Date(int month, int day, int year); 
可以改写为: 
Date(const Month& month, const Day& day, const Year& year); 
其中class或者structMonth等ADT其构造函数必须是explicit,阻止其进行隐式转换。

再例如Month,其只能从1到12,有两种方案可以进行限制P80

P80 other准则:

预防客户错误的另一个办法是,限制类型内什么事可做,什么事不可做。常见的限制是加上const。

“让types容易被正确使用,不容易被误用”的表现形式:“除非有好理由,否则应该尽量令你的type为行为与内置types一致”

P81 若function接口返回指针,怕用户忘记删除的话可以返回智能指针。有时候还需要指定删除器。注意 将原始指针传给智能指针进行构造 比 先将智能指针初始化为0,再赋值 要好。

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

需要留意以下的问题:

1新type的对象应该如何被创建与销毁

2对象的初始化和对象的赋值该有什么样的差别?关键点是构造函数赋值操作符的实现,初始化赋值的区别。

3新type的对象如果被pass-by-value,意味着什么?

4什么是新type的“合法值”,若有,需要在构造函数、赋值操作符和所谓的setter函数中注意使用。

5你的新type需要配合某个继承图系吗?需要注意基类的virtual或non-virtual,以及若自己是多态基类,需要令析构函数virtual

6你的新type需要什么样的转换?P71P102的Rational以及条款15,都有隐式显示转换的示例。

7什么样的操作符和函数对此新type而言是合理的?可见条款23/24/46。

8什么样的标准函数应该被驳回?那就声明为private,见条款6

9谁该取用新type成员?关乎成员的publicprivateprotected以及friend函数的设置。

10什么是新type的“未声明接口”

11新type的一般化,是否需要从classclass template的转变。

12是否真的需要一个type,有什么其实仅仅是需要一个扩展功能的函数而已条款23 P100~101

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

C++默认的情况下是pass-by-value 
而其缺点如下:

1、P87 开销大

2、P87 对象被切割问题

什么时候选用pass-by-value比较好:

1、内置类型

2、STL的迭代器与函数对象

自定义的type无论如何都不适宜pass-by-value的原因:

1、对象小并不意味其copy构造函数不昂贵,涉及深拷贝的时候

2、某些编译器对待“内置类型”和“自定义类型”的态度截然不同,纵使两者的底层表述一致。如double和只有一个double的class是有区别的。

3、作为一个用户自定义的类型,其大小容易变化

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

这条条款先与条款10以及P30条款4相区别。 
若想返回reference,必先存在该对象,才能返回reference,而该对象也应该在函数中声明出来,那么这样有好处吗?

1、在stack上声明该对象出来,会调用构造函数(存在开销),而且当退出函数的时候该对象被delete。

2、在heap上声明该对象,会调用构造函数(存在开销),而且谁在函数外对这个变量delete?有什么还会出现内存泄漏。

3、在声明为local static,存在多线程安全问题,而且多次调用都是对同一个内存上的值作修改。但是P30的单例模式是一个例外。

综上所述,

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

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

这个条款的好处有:

1、public的接口全是函数,调用的时候都得在后面加(),保持一致性。

2、使用函数才能对变量修改,可以让你对成员变量的处理有更加精确的控制。可以定义其是否只读、只写or可读可写。

3、保护类的封装性。方便修改背后的实现方式。

P97 可变性与封装性成正比,public与protected的封装性都是很差的,或者说根本不提供封装P98

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

需要明白:

越多member/friend函数可以访问它,数据的封装性就越低。

因此non-member、non-friend更能保护类的封装性。 
上面的论述有两点注意:

1、这个论述只适应于non-member、non-friend函数,friend与member函数一样是对类的封装性也有同样冲击力。下一条款还会在member与non-member中作出选择

2、因在意封装性而让某函数“成为class的non-member函数”并不意味着“不能使其成为别的class的member函数”

在C++,比较自然的做法是让一个扩展类功能的便利函数成为non-member、non-friend函数并且位于类所在的同一个namespace内。而且参照P101,多个便利函数还可以分离编译相依关系,扩大类的机能扩充性

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

虽然构造函数支持隐式类型转换是不好的注意,但时候是个例外,最常见的是建立数据类型的时候,往往需要隐式转换。 
例如存在一个有理数类:

class Rational{public:    Rational(int numerator = 0, int denominator = 1);    int numerator() const;    int denominator() const;.........};

当需要两有理数相乘时候,可以实现为member函数

class Rational{public:    const Rational operator* (const Rational& rhs) const;};

但是这个函数不能支持下面这个形式

Rational result = 2 * oneself

在这情况下,可以使用non-member函数:

class Rational{......};const Rational operator* (const Rational& lhs, const Rational& rhs){    ......}

综上所述,

如果需要为某个函数的所有参数进行类型转换,那么这个函数必须是个non-member。

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