概述
- 本章主要介绍良好的C++接口的设计和声明。
- 让接口容易被正确使用,不容易被误用。
条款18:让接口容易被正确使用,不易被误用
假如我们设计了以下代码:
class Date
{
public:
Date(int month, int day, int year);
...
};
初看此接口也通情达理,年月日都有了。但是客户端经常会出现错误:
Date(30,3,1997); // 月份和日期反了
遗憾的是,即使这样调用编译还是不会报错。(就个人见到的来说,会在构造函数中做判断处理,诸如限定年月日的数值大小,但是作者在这本书中介绍的是编写良好的接口,使得客户端能够正确使用这个接口。)
我们可以考虑将参数封装成不同的类型:
struct Day
{
explicit Day(int day)
:val(day)
{
}
int val;
}
就像这样,month和year也可以封装成此结构体。与此同时,我们可以将Data构造函数声明成这样:
Date(const Month& month, const Day& day, const Year& year);
这样的话三个参数都是不同的类型,客户端就不会错误的调用而不报编译错误。
诸如此类。
作者总结
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制删除器。这可防范DLL问题,可用来自动解除互斥锁等等。
条款19:设计class犹如设计type
对于设计一个class,作者提出了一下设计规范:
- 新类型的对象应该如何被创建和销毁。 这是关于new和delete时候需要分配和释放多少,哪些内存来考虑的。
- 对象的初始化和对象的赋值有什么样的差别? 不会混淆“初始化”和“赋值”操作,以及他们的效率。
- 新type的对象如果被pass by value意味着什么? 这就是copy构造函数的编写。
- 什么事新type的“合法值”? 对于类中成员还要进行必要的约束,不是每个值都是合法的。要进行错误检查,异常检测等。
- 新type需要配合某个继承图系吗? 要注意virtual和non-virtual,尤其是析构函数。
- 新type需要什么样的转换? 判断隐式转换是否必要,显式转换又是否必要?explicit和non-explicit的使用。
- 什么样的操作符和函数对此新type而言是合理的? 将合适的访问声明为member函数。
- 谁该取用新type的成员? 考虑好哪些变量作为public,哪些作为private,哪些作为protected.
- 什么是新type的“未声明接口”?
- 你的新type有多么一般化? 可能你的type是一整个type家族,那就定义一个class template吧。
- 是否真的需要一个新type? 很可能你只需要一个non-menber函数或template就可以达到新目标。
作者总结
class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款所覆盖的所有讨论主题。
条款20:宁以pass-by-reference-to-const替换pass-by-value
Frist Of All,我们要明确一点:
pass-by-value操作需要对象调用copy构造函数,这个操作也许(很可能)非常的费时。
还是老样子,用一段代码来分析:
class Person
{
public:
Person();
virtual ~Person();
...
private:
string name;
string address;
}
//继承上类
class Student : public Person
{
public:
Student();
~Student();
private:
string schoolName;
string schoolAddress;
}
现在有一个接口:
bool validateStudent(Student s);
接下来我们调用这个接口:
Student plato;
bool isPlatoOK = validateStudent(plato);
好了,背景已经阐述完毕。上述我们已经构建了一段代码,并且提供了一个接口,这个接口参数采用的是pass-by-value方法。那这个接口调用的时候会发生哪些函数调用呢?
(1) Student的copy构造函数会被调用。返回时销毁s,会调用Student的析构函数。(所以参数的传递成本是一次析构函数和一次copy构造函数的调用)。
(2) Student里面创建了两个string,调用了两个string的默认构造函数。随后销毁的时候,会调用两次析构函数。
(3) 构造Student之前会构造基类Person,因此还需要调用基类构造函数。 基类中还有两个string类型成员变量,就再调用了2次string的默认构造函数。销毁时也会调用两次析构函数。
综合以上三点,如果我们用pass-by-value,就会发生六次构造函数,六次析构函数!
而我们以pass-by-reference传递的时候,没有任何的构造函数或析构函数被调用,因为没有任何的新对象被创建。
上面说的是效率上的问题,但不仅仅如此,使用pass-by-reference还可以解决对象切割(slicing) 问题。
对象切割
书里翻译的偏向晦涩一些,我用自己的理解+代码来帮助理解一下:
当一个derive class对象以pass-by-value方式传递给一个base class对象时,base class对象的拷贝构造函数会被调用,derive class对象仅仅留下base class对象成分,derive部分的性质被完全切割掉了。
可以用以下代码理解:
// 基类window
class Window
{
public:
...
string name()const;
virtual void display()const;
};
// 子类WindowWithScrollBars
class WindowWithScrollBars
{
public:
...
virtual void display() const;
};
现在我们有个打印窗口名称的函数:
void PrintWindowName(Window w)
{
cout<<w.name();
w.display();
}
如果我们传递的是一个derive对象:
WindowWithScrollBars wwsb;
PrintWindowName(wwsb);
很明显,当我们用这种pass-by-value的方式调用的时候,Window类的拷贝构造函数将被调用(这样derive对象就没有被拷贝构造!),也就导致了derive对象的特性化部分完全被切割了,所以在这里无论怎么调用,都只会执行Window类的display函数!
内置类型是否都适合采用pass-by-value方法?
不一定。第一大原因是内置类型虽小,但是把它放进一个类中,某些编译器会拒绝将之放入缓存器内,而如果是“光秃秃”的内置类型(即不放在类中),那么编译器将会很乐意放进缓存器内。
假如是前者,那么使用pass-by-reference是更好的,因为引用底层是用指针来实现的,所以放进缓存器内是绝无问题的。
还有一个原因是因为虽然现在是内置类型,不排除以后扩大,变成一个复杂的用户自定义类型。为长远考虑,使用pass-by-reference为佳。
作者总结
尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可以避免切割问题。
以上规则并不适用于内置类型,以及STL迭代器和函数对象。对它们而言,pass-by-value往往比较适当。