Effect C++ 笔记 【4 Designs and Declarations】

4 设计与声明

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

//这章举了几个例子,一个是用新的 struct 限制输入参数;  一个是智能指针保证 资源释放。   需要看完 STL 再回头看。

Tips:   

  1. “容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。  //全面考虑,各种可能得古怪的输入
  2. 接口一致性,以及与内置类型行为兼容,有助于 “正确使用”。
  3. 任何接口如果要求客户必须记得做某些事情,就是有 “不正确倾向”
  4. “阻止误用” 的办法  :  建立新类型,限制类型操作( const ),束缚对象值( emule ),消除客户资源管理责任( 智能指针 )
  5. tr1::shared_ptr 支持定制型删除器。 可防范 DLL问题,可被用来自动解除互斥锁等等。

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

如何设计高效的 classes,你要面对以下提问:

  1. 新 type 的对象如而后被创建和销毁?   ——  构造函数、析构函数、内存分配和释放(new、delete)
  2. 对象的初始化和对象的赋值该有什么样的区别? —— 构造函数、赋值操作符  (别混淆初始化和赋值,调款4)
  3. 新 type 的对象如果被pass by value,意味着什么? —— 拷贝构造函数定义一个type的pass by value 的实现
  4. 什么是新 type 的“合法值”? —— 成员函数必须进行错误检查工作
  5. 你的新 type 需要配合某个继承图系么? —— 如果你继承既有的class,就得受父类的约束,特别是“他们的函数是virtual”的影响(条款34、36)。如果其他类继承你的class,那会影响你所声明的函数-尤其是析构函数-是否为virtual(条款7)。
  6. 你的新 type 需要什么样的转换? —— 如果希望允许类型 T1 之物被隐式转换为 T2 之物,就必须在 class T1 内写一个类型转换函数(operator T2)或在 class T2 内写一个 non-explicit-one-argument(可被单一实参调用)的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数。
  7. 什么样的操作符和函数对此 type 而言是合理的? —— 某些该是 member 函数,某些则否 (23、24、46条款)
  8. 什么样的标准函数应该被屏蔽? —— 那些正是你必须声明为 private的
  9. 谁该取用新 type 的成员? —— 这个问题帮助你决定,那个成员为public,那个为protect或private。以及哪个class或function是friend。
  10. 什么是新 type 的“未声明接口”? —— 对效率、异常安全性(见条款29)以及资源运用提供何种保证?没看懂
  11. 你的新 type 有多一般化? —— 或许应该是一个 class template
  12. 真的需要一个新 type 吗?

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

传值调用,由拷贝构造函数完成。

使用传引用 代替 传值 两个作用:

  1. 对于自定义类型,节约资源(剩了拷贝构造,和析构)
  2. 防止 切割问题(slicing problem)    :    基类指针指向一个派生类对象 ,然后这个指针被函数传值调用,那么拷贝构造只复制了该对象的基类部分。

Tips:

  • 尽量传const引用 代替 传值。 高效且避免切割问题。
  • 只对 内置类型,和 STL 的迭代器、函数对象,使用传值

条款21:必须返回对象时,别妄想返回其 reference

所谓 reference 只是个别名,代表某个【既有】对象。

任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为他一定是某物的另一个名称。

 

Tips:

  1. 绝不要返回 pointer 或 reference 指向一个 local stack 对象                         // 局部变量销毁后,指针悬挂了
  2. 或 heap-allocated对象                                                                           //  可能无法正确的 delete 掉这个对象  ,比如 w=x*y*z,operator*返回引用的话,x*y返回的引用就无法delete
  3. 或 指向 local static 对象而又必须使用很多这样的对象                                  // 资源浪费
  4. 简单办法,【返回一个新对象】!

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

结论很简单:【成员变量应该是 private】

Tips:

  1. 将成员变量声明为 private。 好处:
    • 访问数据一致性: 如果public接口内每样东西都是函数,客户就不需要在打算访问class成员时犹豫是否使用小括号,因为样东西都是函数。
    • 细微划分访问控制:如果成员变量设为public,每个人都可以读写它。但是以函数取得或者设置其值,可以实现各种控制。
    • 为“所有可能得实现”提供弹性:例如,可使成员变量被读写时通知其他对象、验证class约束条件、函数前提和事后状态、多线程环境下执行同步控制......
  2. protected 并不比 public 更具封装性:   一旦你将一个成员变量声明为 public或protected 而客户开始使用它,就很难再改变那个成员变量涉及的一切。

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

考虑封装性:作为一种粗糙的量测,越多函数可以访问这个数据,那数据的封装性就越低。

条款22,曾说,成员变量应为private。因为如果它不是,就有无限的函数可以访问他。而 能够访问private成员变量的函数 只有class的 menber函数加上 friend函数 而已。

因此,“非成员非友元”函数比“成员函数”有更大的封装性。(注意是'非成员且非友元'。 友元函数和成员函数的访问权利是相同的)

 

C++,比较自然地做法是:   让这种 为对象提供便利的函数 成为一个non-member函数且位于 其服务的类 的同一个namespace内。

要知道,namespace和classes 不同,前者可以跨越多个源码文件而后者不能 。  

这正是C++标准库的组织方式。 标准库不是拥有单一的、整体的、庞大的 <C++StandardLibrary>头文件并在其中内含std命名空间里的每个东西,而是有数十个头文件,每个头文件声明std的某些机能。 但这种切割方式并不适用于class成员函数,因为一个class必须整体定义,不能分割为片段。

将所有便利函数放在多个头文件内,但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。 需要做的就是添加更多 非成员非友元函数 到此命名空间。

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

隐式转换 发生在 函数参数列表中。 对于member函数,隐式转换发生在 “被调用的成员函数所隶属的那个对象”--即this对象--的那个参数表。(这个主要是对operater说的,因为它们容易被重载。因此需要分清是member的还是non-member,接受的是两个参数lhs,rhs  还是  只有其中之一)

 

【重要】:  member函数的反面 是 non-member, 而不是 friend 。 与某class有关的函数如果不该成为member,不是就一定是friend。non-member函数也完全可以通过class的public接口完成一定功能

 

条款25:考虑写出一个不抛异常的 swap 函数

典型的标准程序库提供的 swap 写法就是 一个中间temp变量拷贝构造和copy assignment操作:

但是,复制中,常见就是“以指针指向一个对象,内含真正数据”那种类型。 所以为“pimple手法”(pointer to implementation)。   这样写Widget class,看起来想这样:

调换两个 Widget 对象值,我们需要做的 就是调换两个 pImpl 指针, 但缺省的swap算法不知道这点。 

我们希望告诉 std::swap  : 当 Widget 被置换时, 该做的是置换其内部 pImpl 指针。

一个做法是: 将 std::swap 针对 Widget 特化 !   

下面是基本构想,但目前这个形式无法通过编译:

【"template <>" 表示它是 std::swap 的一个全特化(total template specialization)版本,函数名称之后的“<Widget>”表示这一特化版本系针对“T是Widget”而设计。】

【通常,我们不允许改变 std 命名空间内的任何东西但是 可以为 标准template 制造特化版本 。】

这个函数无法编译,因为它企图访问 a和b 的private 成员 pImpl。  我们可以将这个特化声明为 friend, 但这里

我们令 Widget 声明一个名为 swap 的 public 成员函数。 然后将 std::swap特化, 令它调用该成员函数:

这种做法与 STL 容器有一致性 , 【 所有 STL 容器都提供 有 public swap 成员函数 和 std::swap 特化版本(用 以调用前者)。】   

 

假设Widget 和 WidgetImpl 都是 class templates 而非 class。那么,在Widget内放个swap成员函数没问题, 但在特化 std::swap 时遇上乱流,我们想写成这样:

看起来合理,但不合法。 我们企图偏特化一个 function template 。【 但C++只允许对 class template 偏特化 】

当你打算偏特化一个 function template 时,管用的做法是简单地为它添加一个重载版本。 

一般而言,重载 function template 没有问题。但 std 是个特殊的命名空间 。客户可以全特化std内的template , 但不能添加 新的template(其实是任何东西)到std里

 

那么,如何是好?    我们要让其他人调用swap时能取得我们提供的高效的template特定版本。  还是声明一个 non-member swap 让他调用 member swap, 但不再将那个non-member swap 声明为 std::swap 的特化版或重载版。

假设 Widget 所有相关机能都在 命名空间 WidgetStuff 内, 那整个结果像这样:

这个做法对 class 和 class template都行的通。  (如果,没有额外使用命名空间,上述每件事仍适用。 但,何必在 global 空间内塞满各式各样的 class , template,function, enum以及typedef 名称呢)

但是,你还应该为 class 特化 std::swap。 因为,如果你想让你的“class专属版”swap在尽可能多的语境下(就是说,在任何时候都能找到最合适的一个版本)被调用,你需要同时在该 class 所在命名空间内写一个 non-member版本,及一个 std::swap特化版本。

从客户角度看,假设你正写一个 function template, 其内需要置换两个对象:

应该调用哪个 swap ?????

我们希望的是:

这里,C++的名称查找法则 确保将找到 global 作用域 或 T 所在之命名空间内的 任何T专属swap。

  • 如果 T 是 Widget 类型,并位于 命名空间 WidgetStuff 内, 编译器使用"实名参数取决之查找法则"( argument-dependent lookup) 找出 WidgetStuff 内的 swap。
  • 如果 T 没有专属swap存在,编译器使用 std内的 swap 。  //  因为使用了using 声明
  • 如果 T 存在swap专属 特化版本, 那么因为你已这对 T 特化了 std::swap, 因此特化版被调用。

【总结】:

  • 如果 swap 缺省实现代码对你的 class 或 class template 提供可接受的效率,你不用做任何事情。
  • 如果 swap 缺省版本效率不足 (几乎总意味着 使用了 指针指向内容 ‘pimpl’ 的手法):
    1. 提供 public swap 成员函数。 这个函数绝不该抛出异常。
    2. 在你的 class 所在命名空间内,提供 non-member swap, 并用它 调用 1 中的 swap 成员函数。
    3. 如果你是class(而不是 class template),  特化 std::swap。并用它 调用 1 中的 swap 成员函数。
    4. 调用swap, 请包涵一个 using 声明式。 以便 让std::swap在函数内可见。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值