Chapter 4 of Effective C++ (设计与声明)

条款18:让接口容易被正确使用,不易被误用
Make interfaces easy to use correctly and hard to use incorrectly

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。

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

乍见之下这个接口很合理,但它使客户很容易犯下至少两个错误。

// 以错误次序传递参数
Date d(30, 3, 1995);

// 传递一个无效的月份或天数
Date d(2, 30, 1995);

许多客户端错误可以因为导入新类型而获得预防(类型系统)。让我们导入简单的外覆类型(wrapper types)来区别天数、月份和年份,然后在Date构造函数中使用这些类型:

struct Day{
explicit Day(int d) : val(d) {}
int val;
}

struct Month{
explicit Month(int m) : val(m) {}
int val;
}

struct Year{
explicit Year(int y) : val(y) {}
int val;
}

class Date{
public:
    Date(const Month& m, const Day& d, const Year& y);
}

Date d(30, 3, 1995);                            //错误!不正确的类型(必须显式调用)
Date d(Day(30), Month(3), Year(1995));          //错误!不正确的类型
Date d(Month(3), Day(30), Year(1995));          //正确

令Day, Month和Year成为成熟且充分锻炼的classes并封装其数据,比简单使用structs好。但即使当前也足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。

还有一个方法是预先定义所有有效的Month(如果直接用enum则不具备类型安全性,enum可以被当作int使用)。

class Month{
public:
    static Month Jan() { return Month(1); }
    ...
    static Month Dec() { return Month(12); }
private:
    explicit Month(int m);
    ...                    // 月份专属数据
}

Date d(Month::Mar(), Day(30), Year(1995));

如果“以函数替换对象,表现某个特定月份”让你觉得诡异,或许是因为忘记了non-local static对象的初始化次数有可能出现问题。建议回顾条款4。

预防客户错误的另一个办法是,限制类型内什么事可做,什么不可做。常见的是加上const,例如条款3曾说明“以const修饰operator*”的返回类型可阻止客户因“用户自定义类型”而犯错。

if (a*b=c) ...    //其实是想做一个比较的操作

下面是另一个一般性准则除非有好理由,否则应该尽量令你的types行为与内置types一致。例如a和b都是int,对a*b赋值不合法,一般情况你的types也应有此相同的表现。

同时,如果任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事情。

Investment* createInvestment();

为避免资源泄露,createInvestment返回的指针必须被删除,但这给了客户犯错的机会:没有删除指针,或删除同一个指针超过一次。

条款13将返回的指针存储与智能指针内,因而将delete责任推给智能指针。但万一客户忘记使用智能指针怎么办?因此,我们可以令factory函数直接返回智能指针。

std::shared_ptr<Investment> createInvestment();

shared_ptr有一个好的性质是:它会自动使用它的“每个指针专属的删除器”,从而消除一个潜在的客户错误“cross-DLL problem”。这个问题发生于“对象在动态连接程序库中被new创建,却在另一个DLL内被delete销毁”。而shared_ptr则可避免此问题。

(因为new/delete使用的是局部堆,也就是说不同的DLL虽然共享一个地址空间,但完全可能会维护不同的局部堆(堆分段),这与编译器的实现有关。如果是不同的局部堆,当你在DLL中new时,是在DLL的堆中分配的;而当你在DLL2中delete时,DLL2会认为它是在DLL2的局部堆中分配的,从而用DLL2的堆信息去释放它,从而可能导致错误)

请记住:

  • 好的接口很容易被正确使用,不容易被误用。你应当在你的所有接口中努力达成这些性质。
  • “促进正确使用”的办法包括接口一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • shared_ptr支持定制删除器。这可预防DLL问题,可被用来自动解除互斥锁。
条款19:设计class犹如设计type
Treat class design as type design

当你定义一个新class,也就定义了一个新type。其他内容太抽象了,我还没达到这个水平。。。

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

当函数参数为值时,使用时则会调用对象的拷贝构造函数生成一个新的对象。

class Person{
public:
    Person();
    virtual ~Person();
    ...
private:
    std::string name;
    std::string address;
}

class Student: public Person{
public:
    Student();
    ~Student();
    ...
private:
    std::string schoolName;
    std::string schoolAddress;
}

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

上述函数的调用(以by value方式传递一个Student对象)会造成student和person,以及四个string的构造/析构函数的调用。而使用pass by reference-to-const则可以避免此问题。

bool validateStudent(const Student& s);

同时,以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,则仅有base class的拷贝构造函数会被调用。

class Window{
public:
    std::string name() const;
    virtual void display() const;
};

class WindowWithScrollBars: public Window{
public:
    ...
    virtual void display() const;
}

void printNameAndDisplay(Window w)
{
    w.display();
}

//当你传递一个WindowWithScrollBars的对象
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

以上函数调用的总是Window::display,而不是WindowWithScrollBars::display。解决此问题的方法就是以by reference-to-const传递w。

void printNameAndDisplay(const Window& w)
{
    w.display();
}

如果窥视C++编译器底层,reference往往以指针实现,因此pass by reference通常意味真正传递的是指针。而对于内置类型而言,pass by value往往比pass by reference的效率高些。这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为pass by value,并且迭代器和函数对象的实践者有责任看看它们是否高效且不受切割问题影响。

请记住:

  • 尽量以pass by reference to const替换pass by value。前者通常效率较高,且可避免切割问题。
  • 以上规则并不适用于内置类型,以及STL迭代器和函数对象。对他们而言,pass by value往往比较稳当。

条款21:必须返回对象时,别妄想返回其reference
Don't try to return a reference when you must return an object

考虑一个表示有理数的类,内含一个函数用来计算两个有理数乘积:

class Rational
{
public:
    Rational(int numerator = 0, int denominator = 1);
private:
    int n, d; //分子和分母
    friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

这个版本的operator*为值传递方式,我们需要考虑其对象的构造和析构成本。假设我们返回对象的引用会怎么样呢?

  • 在栈上创建Rational对象:
const Rational& operator*(const Rational& lhs, 
						  const Rational& rhs)
{
	Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // warning! 糟糕的代码
	return result;
}
  • 在堆上创建Rational对象:
const Rational& operator*(const Rational& lhs, 
					      const Rational& rhs)
{
    Rational *result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d); //更糟糕的写法
    return *result; //虽然编译器不报错,但是逻辑上是错误的
}

 这里你仍然需要为构造函数的调用买单,因为对new分配的内存进行初始化是通过调用一个合适的构造函数来实现的,而且现在有另外一个问题:谁该对着被你new出来的对象实施delete?

Rational w, x, y, z;
w = x*y*z; //相当于operator*(operator*(x,y),z);

即使调用者诚实谨慎,调用者也难免遇到上述场景,在同一个语句中调用了两次operator*,因此使用了两次new,这也需要使用两次delete来对new出来的对象进行销毁。但却没有合理的办法取得operator*返回的references背后隐藏的那个指针,这会导致资源泄露。

  • 创建static Rational对象:

可能注意到了,不管是在堆上还是栈上创建从 operator* 返回的结果,你都必须要调用一个构造函数。可能你能回忆起来我们的初衷是避免这样的构造函数调用。可能你认为你知道一种只需要调用一次构造函数,其余的构造函数被避免调用的方法。这种方法基于另外一种 operator* 的实现:令其返回指向static Rational对象的引用:

const Rational& operator*(const Rational& lhs, 
						  const Rational& rhs)
{	// warning, 又一堆烂代码
    static Rational result; //静态局部变量
 	...                     //将lhs乘以rhs,然后将结果保存在result中
    return result;          //虽然编译器不报错,但是逻辑上是错误的
}

就像所有用上static对象的设计一样,这一个也立刻造成我们堆多线程安全性的疑虑。不过这还只是它显而易见的弱点,如果想看看更深层的瑕疵,考虑以下这些完全合理的客户代码。

bool operator==(const Rational& lhs, 
				const Rational& rhs); // for Rationals
Rational a, b, c, d;
...
if ((a * b) == (c * d)) {
	//乘积相等,做相应动作
} else {
	//不相等,做相应动作
}

两次operator*调用的确各自改变了static Raitional对象值,但由于它们返回的都是reference,实际上是同一个Rational自己和自己进行比较。

书上还讲了一个可能的错误方法,static数组。在此就不讲了,感觉这个方法从想法开始就不合理。

  • 一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

当然,你会从 operator* 的返回值中引入构造和析构的开销,但从长远来看,这是为正确的行为付出了一个小的代价。(而且大多数编译器会进行返回值优化 (Return Value Optimization, RVO) )

请记住:

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

条款22:将成员变量声明为private
Declare data members private
  • 语法一致性:如果成员变量不是public,客户则只能通过成员函数进行访问。如果public的每样东西都是函数,则客户访问类成员时就不需要记住是否需要使用小括号了。
  • 使用函数可以让你对成员变量的处理有更精确的控制:个人理解就是大家都可以随意修改你的成员变量,而通过函数的函数,他们只能调用你提供的函数。
  • 封装:你可以修改成员变量相关的函数,而不让客户知晓类的内部实现已经发生了变化。同时,如果变量为public,改变任何public事物的能力还是极端受到束缚,因为那会破坏太多客户码。

protected成员变量和public十分类似。就连封装来说,protected也并非高于public。假设我们有一个public成员变量,我们最终取消了它。多少代码可能因此受到破坏呢?那是一个不可知的大量。而对于protected成员变量来说,所有derived classes都会被破坏。

请记住:

  • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供充分的实现弹性。
  • protected并不比public更具封装性。

---

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值