Effective C++(四)接口设计与声明

  1. 条款18:让接口容易被正确使用,不易被误用

    • 请记住:
      • 好的接口很容易被正确使用,不容易被误用。
      • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
      • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
      • tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。
    • 许多客户端错误可以因为导入新类型而获得预防。令day, month, year称为成熟且经充分锻炼的classes并封装其内数据,比简单实用上述的struct好。办法之一是利用enum表现月份,但enums不具备我们希望拥有的类型安全性。比较安全的解法是预先定义所有有效的Months。预防客户端的另一个错误是,限制类型内什么事可做,什么事不能做,常见的限制是加上const。
    • 除非有好理由,否则应该尽量令你的types的行为与内置types一直。
    • 避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。
    • tr1::shared_ptr允许当智能指针被建立起来的指定一个资源释放函数绑定于智能指针身上。
    • 返回一个“将getRidOfInvestment绑定为删除器”的tr1::shared_ptr。tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数为0时将被调用的“删除器”
      std::tr1::shared_ptr<Investment> createInvestment()
      {
      std::tr1::shared_ptr<Investment> reVal( static_cast<Investment *>0, getRidOfInvestment);
      reVal = ...;
      return retVal;
      }
      
      “将原始指针传给pInv构造函数”会比“先将pInv初始化为null再对它做一次赋值操作”为佳。
    • tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的“cross-DLL problem”。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。
    • 最常见的tr1::shared_ptr实现品来自Boost。Boost的tr1::shared_ptr是原始指针的两倍大。它比原始指针大且慢,而且使用辅助动态内存。
  2. 条款19:设计class犹如设计type

    • 当你定义一个新的class,也就定义了一个新type。
    • 新type的对象应该如何被创建和销毁
    • 对象的初始化和对象的赋值该有什么样的差别
    • 新type的对象如果被pass by value,这意味着什么
    • 什么是新type的“合法值”
    • 你的新type需要配合某个继承图系吗?
    • 你的新type需要什么样的转换
    • 什么样的操作符和函数对此新type而言是合理的
    • 什么样的标准函数应该驳回
    • 谁该取用新type的成员
    • 什么是新type的“未声明接口”
    • 你的新type有那么一般化?
  3. 条款20:宁以pass-by-reference-to-const替换pass-by-value

    • 请记住:
      • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)
      • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
    • 参数的传递成本是n次构造函数和n次析构函数
    • pass-by-reference-to-const,这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建
    • 以by-reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成次对象的行为像个derived class对象”的那些特化性质全被切掉了,仅仅留下一个base class对象。
    • 如果窥视C++编译器的底层,你会发现,reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。
  4. 条款21:必须返回对象时,别妄想返回其reference

    • 请记住:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例
    • 所谓的reference只是个名称,代表某个既有对象。因为它一定是某物的另一个名称。
    • 函数创建新对象的途径有二:在stack空间或在heap空间创建之。
    • 一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。
  5. 条款22:将成员变量声明为private

    • 请记住:
      • 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
      • protected并不比public更具封装性。
    • protected成员变量就像public成员变量一样缺乏封装性。其实只有两种访问权限:private(提供封装)和其他(不提供封装)
  6. 条款23:宁以non-member、non-friend替换member函数

    • 请记住:宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
    • 推崇封装的原因:它使我们能够改变事物而只影响有限客户。愈少代码可以看到数据(也就是访问它),愈多的数据可被封装,而我们也就愈能自由地改变对象数据。愈多函数可以访问它,数据的封装性就愈低。
    • 导致较大封装性的是non-member、non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量
    • namespace和classes不同,前者可跨越多个源代码文件而后者不能。
    • 分离它们的最直接做法就是将书签相关便利函数声明于一个头文件,将cookie相关便利函数声明于另一个头文件,再将打印相关便利函数声明于第三个头文件。将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数。
  7. 条款24:若所有参数皆需类型转换,请为此采用non-member函数

    • 请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-menber
    • 只有当参数被列于参数列内,这个参数才是隐式类型转换的合法参与者。地位相当于“被调用之成员函数所隶属的那个对象”——即this对象——那个隐喻参数,绝不是隐式转换的合法参与者。
  8. 条款25:考虑写出一个不抛出异常的swap函数

    • 请记住:
      • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
      • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap
      • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
      • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
    • 所谓swap两对象值,意思是将两对象的值彼此赋予对方。缺省情况下,swap动作可有标准程序库提供的swap算法完成。
    • “以指针指向一个对象,内含真正数据”那种类型,这是所谓的“pimpl手法(pointer to implementation)”
    • 一旦要置换两个Widget对象值,我们需要做的是置换其pImp指针。
    • 通常我们不能够(不允许)改变std命名空间内的任何东西,但可以为标准templates制造特化版本,使它专属于我们自己的classes。
    • 任何地点的任何代码如果打算置换两个Widget对象,因而调用swap,C++的名称查找法则(更具体地说是所谓argument-dependent lookup或koening lookup法则)会找到Widget的专属版本。
    • C++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap。
    • 首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。其次,如果swap缺省实现版的效率不足。你可以这样做:
      • 1.提供一个public swap成员函数,让它高效的置换你的类型的两个对象值。这个函数绝不该抛出异常
      • 2.在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
      • 如果你正编写一个class,为你的class特化std::swap。并令它调用你的swap成员函数
      • 最后,如果你调用swap,请确定包含一个using声明式,以便std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap
    • 成员版的swap绝不可抛出异常。这一约束只施行于成员版!不可施行于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础,而一般情况下两者都允许抛出异常

猜你喜欢

转载自blog.csdn.net/u010991048/article/details/38277569