条款18:让接口更容易的被使用,不易误用

接口设计主要是给应用接口的人使用的,他们可能不是接口的设计者,这样作为接口的设计者就要对接口的定义更加易懂,让使用者不宜发生误用,例如对于一个时间类:

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

在应用Date类的时候,对于Date的三个参数我们很容易用错,因为它们的类型相同,我们可能会将实参20传给month等,我们在设定接口的时候要保证接口使用过程中不管怎么用都会不出错,例如我们可以定义三个类型:

struct month{
explicit month(int i):val(i){}
int val;
};
struce day{
explicit day(int i):val(i){}
int val;
};
struct year{
explicit year(int i):val(i){}
int val;
};

这样我们在使用的时候就不会出现将day参数和month参数用错的情况,例如:

Date d(month(12),day(19),year(2013));

但是此时我们可能经month的参数用错,例如设置为>12,或者将day的参数大于该月的天数,这样我们可以经一步设置:

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):val(m){}
int val;
};

这样我们就可以调用:

Date d(month::Jan(),day(18),year(2013));

我们用类的静态函数来替换对象来制定月份,相关的我们也可以设定day和year,这样我们就不会出现Month用错的情况了。
       我们设定接口的一个原则就是限定类型内什么事情可以做,什么事情不可以做,我们在设定一个类型的时候,我们要让它与内置的类型有相同的表现形式。
       当我们的接口返回的是指针的类型的时候,应用接口的客户可能会出现对指针所指向资源没有释放的情况,我们此时可以使用前面介绍过的资源管理类来对资源进行管理,常用的是std::tr1::shared_ptr智能指针,它可以自己制定其所指资源释放的时候所调用的函数,具体使用方式请参看相关资料。

请记住:

  • 好的接口要容易被正确使用而不是误用
  • "促进正确使用"的方法包括接口的一致性,与内置类型的行为兼容等
  • "阻止误用"的方法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户对资源的管理责任。
  • tr1::shared_ptr支持定制型删除器;

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

条款主要的是介绍在设计class的时候应该注意的问题,只要有以下几点:

  • 新的type的对象应该如何被创建和销毁?主要是指的新的type的构造函数和析构函数的定义。
  • 对象的初始化和对象的赋值有什么样的区别?主要是新type的构造函数和赋值函数的定义。
  • 新的type的对象如果被passed by value(以值传递),意味着什么? 记住拷贝构造函数用来定义一个type的passed by value该如何实现。
  • 什么是新的type的“合法值”?在class类中包含各种的成员变量,这些成员变量的取值范围是什么?
  • 你的新的type需要配合某个继承图系吗?如果你继承自某些既有的class,你就受到那些class的设计的束缚,特别是受到“它们的函数是virtual或non-virtual”的影响。
  • 你的新type需要什么类型的转化?主要是指的隐式转化和显示转化。
  • 什么样的操作符和函数对该新type来说是合理的?主要是指的该type需要定义的member函数。
  • 什么样的标准函数应该被驳回?那些正是你必须声明为private者。
  • 谁该取用新的type的成员?主要是新type的public/private/protected函数或者friend函数。
  • 什么是新的type的“未声明接口”?
  • 你的type有多么的一般化?如果他是一个type的家族,那么可以试着写下class template。
  • 是否真的需要一个type类型?如果定义dervie class 只是为base class添加新的技能,那么可以优先考虑non-member和friend函数。

请记住:

  • class设计即使type的设计,在定义一个新的type的时候,请确定你已经考虑过本条款所讨论的主题。
条款20:宁以pass-by-reference-to-const替换pass-by-value

       考虑下面的例子:
class People{
public:
Person();
virtual ~People();
...
private:
std::string name;
std::string address;
}; class Student:public People{
public:
Studenet();
~Student();
...
private:
std::string schoolName;
std::string shoolAddress;
};

考虑下面的调用:

bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);

在函数validateStudent函数调用的时候会发生什么事情?首先会调用Student的copy构造函数从plato copy构造对象s,在函数validateStudent函数执行结束后,调用对象s的析构函数,这样总共调用了一次copy 构造函数和一次析构函数,但是在Student类内部,有两个string成员对象,因此在Student拷贝构造函数的时候也会调用string类的两个拷贝构造函数,相应结束的时候也会调用string的析构函数,又因为Student类继承子People,因此Student类的构造会引起People类对象的构造,对应的People对象两个string对象的拷贝构造和对应的析构函数,这样一个函数的执行就涉及到了六次的拷贝构造函数(一次Student,一次People,四次string)和六次的析构函数,虽然调用时正确的,但是调用的效率是太低了,我们可以通过pass-by-reference-to-const来调用。

bool validateStudent(const Student& s);

通过引用的调用可以防止对应的构造函数与析构函数的调用,const关键字主要是用来说明实参的对象是不能改变的,在pass-by-value的调用方式中,实参的对象也是不能改变的,改变的是复制的临时对象!在函数返回的对象也是有相似的情况!
       在pass-by-value还可能造成一种对象切割的情况,例如:

class Window{
public:
...
std::string name() const;
virtual void display() const;
}; class WindowWithScrollBars:public Window{
public:
...
virtual void display() const;
....
}; void printNameAndDisplay(Window w){
std::cout << w.name()<<endl;
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

在上面的这种情况下,当wwsb传递给函数的参数w时,发生的是pass-by-value,此时调用的Window的copy构造函数,因此,copy 构造函数构造的对象是Window类型的,不管实参类型是Window类型还是Window的子类型,这种情况叫对象的切割,对应的如果我们将printNameAndDisply函数的参数修改为const Window& w,此时我们就可以在函数内部调用到子类的函数,这就是多态的应用!需要注意!
       在C++中并不是所有的对象都是要以pass-by-reference-to-const的形式,对于内置的类型和stl中的迭代器类型和函数对象还是要采用pass-by-value的形式!

请注意:

  • 尽量以pass-by-reference-to-const来替换pass-by-value,前者不仅高效还能防止对象的切割问题。
  • 对于内置类型和STL中的迭代器和函数对象还是要采用pass-by-value的形式比较高效。
  • 如果你有个对象属于内置类型(例如 int),pass by value 往往比pass by reference的效率高些。
条款21:必须返回对象时别妄想返回reference

       上一个条款讲解了以pass-by-reference来替代pass-by-value可以很好的增加效率,但是pass-by-reference也是不能乱用的。本条款主要讲在函数返回对象的时候不应该让他返回一个reference!例子:

class Ration{
public:
Ration(int numerator = 0,int denominator = 1);
...
private:
int n,b;
friend const Ration operator*(const Ration& lhs,const Ration& rhs);
}; const Ration operator*(const Ration& lhs, const Ration& rhs){
Ration result(lhs.n * rhs.n, lhs.b * rhs.b);
return result;
}

上面是个有理数的类,在操作符*中我们返回值一个pass-by-value的形式进行的,下面我们看下如果pass-by-reference会出现什么情况:

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

上面的例子中,在函数内result的对象是在函数的堆栈中申请的,如果函数结束那么对象的空间将会自动的适当,此时返回的引用是一个指向无用空间的引用,一旦对这个引用进行使用将会出现不确定的结果!还有一种情况是返回的引用的对象是在函数的堆中申请的,如下:

const Ration& operator*(const Ration& lhs,const Ration& rhs){
Ration result = new Ration(lhs.n * rhs.n, lhs.b * rhs.b);
return result;
}

在这种情况下如果不对new的空间进行内存的释放,返回的引用指向的空间都是有效的,但是容易产生的一个问题是内存泄露的情况,如果有计算:Ration result = (a*b)*c,其中a、b、c都是Ration类型的对象,此时就会产生内存泄露的问题!
       书中还介绍了一种情况就是返回一种static的对象,例如:

const Ration& operator*(const Ration& lhs,const Ration& rhs){
static Ration result(lhs.n * rhs.n, lhs.b * rhs.b);
return result;
}

此时在函数内部返回的是static的对象,这个对于多线程情况下容易发生错误,此外由于对象是函数local static,则这个对像在一个函数内部是只保留一份copy的,那么我们对这个函数的多次调用中指向的result对象都是相同的!例如:

if(a * b == b * c){

...

}

上面这个例子中,不管a/b/c是什么样子的数,返回的结果都是真,因为*返回的是一个静态的成员,而该成员在函数中只保留一份!

请记住:

  • 绝对不要返回一个ponit或者reference指向一个local stack对象、一个heap对象或者static对象,虽然pass-by-value会造成函数构造和析构的成本,但是至少能保证正确性!
条款22:将成员变量声明为private

对于C++而已,封装型是其三大特征之一,之所以封装型是如此的重要,是因为封装型能为接口的使用者提供透明的服务,内部的任何改变对其客户使用者来说都是透明的,都不需要做任何的改变,将class中的成员变量设为private的并且为成员变量设定对应的接口函数,这样外面的调用者只能通过接口函数来使用成员变量,对成员变量的任何改动只要接口不变都是可以对客户保持透明不变的!举个简单的例子:

class SpeedDataCollection{
...
public:
void addValue(int speed);
double averageSoFar() const;
....
};

这是一个计算速度平均值的类,我们在函数averageSoFar中计算到现在为止收集到的所有速度的平均值,对该接口使用时我们只需要定义一个对象然后调用该接口函数就可以了,我们不用关系数据是怎样收集的怎样计算的,对于不同的要求我们可以改变速度的计算方式或者存储方式,但是对接口使用者来说这些都是透明的看不到的!

假设我们有一个public成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的客户码都会被破坏,而那是一个不可知的大量。因此public成员变量完全没有封装性。假设我们有一个protected成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的derived classes都会被破坏,那往往也是个不可知的大量。因此,protected成员变量就像public成员变量一样缺乏封装性。从封装性的角度观察之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

请记住:

  • 对类的成员变量要采用private的形式。在封装型上public和protected都是不提供封装的,实质上两个没什么区别的!

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

在C++中封装性是其三大特性之一,本条款可以看作是最大限度的增加class的封装性,在C++中member函数包括friend函数是由对类的成员变量的访问权限,而相对应的non-member函数和non-friend函数都是没有权限的,这样在封装性上肯定是后者对其封装效果好,因此如果一个class的member函数可以用一个non-member函数完整的实现,则尽量的使用non-member函数和non-friend函数,当然这个函数可以是另一个类的成员函数或者friend函数,不过常见的用法是和该类作为同一个namespace的成员。
       例如:

namespace WebBrowsStuff{
class webBrowser{...};
void clearBrowser(webBrowser& wb);
...
}

在该namespace空间中,clearBrowser类作为一个non-member函数来对class webBrowser来进行处理!其中namespace是可以在不同的源码文件中存在的,这样如果我们对webBrowser处理的另外一个对不同客户使用的non-member函数我们可以放在其他的源码文件中,我们只要能保证namespace的名字相同就行,这样我们就能很方便的对处理函数进行扩展!

请记住:

  • 宁可拿non-member 和 non-friend函数来替换member函数,这样做可以增加封装性,包裹弹性和技能扩充性!

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

考虑一个有理数的类:

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

对于不同类型的数中我们希望能够进行隐式的类型转化,例如int型与double类型进行操作时int型会隐式的转化为double类型这里我们也希望能将int型和Ration类型进行类型转化,首先观察下operator*函数:

class Ration{
public:
...
const Ration operator* (const Ration& lhs,const Ration& rhs) const;
};

下面的操作:

Ration oneEight(1,8);
Ration oneHalf(1,2);
Ration result = oneEight * oneHalf;
result = result * oneEight;

在当前的这种情况下上面的操作能进行的很好,接下来看下面的操作:

result = oneEight * 2;
result = 2 * oneEight;

第一个能执行的很好,但是第二个却不能编译通过,对于内置类型中的交换律该类不能很好的支持,如果我们修改成下面的形式我们就能很好的看出来为什么第二个不能支持:

result = oneEight.operator*(2);
result = 2.operator*(oneEight);

明显的对于第二个,2不是一个Ration类,想对应的也就没有operator*函数,所以失败,在第一个中,在oneEight的operator*操作中需要的是一个Ration的对象,在这里2隐式的转化为了一个Ration对象,因为Raion构造函数有默认值并且不是explicit修饰的!为了能保证支持下面那种情况,我们要把第二种情况下的2也能转化为一个Ration类型,因为只有参数中才能进行隐式类型转化,因此我们也就是将第二种情况下的2也作为参数传入,此时我们将member函数转化为一个non-member函数,如下:

const Ration operator*(const Ration& lhs,const Ration& rhs){
...
}

因为此时不是做为一个class的member函数,因此第一个参数也就不会是一个this指针的Ration对象,而是作为一个operator*的第一个参数存在,所以上面的两种情况都能很好的做到支持!
       还有人可能想到需不需要把该函数作为该class的friend函数,记住如果non-member和non-friend函数能解决的就不要应用对应的member和friend函数,这个在上一条款中已经说过了!

请记住:

  • 如果你需要为一个函数的所有参数进行类型转化(包括this指针所指向的那个隐喻参数),那么这个函数为non-member函数!

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

Effective C++ ——设计与声明的更多相关文章

  1. Effective C++ —— 设计与声明(四)

    条款18 : 让接口容易被正确使用,不易被误用 欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误操作.  1. 明智而审慎地导入新类型对预防“接口被误用”有神奇疗 ...

  2. 《Effective C++》第4章 设计与声明(2)-读书笔记

    章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti ...

  3. 《Effective C++》第4章 设计与声明(1)-读书笔记

    章节回顾: <Effective C++>第1章 让自己习惯C++-读书笔记 <Effective C++>第2章 构造/析构/赋值运算(1)-读书笔记 <Effecti ...

  4. 《Effective C++》阅读总结(四): 设计、声明与实现

    第四章: 设计与声明 18. 让接口更容易被正确使用,不易被误用 将你的class的public接口设计的符合class所扮演的角色,必要时不仅对传参类型限制,还对传参的值域进一步限制. 19. 设计 ...

  5. EffectiveC++ 第4章 设计与声明

    我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的"可能比较准确"的「翻译」. Chapter4 设计与声明 Designs and Declarat ...

  6. Effective C++笔记:设计与声明

    条款18:让接口容易被正确使用,不易被误用 1,好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2,“促进正使用”的办法包括接口的一致性,以及与内置类型的行为兼容. ...

  7. Effective C++笔记04:设计与声明

    条款18:让接口easy被正确使用,不易被误用 1,好的接口非常easy被正确使用,不easy被误用.你应该在你的全部接口中努力达成这些性质. 2,"促进正使用"的办法包含接口的一 ...

  8. 《Effective C++》设计与声明:条款18-条款25

    条款18:让接口容易被正确使用,不容易被误用 注意使用const,explicit,shared_ptr等来限制接口. 必要时可以创建一些新的类型,限制类型操作,束缚对象等. 注意保持接口的一致性,且 ...

  9. Effective C++笔记(四):设计与声明

    参考:http://www.cnblogs.com/ronny/p/3747186.html 条款18:让接口容易被正确使用,不易被误用 1,好的接口很容易被正确使用,不容易被误用.你应该在你的所有接 ...

随机推荐

  1. Java操作属性文件与国际化

    在前面讲到的java类集中的Hashtable中,有一个子类Properties,此类定义如下: public class Properties extends Hashtable<Object ...

  2. Spring官方文档翻译(转)

    http://blog.csdn.net/tangtong1/article/details/51326887 http://blog.csdn.net/tangtong1/article/detai ...

  3. [AtCoder arc090E]Avoiding Collision

    Description 题库链接 给出一张 \(N\) 个节点, \(M\) 条边的无向图,给出起点 \(S\) 和终点 \(T\) .询问两个人分别从 \(S\) 和 \(T\) 出发,走最短路不相 ...

  4. [NOI2005]寿司晚宴

    题目描述 为了庆祝NOI的成功开幕,主办方为大家准备了一场寿司晚宴.小G和小W作为参加NOI的选手,也被邀请参加了寿司晚宴. 在晚宴上,主办方为大家提供了n−1种不同的寿司,编号1,2,3,⋯,n-1 ...

  5. [Codeforces]848C - Goodbye Souvenir

    题目大意:n个数字,m次操作,支持修改一个数字和查询一个区间内每种数字最大出现位置减最小出现位置的和.(n,m<=100,000) 做法:把每个数字表示成二维平面上的点,第一维是在数组中的位置, ...

  6. 2017ACM/ICPC广西邀请赛-重现赛 1004.Covering

    Problem Description Bob's school has a big playground, boys and girls always play games here after s ...

  7. [Codeforces Round#417 Div.2]

    来自FallDream的博客,未经允许,请勿转载,谢谢. 有毒的一场div2 找了个1300的小号,结果B题题目看错没交  D题题目剧毒 E题差了10秒钟没交上去. 233 ------- A.Sag ...

  8. 容器化分布式日志组件ExceptionLess的Angular前端UI

    写在前面 随着微服务架构的流行,日志也需要由专门的分布式日志组件来完成这个工作,我们项目使用的是 ExceptionLess 这个组件,它是前后端分离的:这篇文章我们就来实践容器化 Exception ...

  9. 数据结构与算法 —— 链表linked list(05)

    反转一个单链表. 进阶:链表可以迭代或递归地反转.你能否两个都实现一遍? 示例 : 给定这个链表:1->2->3->4->5 返回结果: 5->4->3->2 ...

  10. 分布式改造剧集之Redis缓存采坑记

    Redis缓存采坑记 ​ 前言 ​ 这个其实应该属于分布式改造剧集中的一集(第一集见前面博客:http://www.cnblogs.com/Kidezyq/p/8748961.html),本来按照顺序 ...