SamJ's blog

EffectiveC艹笔记(七)

Word count: 1.4kReading time: 5 min
2019/12/15 Share

条款三十一: 将文件间的编译依赖降至最低

当我们实现定义一个Person类时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string>
#include "date.h"
#include "address.h"

class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string _theName;
Date _theBirthDate;
Address _theAddress;
}

由于前面的include会导致Person的定义文件和其包含的头文件形成了编译依赖关系,如果头文件中有所改变,就会导致任何使用Person类的文件要重新编译。考虑用类的声明,替换include, 即替换定义。

为了将对象的实现细节和对象的定义分离开来,使用pimpl模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <string>
#include <memory> //通常,标准库的include是无法/无需替换的

class PersonImpl;
class Date;
class Address;

class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string address() const;
std::string birthDate() const;
private:
std::shared_ptr<PersonImpl> pImpl;
}

通过这样的设计,使用Person的客户就完全与Date,AddressPerson的实现细节所分离。

接口与实现分离的思想:

  • 如果能用对象的引用或指针完成任务,就不要使用对象本身。

  • 尽量以class的声明式代替定义式(include

  • 为声明式和定义式提供不同的头文件,提供这样的两个头文件是程序库作者的任务。

    1
    2
    3
    #include "datefwd.h"			//这个头文件内有class Date 的声明,相当于class Date;
    Date today();
    void clearAppointments(Date d);

另一个实现这种分离的方式是把Person定义成虚基类。

  • 支持编译依赖最小化的思想是:不依赖于定义式,依赖于声明式。有两种方式实现,pimpl设计模式,虚基类
  • 程序库头文件应该以仅有声明式的形式存在,包括设计template的情况()。

条款三十二:确保public继承构建出的是“is-a”的关系

以C++进行面向对象编程,public继承意味着“is-a”的关系。意思是如果以class D继承class B,你所表达的含义是每一个类型为D的对象同时也是一个类型为B的对象,需要B的地方,D对象可以使用,反之却不成立。

但我们在实现时却不能单凭直觉就这样做。举两个例子:

  1. 企鹅是一种鸟,同时,鸟可以飞,如果单纯的描述这样的事情:

    1
    2
    3
    4
    5
    6
    7
    8
    class Bird{
    public:
    virtual void fly();
    };

    class Penguin:public Bird{
    ...
    }

    这时,出现了企鹅可以飞的错误,在说鸟可以飞的时候,要注意并不是所有的鸟都可以飞。

    对于这个问题,这样设计显然更好:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Bird{
    ... //没有fly函数
    }
    class FlyingBirt: public Bird{
    public:
    virtual void fly();
    ...
    };
    class Penguin: public Bird{
    ... //没有fly函数
    }

    我认为最好还是使用这种在编译器拒绝错误的方式。

  2. 正方形类是否应该继承矩形类?

    按照我们在学校学过的,正方形是一种矩形,我们会得到这样的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Rectangle{
    public:
    virtual void setHeight(int newHeight);
    virtual void setWeidth(int newWidth);
    virtual int height() const;
    virtual int width() const;
    };
    void makeBigger(Rectangle& r) //增加r的面积
    {
    int oldHeight = r.height();
    r.setWidth(r.width() + 10); //r的宽度加10
    assert(r.height() == oldHeight);
    }

    class Square: public Rectangle{...};

    Square s;
    assert(s.width() == s.height());
    makeBigger(s);
    assert(s.width() == s.height());

    显然,在makeBigger后,出现了问题,正方形不再是正方形。所以以is-a表述正方形和矩形的关系并不正确。

is-a并不是唯一存在于class之间的关系,还有has-a和is-implemented-in-terms-of。正确并准确地实现这些关系是C++的OOP中很重要的部分。

  • public意味着is-a,适用于基类身上的每一项操作也要适用于派生类。

条款三十三:避免遮掩继承而来的名称

这个条款其实是有关于作用域的问题。

在函数中会发生局部变量遮盖全局变量的现象:

1
2
3
4
5
6
int x;
void someFunc()
{
double x;
std::cin >> x;
}

这时cin操作的对象会是函数内定义的double x

类似的,类在进行继承时也会出现这种名称相同覆盖的现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
private:
int _x;
};

class Derived: public Base{
public:
virtual void mf1();
void mf3();
void mf4();
};

Derived d;
int x;
d.mf1(x); //会出现错误,派生类中的mf1()遮盖了基类中的mf1()
d.mf3(x); //同样的错误

这里不论函数是虚函数还是非虚函数,都会发生这种同名覆盖现象,特别是在派生类中重载了基类的函数时,就不能再调用基类的函数,这不符合我们前面提到的is-a关系。为了防止这种遮盖现象,可以使用using

1
2
3
4
5
6
7
8
class Derived: public Base{
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};

现在就不会再出现上面的错误。

如果不想继承基类的所有函数(在非public继承下),这时要用到转交函数:

1
2
3
4
5
6
7
8
9
10
11
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
};

class Derived: private Base{
public:
virtual void mf1()
{ Base::mf1();}
};

这时派生类只能调用无参版本的mf1函数。

  • 派生类内的名称会对基类的同名函数进行掩盖,在public继承下可以使用using避免出现这样的情况。
  • 转交函数可以在不想继承基类所有函数的情况下使用。
CATALOG
  1. 1. 条款三十一: 将文件间的编译依赖降至最低
  2. 2. 条款三十二:确保public继承构建出的是“is-a”的关系
  3. 3. 条款三十三:避免遮掩继承而来的名称