Effective C++ (4): Designs and Declaration

Introduction

这一章主要讲述了如何去设计对象, 接口.

Rule 18: Make Interfaces easy to use correctly and hard to use incorrectly

在设计接口的时候, 应该尽量做到, 如果使用者能够通过编译, 那么就应该能得到他所期望的结果. 如果有任何错误, 应该尽量提前至编译期就能够报出来.

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

}
在使用时, 可能客户会弄错 day 和 month 的位置, 产生错误, 那么可以定义 类型系统(type system) 来避免这种问题, 定义类似于 struct Day, struct Month, struct Year, 并且在类型系统中限制变量的范围等.

在设定资源获取接口的时候, 通过返回 shared_ptr 能够阻止一大群用户犯下资源泄漏的问题.

std::tr1::shared_ptr<Investment> createInvestment()
{
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);    // declare null pointer
    retVal = ...;   // point to the right object
    return retVal;
}

如果能够直接将pInv指向正确的对象会比先指向null, 再指向正确的对象更好. 这样做最大的好处是能够放置 “cross-DLL problem“, 就是解决 “跨 DLL 之 new/delete 成对应用” 的问题, 经常我们在一个文件new了却忘了在另一个文件delete, 使用 shared_ptr 可以避免这一点.

Remeber:

  • 设计接口应该容易被正确使用, 不容易被错误使用. 当错误使用的时候应该尽可能提前报错
  • “促进正确使用” 的办法包括接口的一致性,以及与内置类型的行为兼容
  • “阻止勿用” 的办法包括建立新类型, 限制类型上的操作, 束缚对象值, 以及消除客户的资源管理责任
  • shared_ptr 支持定制型删除器( custom deleter ), 可防范 cross-DLL problem

Rule 19: Treat class design as type design

在设计一个类之前, 请先考虑如下问题:

  1. 新type的对象应该如何被创建和销毁? 影响到你的构造函数, 析构函数, 以及内存分配函数和释放函数.
  2. 对象的初始化和赋值有怎样的区别? 区分出如何书写 copy assignment 函数
  3. 对象如果被pass-by-value的时候意味着什么? 思考如何书写拷贝构造函数
  4. 什么是新 type 的”合法值”? 考虑建立值的约束, 异常抛出等
  5. 新type需要配合某个继承图系( inheritance graph )吗? 影响到 virtual 函数
  6. 新type需要怎样的转换? 对于彼此有转换行为的类型, 需仔细考虑
  7. 什么样的操作符对于新type是合理的? 考虑到重载操作符等
  8. 谁会使用新type的成员? 决定成员的 public, private, protected, 也帮助决定哪个 classes 或 functions 是 friends.
  9. 什么是新type的”未声明接口” (undeclared interface)? 它对效率, 异常安全以及资源是用提供何种保证?
  10. 你的新type有多么一般化? 考虑到 template 的问题
  11. 你真的需要一个新type吗? 如果只是新加个简单的机能, 可能单纯定一个 non-member 函数或 templates 更有效

Remeber:
Class的设计就是type的设计. 在定义一个新type之前, 请确定你已经考虑过了本条款覆盖的所有讨论主题.

Rule 20: Prefer pass-by-reference-to-const to pass-by-value

slicing problem说的是当一个derived对象以by-value方式传递并被视为一个base class对象的时候, base class对象的copy构造函数会被调用, 并且 “造成此对象的行为像一个 derived class对象”

Remeber:

  • 尽量以 pass-by-reference-to-const 替换 pass-by-value, 前者通常比后者高效, 并且避免切割问题(slicing problem)
  • 以上规则并不适用于内置类型, 以及STL的迭代器和函数对象. 对它们而言, pass-by-value 往往比较合适

Rule 21: Don’t try to return a reference when you must return an object

在该返回值的时候还是需要返回值的. 一般返回有以下几种可能:

  • 返回值. 承受该有的构造和析构成本
  • 返回指向 heap-allocated 对象的指针. 传递指针的方式会快一点.
  • 返回指向 static 变量的 reference. 一般用于单例模式.

Remeber:
绝对不要返回一个pointer或reference指向一个local stack对象, 或返回一个 reference 指向一个 heap-allocated 对象, 或返回 pointer 或 reference 指向一个 local static 对象但是却不是单例.

Rule 22: Declare data members private

成员变量的封装性与”成员变量的内容改变时所破坏的代码数量”成正比. 所以无论是使用protect还是public, 当一个变量移除的时候, 所有使用它的客户代码都会被破坏. 而如果全部使用函数来获取变量, 那么可以通过别的途径来实现这个函数.

Remeber:

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

Rule 23: Prefer non-member non-friend functions to member functions

举个例子, 对于浏览器:

class WebBrowser{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
}

如果要写一个函数, 执行以上三个成员函数, 那么我是写一个 clearEverything() 作为成员函数好一些呢? 还是写一个 non-member, non-friend 函数好一些呢?
封装性可理解为, 让尽量少的函数可以访问类的数据.
从这种层面上来说, non-member, non-friend 函数会好一些.

Remeber:
non-member non-friend functions 相较于 member functions 更好, 能够增加类的封装性, 包裹弹性( packaging flexibility ).

Rule 24: Declare non-member functions when type conversions should apply to all parameters

如果你需要为某个函数的所有参数进行类型转换的, 那么这个函数必须是个 non-member 的.

以有理数与整数相乘为例子:

class Rational{
public:
Rational(int numerator = 0, int denominator = 1); /// 构造函数不为 explicit, 允许 int-to-Rational 隐式转换
int numerator() const;
int denominator() const;

private:

};

如果将operator* 作为成员函数来写的话:

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

那么在类型转换的时候会出现问题:

Rational oneHalf(1,2);
Rational result;
result = onHalf*2;  // 没问题, 会先将 2 隐式转换为 Rational, 然后再相乘
result = 2* onHalf; // 报错, Rational不在int构造函数参数列表中, 不会进行隐式类型转换.

在这种时候, 需要对operator所有参数均进行类型转换, 只能通过 non-member function 来实现:

class Rational{
    ...
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(),
            lhs.denominator() * rhs.denominator());
}

这样就能解决刚刚类型转换的问题, 那么这个函数要不要设置为friend函数呢? 在不需要使用到class成员变量就能够达到目的的情况下, 不需要设置为 friend , 可以提高封装性.

Remember:
如果你需要为某个函数的所有参数进行类型转换的(包括this指针所隐喻参数), 那么这个函数必须是个 non-member 的.

Rule 25: Consider support for a non-throwing swap

这条rule解释起来比较麻烦. 我们先要了解 std swap 是如何实现的:

template<typename T>
void swap(T &a, T &b){
    T temp(a);
    a = b;
    b = temp;
}

整个过程包括3次复制, 所以效率在某些情况是很低的. 比如一个类里面只有一个指针的时候, swap实质上只是交换指针所指的东西, 而通过 std::swap 则还会复制指针所指向的内容, 造成效率低下:

class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)
    {
        ...
        *pImpl = *(rhs.pImpl);   //复制了指针指向之物
        ...
    }
private:
    WidgetImpl* pImpl;
}

对于上面这种情况, 就需要制作自己特化的swap了. 遵从以下步骤:

  1. 提供一个 public swap 函数, 并保证不抛出异常.

    class Widget{
    public:
        ...
        void swap(Widget& other)
        {
            using std::swap;
            swap(pImpl, other.pImpl);
        }
        ...
    };
    
  2. 在你的 class 的命名空间内提供一个 non-member swap, 并令它调用上述 swap成员函数.

    namespace WidgetStuff{
        ...
        template<typename T>
        class Widget { ... };
        ...
        template<typename T>
        void swap(Widget<T>& a, Widget<T>& b)
        {
            a.swap(b);
        }
    }
    
  3. 如果你正在编写一个 class (而非 class template), 为你的 class 特化 std::swap. 并令它调用你的 swap 成员函数.

    namespace std{
        template<>              // 修订后的 std::swap 特化版本
        void swap<Widget>( Widget &a, Widget &b)
        {
            a.swap(b);
        }
    }
    

Remeber:

  • 当 std::swap 对你的类型效率不高的时候, 提供一个 public swap 成员函数, 并确保该函数不抛出异常.
  • 然后再实现一个 non-member swap 来调用前者, 对于 classes(而非 templates), 也请特化 std::swap
  • 调用swap的时候, 请先使用 using std::swap, 然后再不带任何”命名空间资格修饰”调用swap

系列文章

Effective C++ (1): Accustoming Yourself to C++
Effective C++ (2): Constructors, Destructors, and Assignment Operators
Effective C++ (3): Resource Management
Effective C++ (4): Designs and Declaration
Effective C++ (5): Implementation
Effective C++ (6): Inheritance and Oject-Oritent Design
Effective C++ (7): Templates and Generic Programming
Effective C++ (8): Customizing new and delete
Effective C++ (9): Miscellany

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这个错误提示是因为在代码中缺少了声明语句。在编写代码时,需要在使用变量或函数之前先进行声明,告诉编译器这些变量或函数的类型和名称。如果没有声明语句,编译器就无法识别这些标识符,从而导致错误。因此,需要检查代码中是否缺少了声明语句,并进行相应的补充。 ### 回答2: error是英文中表示错误或者问题的一种词汇。在计算机领域,error则指代着计算机系统或程序发生的错误或异常。计算机程序经常出现错误,有时甚至可能导致系统崩溃。在程序开发和调试过程中,发现和解决错误是一个非常重要的流程。 可以通过多种方式发生错误,例如语法错误、逻辑错误、内存错误等等。对于不同类型的错误,开发人员需要采取不同的解决策略。在早期的计算机编程中,错误处理通常是通过向终端打印错误消息来提醒用户程序出现了什么问题,并终止程序运行。 如今,计算机错误处理变得更加复杂和精细,常见的方法包括使用调试器来定位并修复错误、使用日志记录系统来记录错误、使用测试工具来确保程序正确性、加强代码审查活动等等。在软件的维护和升级过程中,错误处理依然是一个非常关键的部分。 也许可以说,error是计算机领域中最常见的词汇之一,没有哪个编程人员能够避免错误的发生。因此,正确处理错误、学习如何定位问题、以及熟练运用调试和测试工具是每一个程序开发人员必备的技能。为了使程序更加稳定和可靠,许多开发团队都会加强错误处理部分的培训和知识分享活动,以便在高度竞争的市场中获得更大的优势。 ### 回答3: Error是英语中的一个词语,通常翻译为“错误”、“故障”等。在计算机领域,Error是一个常见的术语,代表着计算机程序在执行时出现的错误。因此,Error通常是与程序或操作系统有关的问题,涉及到了数据处理、存储、通信等方面。 Error可以分为两种类型:软件Error和硬件Error。软件Error是指程序在运行过程中出现的问题,如输入错误、逻辑错误、编译错误等。而硬件Error则是指电脑硬件出现的问题,如CPU故障、硬盘故障、内存故障等。 Error的出现可能会导致程序无法正常执行,甚至导致系统崩溃。同时,Error也是程序开发人员所关注的问题之一,因为它会对程序的稳定性和可靠性造成影响。 为了避免Error的出现,程序员通常会进行错误的预防和处理。一般来说,预防Error的方法有以下几种:规范输入、检查边界、避免死循环、提高代码健壮性等。而当Error已经出现时,程序员需要寻找错误的根源,并进行调试和修复。 总之,Error是程序开发中不可避免的问题。程序员需要采取一系列措施来避免Error的出现,并及时处理已经出现的Error,从而确保程序的稳定性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值