读书笔记4: <<Effective C++>>之设计与声明

每一个Item都很经典,都需要去思考揣摩,我在这里将要点抽象出来,便于日后快速回忆;我只是在做文章的“搬运工”。

Item 18 使接口易于正确使用

1. function接口,class接口,template接口......每一种接口都是客户(调用者)与你的代码互动的手段。

2. 防止可能的客户(调用)错误的另一个方法是:限制类型内什么事可以做什么事不能做;施加限制的一个普通方法就是加上 const。

3. 避免自定义与内置类型不兼容的一个方法是提供行为一致的接口。很少有其他性质比得上“一致性”更能使“接口容易被正确使用”,也很少有其它性质比得上“不一致性”更加剧接口的恶化。

4. shared_ptr 能使接口易于正确使用,一个消除某些客户错误的简单方法。但它比一个裸指针大,比一个裸指针慢,而且要使用辅助的动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。另外shared_ptr将动态分配内存用于簿记和 deleter 专用(deleter-specific)数据,当调用它的 deleter 时使用一个虚函数来调用,在一个它认为是多线程的应用程序中,当引用计数被改变,会导致线程同步开销。

小节:不同的function,class,template之间的调用、交互、读、写都是接口操作。

疑问:如果想深入理解shared_ptr,还需要很多时间,暂时跳过,后面再看。

Item 19 视类设计为类型设计

1. 像语言设计者一样在语言内建类(类型)的设计中倾注大量心血。

小节:这一小节中的所有内容都包含在其它小节中,看完之后再回来想想,就不再拷贝了。

Item 20 传“常量引用”取代传值

1. 传const引用, 没有任何构造函数和析构函数被调用,因为没有新的对象被构造。

2. 以传引用方式传递参数还可以避免切断问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象的行为像一个派生类对象的特殊特性被“切断”了。

3. 如果你有一个内建类型的对象(例如,一个 int),以传值方式传递它常常比传引用方式更高效。那么,对于内建类型,当你需要在传值和传引用给 const 之间做一个选择时,没有道理不选择传值。同样的建议也适用于 STL 中的迭代器(iterators)和函数对象(function objects),因为,作为惯例,它们就是为传值设计的。迭代器(iterators)和函数对象(function objects)的实现有责任保证拷贝的高效并且不受切断问题的影响。

疑问:STL 中的迭代器(iterators)和函数对象(function objects)使用值传递为什么也高效呢?

Item 21 当你必须返回一个对象时不要试图返回一个引用。

1. 一个引用仅仅是一个名字,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,你应该立刻问自己它是什么东西的另一个名字,因为它必定是某物的另一个名字。

2. 任何返回一个引向局部变量的引用的函数都是错误的。(对于任何返回一个指向局部变量的指针的函数同样成立。)

2. 返回一个引向堆内存的引用,通过 new 分配的内存要通过调用一个适当的构造函数进行初始化,但是现在你有另一个问题:谁是删除你用 new 做出来的对象的合适人选?

3. 返回一个引向局部static的引用,这个会立即引起我们的线程安全(thread-safety)的混乱。

小节:本小节讲了不少内容,实际都是:绝不要返回一个局部栈对象的指针或引用,绝不要返回一个被分配的堆对象的引用,绝不要返回一个局部static 对象的指针或引用。

Item 22, 将数据成员声明为private

1. 使用函数(相对于变量)可以让你更加精确地控制成员的可存取性。

2. 将数据成员隐藏在功能性的接口(函数)之后能为各种实现提供弹性。例如,它可以在读或者写的时候很简单地通报其他对象,可以检验类的不变量以及函数的前置或后置条件,可以在多线程环境中执行同步任务,等等。

3. 关于封装的要点可能比它最初显现出来的更加重要。如果你对你的客户隐藏你的数据成员(也就是说,封装它们),你就能确保类的不变量总能被维持,因为只有成员函数能影响它们。

4. 假设我们有一个 public 数据成员,随后我们消除了它。有多少代码会被破坏呢?所有使用了它的客户代码,其数量通常大得难以置信。从而 public 数据成员就是完全未封装的。假设我们有一个 protected 数据成员,随后我们消除了它。现在有多少代码会被破坏呢?所有使用了它的派生类,典型情况下,代码的数量还是大得难以置信。从而 protected 数据成员就像 public 数据成员一样没有封装,因为在这两种情况下,如果数据成员发生变化,被破坏的客户代码的数量都大得难以置信。从封装的观点来看,实际只有两个访问层次:private(提供了封装)与所有例外(没有提供封装)。

小节:私有数据可以被自己自由改变,而不会影响客户端。

Item 23, 用非成员非友元函数取代成员函数

1. 面性对象原则指出:数据和对它们进行操作的函数应该被绑定到一起,而且建议成员函数是更好的选择。不幸的是,这个建议是不正确的。它产生于对面向对象是什么的一个误解。面向对象原则指出数据应该尽可能被封装。在很多方面非成员方法比一个成员函数更好。

2. 如果某物被封装,它被从视线中隐藏。越多的东西被封装,就越少有东西能看见它。越少有东西能看见它,我们改变它的弹性就越大,因为我们的改变仅仅直接影响那些能看见我们变了什么的东西。某物的封装性越强,那么我们改变它的能力就越强。这就是将封装的价值评价为第一的原因:它为我们提供一种改变事情的弹性,而仅仅影响有限的客户。结合一个对象考虑数据。越少有代码能看到数据(也就是说,访问它),数据封装性就越强,我们改变对象的数据的特性的自由也就越大,比如,数据成员的数量,它们的类型,等等。作为多少代码能看到一块数据的粗糙的尺度,我们可以计数能访问那块数据的函数的数量:越多函数能访问它,数据的封装性就越弱。

疑问:如果是纯面向对象编程(只有类,没有非成员函数),还需要这一小节吗?我承认非成员函数有更强的封装性。或许我还没有理解面向对象编程。

Item 24, 当类型转换应该用于所有参数时,声明为非成员函数。

1. 如果你需要在一个函数的所有参数(包括被 this 指针所指向的那个)上使用类型转换,这个函数必须是一个非成员。

2. 与成员函数相对的是非成员函数,而不是友元函数。太多的程序员假设如果一个函数与一个类有关而又不应该作为成员时(例如,因为所有的参数都需要类型转换),它应该作为友元。这个示例证明这样的推理是有缺陷的。无论何时,只有你能避免友元函数,你就避免它,因为,就像在现实生活中,朋友的麻烦通常多于他们的价值。

小节:

使用

const Rational operator*(const Rational& lhs,  const Rational& rhs)

{

return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());

}

代替

class Rational

{

...

const Rational operator*(const Rational& rhs) const{}

...

};

Item 25 考虑支持不抛异常的swap

小节:看的有点糊涂,标题是不抛异常的swap,我却很少看到如何保证不抛异常;或许是我没有读懂,对template也不太熟,在这里做个笔记。

时间: 2024-11-05 23:24:59

读书笔记4: <<Effective C++>>之设计与声明的相关文章

【读书笔记】《Linux内核设计与实现》内核同步介绍&内核同步方法

简要做个笔记,以备忘. 需同步的原因是,我们并发访问了共享资源.我们将访问或操作共享资源的代码段称"临界区",如果两个执行线程处于同一临界区中同时执行,称"竞争条件".这里术语执行线程指任何正在执行的代码实例,如一个在内核执行的进程.一个中断处理程序或一个内核线程. 举个简单例子,i++操作.该操作可以转换为下面的机器指令序列: 1.得到当前变量i的值,并保存到一个寄存器. 2.将寄存器的值加1. 3.将i的新值写回到内存中. 当两个线程同时进入这个临界区,若i初值

《游戏人工智能编程案例精粹》读书笔记&mdash;状态驱动智能体设计

一个有限状态机是一个设备,或是一个设备模型,具有有限数量的状态,它可以在任何给定的时间根据输入进行操作,使得从一个状态变换到另一个状态,或者是促使一个输出或者一种行为的发生.一个有限状态机在任何瞬间只能处于一种状态. 状态变换表 状态变换表是一个条件和那些条件导致的状态的表,这个表可以被智能体在规则的间隔内训问,使得它能基于从游戏环境中接收到刺激进行必须的状态转换. 内置的规则 每个状态模块依靠自身的逻辑来决定它是否应该运行自己变换到一个替代状态,智能体只向外部提供操作和获取自身属性的函数,状态

《Effective C++》设计与声明:条款18-条款19

软件设计是"让软件做你想让它做的事情"的步骤和做法.从一般性的构想开始,逐渐清晰,构造细节,最终设计出良好的接口(interface).这些接口而后变为C++的声明. 下面讲的是关于接口设计和声明的做法.设计接口一个很重要的准则是:让接口容易被正确使用,不容易被误用.这是一个大的准则,细化之后包括很多内容:正确性.高效性.封装性.维护性.延展性以及协议的一致性. 条款18:让接口容易被正确使用,不容易被误用 接口是客户和你的代码交换的唯一手段.如果客户正确使用你开发的接口,那自然很好:

JS读书笔记:《JavaScript框架设计》——第12章 异步处理

一.何为异步   执行任务的过程可以被分为发起和执行两个部分. 同步执行模式:任务发起后必须等待直到任务执行完成并返回结果后,才会执行下一个任务. 异步执行模式:任务发起后不等待任务执行完成,而是马上执行下一个任务,当任务执行完成时则会收到通知. 面对IO操作频繁的场景,异步执行模式可在同等的硬件资源条件下提供更大的并发处理能力,也就是更大的吞吐量. 但由于异步执行模式打破人们固有的思维方式,并且任务的发起和任务的执行是分离的,从而提高编程的复杂度. 多线程.多进程均可实现异步模式. 二.从回调

读书笔记-effective c++ Item 1

Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有"合适的"规则看上去都有例外.我们怎样理解这样一门语言? 最容易的方法是不要将其看成单一的一门语言而是将其看成是一个有相关性的语言的联邦.在一个特定的子语言中,一些规则就比较简单,明确并且容易记忆.当你从一个子语言切换到另外一个子语言时,这些规则可能会改变.为了更好的理解c++,你必须识别主要的子语言,幸运

[读书笔记]《Effective Java》第10章并发

第66条:同步访问共享的可变数据 同步的意义. 正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中. 进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果. Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double. 对于原子数据的读取,Java语言规范并不保证一个线程写入的值对于另一个线程将是可见的. 对于共享的数据,即使数据是原子可读写的,也要使用同步. 活动性失败:因为JVM的优化,部分代码无法执行. 1 /** 2

(读书笔记)Java应用架构设计-模块化模式与OSGi

本书主要模块化模式的好处.模块化方法与模式.OSGi简单使用等内容,分3大部分: 第一部分介绍了模块化概念,为什么要模块化,以及一些模块化要考虑的东西,如模块粒度,依赖关系,重用性灵活性等. 第二部分介绍模块化的一些模式,采用了GoF设计模式的格式(模式名称.模式表述.图示.描述.多种实现.效果.样例.小结),看着有些乱,但是收获不少. 第三部分介绍OGSi结合Java如何使用,以及如何模块化现有系统.Java中无法直接模块化(Java SE模块化功能Jigsaw被推迟到了Jave SE 9),

[读书笔记]《Effective Java》第9章异常

第57条:只针对异常的情况才使用异常 异常机制是用于不正常的情形,很少会有JVM实现试图对它们进行优化.在现代的JVM实现上,基于异常的模式比标准模式要慢得多. 把代码放在try-catch块中反而阻止了现代JVM实现本来可能执行的某些特定优化. 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常.如果类具有"状态相关"的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的"状态测试"方法,即指示是否可以调用这个状态相关的方

【读书笔记】《Linux内核设计与实现》进程管理与调度

大学跟老师做嵌入式项目,写过I2C的设备驱动,但对Linux内核的了解也仅限于此.Android系统许多导致root的漏洞都是内核中的,研究起来很有趣,但看相关的分析文章总感觉隔着一层窗户纸,不能完全理会.所以打算系统的学习一下Linux内核.买了两本书<Linux内核设计与实现(第3版)>和<深入理解Linux内核(第3版)> 0x00 一些废话 面向对象思想. Linux内核虽然是C和汇编语言写的,没有使用面向对象的语言,但里面却包含了大量面向对象的设计.比如可以把内核中的进程