Effective C++(4)


4.设计与声明

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

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这种性质
  • “促进正确使用”的方法包括接口的一致性,以及与内置类型的行为兼容
  • 阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • 尽量使用智能指针,避免跨DLL的 new 和 delete,使用智能指针自定义删除器来解除互斥锁(mutexes)

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

  • 新的type的对象应该如何被创建和销毁? 这会影响到类中构造函数、析构函数、内存分配和释放函数(operator new,operator new[],operator delete,operator delete[])的设计。
  • 对象的初始化和对象的赋值该有什么样的差别? 这个答案决定你的构造函数和赋值操作符的行为,以及其间的差异。
  • 新的type如果被pass by value,意味着什么? copy构造函数用来定义一个type的pass by value该如何实现
  • 什么是新type的“合法值”? 你的类中的成员函数必须对类中成员变量的值进行检查,如果不合法就要尽快解决或明确地抛出异常。
  • 你的新type需要配合某个继承图系吗? 如果你继承自某些已有的classes,你就受到那些classes的设计的束缚,特别是受到“它们的函数是virtual 或 non-virtual” 的影响。如果你允许其他classes继承你的class,那会影响你的所声明的函数——尤其是析构函数是否为virtual。
  • 你的新type需要什么样的转换? 如果你希望允许类型T1被隐式转换为类型T2,就必须在class T1内写一个类型转换函数或在class T2内写一个可被单一实参调用的构造函数。
  • 什么样的操作符和函数对此新type而言是合理的? 这会影响到你将为你的类声明哪些函数和重载哪些运算符。
  • 什么样的标准函数应该被驳回? 这会影响到你将哪些标准函数声明为= delete。
  • 谁该取用新 type 的成员? 这会影响到你将类中哪些成员设为 public,private 或 protected,也将影响到友元类和友元函数的设置。
  • 什么是新 type 的“未声明接口”? 为未声明接口提供效率、异常安全性以及资源运用上的保证,并在实现代码中加上相应的约束条件。
  • 你的新 type 有多么一般化? 如果你想要一系列新 type 家族,应该优先考虑模板类。

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

当使用按值传参时,程序会调用对象的拷贝构造函数构建一个在函数内作用的局部对象,这个过程的开销可能会较为昂贵。对于任何用户自定义类型,使用按常引用传参是较为推荐的:

bool ValidateStudent(const Student& s);

因为没有任何新对象被创建,这种传参方式不会调用任何构造函数或析构函数,所以效率比按值传参高得多。

使用按引用传参也可以避免对象切割(Object slicing) 的问题,参考以下例子:

class Window {
public:
    ...
    std::string GetName() const;
    virtual void Display() const;
};

class WindowWithScrollBars : public Window {
public:
    virtual void Display() const override;
};
void PrintNameAndDisplay(Window w) {    // 按值传参,会发生对象切片
    std::cout << w.GetName();
    w.Display();
}

此处在传参时,调用了基类Window的拷贝构造函数而非派生类的拷贝构造函数,因此在函数种使用的是一个Window对象,调用虚函数时也只能调用到基类的虚函数Window::Display。

由于按引用传递不会创建新对象,这个问题就能得到避免:

void PrintNameAndDisplay(const Window& w) { // 参数不会被切片
    std::cout << w.GetName();
    w.Display();
}
  • 尽量以pass-by-reference-to-const 替换pass-by-value。 前者通常比较高效,并可避免切割问题
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较稳妥

条款21:函数返回时,不要随意返回其引用

返回一个指向函数内部局部变量的引用是严重的错误,因为局部变量在离开函数时就被销毁了,除此之外,返回一个指向局部静态变量的引用也是不被推荐的。

尽管返回对象会调用拷贝构造函数产生开销,但这开销比起出错而言微不足道。

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

条款22:将成员变量声明为private

出于对封装性的考虑,应该尽可能地隐藏类中的成员变量,并通过对外暴露函数接口来实现对成员变量的访问:

class AccessLevels {
public:
    int GetReadOnly() const { return readOnly; }
    void SetReadWrite(int value) { readWrite = value; }
    int GetReadWrite() const { return readWrite; }
    void SetWriteOnly(int value) { writeOnly = value; }

private:
    int noAccess;
    int readOnly;
    int readWrite;
    int writeOnly;
};

通过为成员变量提供 getter 和 setter 函数,我们就能避免客户做出写入只读变量或读取只写变量这样不被允许的操作。

protected和public一样,都不该被优先考虑。假设我们有一个public成员变量,最终取消了它,那么所有使用它的客户代码都将被破坏;假设我们有一个protected成员变量,最终取消了它,那么所有使用它的派生类都将被破坏。

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

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

假设有这样一个类:

class WebBrowser {
public:
    ...
    void ClearCache();
    void ClearHistory();
    void RemoveCookies();
    ...
};

如果想要一次性调用这三个函数,那么需要额外提供一个新的函数:

void ClearEverything(WebBrowser& wb) {
    wb.ClearCache();
    wb.ClearHistory();
    wb.RemoveCookies();
}

注意,虽然成员函数和非成员函数都可以完成我们的目标,但此处更建议使用非成员函数,这是为了遵守一个原则:越少的代码可以访问数据,数据的封装性就越强。此处的ClearEverything函数仅仅是调用了WebBrowser的三个public成员函数,而并没有使用到WebBrowser内部的private成员,因此没有必要让其也拥有访问类中private成员的能力。

这个原则对于友元函数也是相同的,因为友元函数和成员函数拥有相同的权力,所以在能使用非成员函数完成任务的情况下,就不要使用友元函数和成员函数。

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

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

现在我们手头上拥有一个Rational类,并且它可以和int隐式转换:

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);
    ...
};

当然,我们需要重载乘法运算符来实现Rational对象之间的乘法:

class Rational {
public:
    ...
    const Rational operator*(const Rational& rhs) const;
};

将运算符重载放在类中是行得通的,至少对于Rational对象来说是如此。但当我们考虑混合运算时,就会出现一个问题:

Rational oneEight(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf / oneEight;

result = oneHalf * 2;    // 正确
result = 2 * oneHalf;    // 报错

在调用operator*时,int类型的变量会隐式转换为Rational对象,因此用Rational对象乘以int对象是合法的,但反过来则不是如此。

所以,为了避免这个错误,我们应当将运算符重载放在类外,作为非成员函数:

const Rational operator*(const Rational& lhs, const Rational& rhs);
  • 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-memeber

参考:https://zhuanlan.zhihu.com/p/613356779

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值