四、设计与声明--条款18-20

概述

  • 本章主要介绍良好的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往往比较适当。

posted @ 2018-09-17 15:08  _NewMan  阅读(220)  评论(0编辑  收藏  举报