《Effective C++》学习总结(条款21- 30)

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

1.在任何函数中返回一个reference(或指针)指向某个local对象,都没有任何意义——因为其即将被销毁
2.不要返回pointer或reference指向一个heap对象(用户不知道如何delete)
3.不要返回pointer或者reference指向local static对象而有可能需要多个这样的对象(同一行不能调用多次该函数,static只有一份)
4.当你必须在“返回一个reference”和“返回一个object”之间抉择,你需要做的是根据情况挑选一个最为正确的,尽可能降低成本
5.请记住:
  • 绝不要返回pointer或reference指向一个local stack对象——因为其被销毁后就没有意义
  • 绝不要返回reference指向一个heap-allocated对象——因为不知道如何delete
  • 绝不要返回pointer或reference指向一个local static对象而又可能同时需要多个这样的对象
  • 条款04已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例

 

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

1.将成员变量隐蔽在函数接口的背后,可以为“所有可能的实现”提供弹性与可能——狭义的理解即为声明为private
2.使用public意味着不封装,而几乎可以说——不封装意味着不可改变(多态)
3.protected并不比public更有封装性——所谓的“封装性”与“当其内容改变时可能造成的代码破坏量”成反比
4.总而言之,从封装性的角度来看,只有两种访问权限:
  • private——提供封装
  • 其他——不提供封装
5.请记住:
  • 切记将成员变量声明为private,再通过成员函数调用获取他们(进行读写访问)。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
  • protected并不比public更有封装性

 

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

1.面向对象守则要求数据尽可能被封装,然而与直观相反地,member函数带来的封装性比non-member函数低
2.为什么封装性这么重要——它使我们能够改变事物而至影响有限客户
3.越多的东西被封装,其可见性越差(越少的人可以看见它),而越少的人看见它,我们就有越大的弹性去改变它。
4.如果要在non-member、non-friend与member函数之间做抉择,而且两者之间提供相同机能,那么,导致较大封装性的是non-member、non-friend函数,因为它并不增加“能够访问class内private成员”的函数数量——这也就意味着选择它会使class有较好的封装性。
5.将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,降低了编译依存性,这正是STL的做法。
6.请记住:
  • 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibilitiy)和机能扩充性

 

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

1.只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。
2.this对象(隐喻参数)绝不是隐式类型转换的合格参与者。
3.member函数的反面是non-member函数,而不是friend函数——这意味着不能够只因函数不该成为member,就自动让它成为friend。
4.无论何时如果你可以避免friend函数,就应该避免,因为其带来的麻烦往往多过价值。
5.请记住:
  • 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

 

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

1.当std::swap自定义类型效率不高时(例如深拷贝时成员较多影响交换效率的时候),提供一个特化版本的swap成员函数,并确定不会抛出异常
2.C++只允许对class templates偏特化(partially specialize),在function templates(如std::swap)身上偏特化是不行的
  • 如P108中所举的例子,void swap<Widget>(Widget& a, Widget& b)是对一个class template进行偏特化,编译通过
  • void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)是对一个function template偏特化,这是被禁止的
3.当用户打算偏特化一个function template时,惯常做法是简单地为它添加一个重载模版
  • 为了不影响std命名空间的管理规则,需要声明一个non-member swap函数于一个自己定义的命名空间中,并令其调用该命名空间内的class 中的public 的swap成员函数。
4.如果你调用swap函数,请确定包含一个using声明式,以便让std::swap在你的函数内部可见,然后不需要加上任何namespace修饰符,直接调用swap
  • 这样做的目的是让编译器选择适合调用参数版本的swap函数(实参取值之查找规则——argument-dependent lookup)
5.成员版swap绝不可抛出异常(条款29)
  • 一般来说对于内置类型的操作是不会抛出异常的
6.请记住:
  • 当std::swap 对你的类型效率不高时,提供一个swap函数,并确定这个函数不抛出异常
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
  • 为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西
补充:->.的区别:
  • 二者均为成员变量符,用于访问类的成员
  • ->主要用于类类型的指针访问的成员,
  • .运算符,主要用于类类型的对象访问类的成员。
class A
{
public:
	int a = 0;
};
int main()
{
	A b;
	A *p = &b;
	b.a; //类类型的对象访问类的成员
	p->a; //类类型的指针访问类的成员
}
补充2:this指针
  • 先要理解class的意思。class应该理解为一种类型,象int,char一样,是用户自定义的类型。用这个类型可以来声明一个变量,比如int x, myclass my等等。这样就像变量x具有int类型一样,变量my具有myclass类型。理解了这个,就好解释this了,my里的this 就是指向my的指针。如果还有一个变量myclass mz,mz的this就是指向mz的指针。 这样就很容易理解this 的类型应该是myclass * `(一个指向class类型的指针),而对其的解引用,其中*this就应该是一个myclass类型的变量。

  • 通常在class定义时要用到类型变量自身时,因为这时候还不知道变量名(为了通用也不可能固定实际的变量名),就用this这样的指针来使用变量自身。

  • this指针的用处: 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。

  • 也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。例如,调用date.SetMonth(9) <===> SetMonth(&date, 9),this帮助完成了这一转换 .

  • 在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看成this的隐式使用。

  • this总是指向这个对象,所以this是一个常量指针,我们不允许改变this中保存的地址

  • this指针的使用: 一如条款10中描述的,在类的非静态成员函数中返回类对象本身的时候,直接使用return *this

  • 另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)

  • 关于this指针的一个经典回答: 当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this是一个指针,它时时刻刻指向你这个实例本身。

 

第五章:实现——Implementations

条款26:尽可能延后变量定义式的出现时间

1.为了减少成本,避免定义一个你没有使用的变量——延后变量定义的时间,直到你确实需要它
2.更进一步的来说,我们应该尝试尽量延后这份定义知道能够给它初值实参为止
  • 这样不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为
  • 更深一层的说,以“具有明显意义的初值”将变量初始化,还可以附带说明变量的目的
3.对于循环体,定义变量有两种方式——循环外或循环内
  • 对于循环外:1个构造函数 + 1个析构函数 + n个赋值操作

  • 对于循环内:n个构造函数 + n个析构函数

  • 总结: 如果classes的一个赋值成本低于一组构造+析构成本,循环外是更高效的选择——尤其是当n值很大时。否则的话,循环内更好。

    此外,二者的变量作用域不同,应该根据实际情况选取你觉得合适的变量定义位置

  • 除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感(performence-sensitive)的部分,否则你应该使用定义在循环内的做法

4.请记住:
  • 尽可能延后变量定义式的出现。这样做可以增加程序的清晰度并改善程序效率

 

条款27:尽量少做转换动作

1.C++规则的设计目标之一是,保证”类型错误“绝不可能发生
2.casting的形式有如下几种:
  • 旧式转型:分为C-style转型和函数风格转型
    • C-style转型:(T)expression //将expression转型为T
    • 函数风格转型:T(expression) //将expression转型为T
  • 新式转型:是C++风格的新casting方式
    • const_cast<T>(expression):通常用来将对象的常量性消除;
    • dynamic_cast<T>(expression):主要用来执行“安全向下转型”,决定某对象是否归属继承体系中的某个类型。static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL ,而dynamic_cast转换失败会返回NULL;对于上行转换,dynamic_cast和static_cast是一样的;
    • reintepret_cast<T>(expression):在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针,不能将非32bit的实例转成指针。最普通的用途就是在函数指针类型之间进行转换,不可移植;
    • static_cast<T>(expression):适用范围最广的,适用于很多隐式转换,基本数据类型的转换,基类指针与子类指针的相互转换,或者添加const属性,任何类型转换为void类型;
3.新式转型较受欢迎的原因:
  • 容易辨识,在debug过程中易被找到
  • 转型动作的目标很窄,特化性很强,编译器容易在代码的书写阶段就指出一些根本性的错误
4.唯一使用旧式转型的时机:当我要调用一个explicit构造函数将一个对象传递给一个函数时
5.如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast;试着发展无需转型的替代设计
6.在C++中,但一对象(例如一个类型为derived的对象)可能拥有一个以上的地址(例如,“以base *”指向它时的地址和“以derived *”指向它时的地址是不同的,这就是多态实现的基础)
7.如果你想要在derived class中调用base class的成员函数,并不需要将this指针转换为base class对应的类型再去调用,这样写就能有效解决问题:
  • Window::oneResize(); //调用Window::oneResize作用于*this身上
8.对使用dynamic_cast的一些忠告:
  • 使用的时机:通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象;
  • 两个一般性做法来避免上述问题:
    • 使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要;
    • 在base class内提供virtual函数做你想对各个派生类做的事情,如此便能通过base class接口处理”所有可能之各种派生类“
9.如果转型是必要的,试着将它隐藏于某个函数后
  • 函数的接口会保护调用者不受函数内部任何动作的影响
10.请记住:
  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast,如果有个设计动作需要转型动作,试着发展无需转型的替代设计
  • 如果转型是必要的,试着将它隐蔽于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内
  • 宁可使用C++风格的新式转型,也不要使用旧式转型。前者更易辨识,而且也比较有分门别类的职能

 

条款28:避免返回handles指向对象内部成分

1.返回一个“代表对象内部数据”的handle(号码牌,用来取得某个对象),随之而来的便是“降低对象封装性”的风险
  • 同时,它可能导致“虽然调用const成员函数却造成对象状态被更改”的情况
2.核心的问题在于:你绝对不应该令成员函数返回一个指针指向“访问级别较低(public > private)”的成员函数。
  • 如果你打算这么做,那么后者的实际访问级别就会提高如同前者,因为客户可以取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它
3.返回一个handle代表对象内部成分在大多数时候是危险的,容易发生dangling handles(空悬的号码牌)的情况——这种handles所指的对象不存在(超出scope被销毁)
4.请记住:
  • 避免返回hadles(包括references、指针、迭代器)指向对象内部。遵守这个条款可添加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

 

条款29:为“异常安全”而努力是值得的

1.异常安全性——当异常被抛出时,带有异常安全性的函数会:
  • 不泄漏任何资源——建立一个lock class,将析构函数中放入unlock,确保互斥器被及时释放(见条款14)
  • 不允许数据败坏
2.“异常安全函数”即使发生异常也不会有泄漏资源或允许任何数据结构败坏,提供以下三个保证之一:
  • 基本承诺: 抛出,程序内的任何事物仍然保持在有效状态下

  • 强烈保证: 异常抛出,程序状态不改变,回复到调用函数之前的状态(往往能够以copy-and-swap实现出来)

  • 不抛掷保证(nothrow): 抛出异常(如内置类型)

  • 对大部分函数来说,抉择往往落在基本保证强烈保证之间

3.所谓的强烈保证中的copy-and-swap策略是指:
  • 为你打算修改的对象作出一份副本,然后在那副本身上做一切必要的修改。若有任何修改动作抛出异常,愿对象仍保持最初的未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)
4.当你撰写新码或修改旧码时,请仔细想想如何让它具备异常安全性。
  • 首先是“以对象管理资源”(条款13),那可以有效阻止资源泄漏;
  • 然后是挑选三个“异常安全保证”中的某一个实施于你所写的每一个函数身上——函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者(木桶效应)
  • 你应该挑选“现实可施作”条件下的最强烈保证等级——可能的话提供“nothrow保证”,当“强烈保证”不切实际时,就必须提供“基本保证”
  • 将你的决定写成文档,一来是为了你的函数用户着想,二来是为将来的维护者着想
  • 函数的“异常安全性保证”是其可见接口的一部分,所以要慎重选择,就像选择函数接口的其他任何部分一样
5.请记住:
  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

 

条款30:透彻了解inlining的里里外外

1.inline函数背后的整体观念
  • 将“对此函数的每一个调用”都以函数本体替换之——好处在于对于一些轻量的,需要反复使用的代码块(如数学计算),使用inline能够大大提升调用成本,编译器也因此有能力对它执行语境相关的最优化;
  • 使用inline的坏处也同样明显:这样做可能增加你的目标吗大小,造成程序体积变大,带来效率上的损失
  • inline只是对编译器的一个申请,而不是强制命令,编译器会根据实际情况来进行判断是否要将代码inline
2.inline函数通常一定被置于头文件内——为了让编译器及时“看见”inline函数,因为大多数建置环境(build environments)在编译阶段进行inlining
3.构造函数和析构函数通常不是好的inline对象
4.随着程序库的升级,inline函数需要重新编译,而non-inline函数值需要重新连接
5.核心思想是:一开始不要将任何函数声明为inline,或至少将inlining施行范围局限在那些”一定成为inline“(条款46)或”十分平淡无奇“的函数上。如果需要对写好的整体框架进行优化,再进一步分析是否需要对哪一部分进行inlining
6.请记住:
  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化
  • 不要只因为function templates出现在头文件,就将它们声明为inline
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值