《Effective C++》学习笔记二

目录

1. 若所有的参数皆需类型转换,请为此采用non-member函数
2. 写一个不抛异常的swap函数
3. 尽可能的延后变量定义出现的时间
4. 尽量少做转型动作
5. 避免返回handles指向对象内部成分
6. 为“异常安全”而努力是值得的
7. 透彻了解1inline的里里外外
8. 将文件间的编译依存关系降至最低
9. 确定你的public继承塑模出is-a关系
10. 避免遮挡继承而来的名称
11. 区分接口继承和实现继承
12. 考虑virtual函数以外的其他选择
13. 绝不重新定义继承而来的non-virtual函数
14. 绝不重新定义继承而来的缺省参数值
15. 通过composition或is-implemented-in-terms-of塑模出has-a
16. 明智而审慎地使用private继承
17. 明智而审慎地使用多重继承
18. 了解隐式接口和编译器多态
19. 了解typename的双重意义
20. 学习处理模板化基类内的名称
21. 将于参数无关的代码抽离templates
22. 运用成员函数模板接受所有兼容类型
23. 需要类型转换时请为模板定义非成员函数
24. 请使用traits classes表现类型信息
25. 认识template元编程
26. 了解new-handler的行为
27. 了解new和delete的合理替换时机
28. 编写new和delete是需固守常规
29. 写了placement new也要写placement delete
30. 不要轻忽编译器的警告
31. 熟悉包括TR1在内的标准程序库
32. 让自己熟悉Boost


1. 若所有的参数皆需类型转换,请为此采用non-member函数

假如operator*设计两个有理数的相乘

class Rational{
public:
  const Rational operator*(const Rational& rhs) const;
}
// 如下使用
Rational one(1,2);
Rational two(2,3);
Rational result = one * two; //ok
result = result * one; //ok
//
result = one.operator*(2); //ok,在non-explicit构造函数的情况下
result = 2.operator*(one); //error,甚至在non-explicit构造函数的情况下

  这里发生了隐式类型转换。只有当参数被列于parameter list内,这个参数才是隐式类型转换的合格参与者。
对于上述代码应该如下修改:

class Rational{  }  //不包括operator*,让它成为一个non-member函数
const Rational operator*(const Rational& lhs, const Rational& rhs){
  return Rational(...)
}
  • 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。

2. 写一个不抛异常的swap函数

  一般而言,重载function template没问题,但std是个特殊的命名空间,尽管可以全特化(total template specification)std内的templates,但不可以添加新的templates(classes或function等)到std里面。

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 若你提供一个member swap,也应该提供一个non-member swap用来调用前者。对于classes而非templates,也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何命名空间修饰。
  • 为用户定义类型进行std template全特化是好的,但千万不要尝试在st里面加入对std而言全新的东西。

3. 尽可能的延后变量定义出现时间

  只要你定义了一个带有构造函数和析构函数的变量,那么当程序控制流到达这个变量定义时,你得承受它的构造成本,析构成本也是如此,即便变量未被使用。
  以下两种循环迭代是赋值结构,哪种更好?

//1.定义在循环外
widget w;
for(int i=0;i<n;i++){
  w = 取决于i的某个值
}
//2.定义在循环内
for(int i=0,i<n;i++){
  widget w(取决于i的某个值);
}

方法1:一个构造函数 + 1个析构函数 + n个赋值操作
方法2:n个构造函数 + n个析构函数
除非你知道classes的一个赋值成本低于一组构造+析构成本时,方法1较好,尤其当n较大时。否则方法2较好。

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

4. 尽量少做转型动作

C风格的转型动作:

(T)expression  //将expression转型为T

函数风格的转型:

T(expression) //将expression转型为T

  以上两种形式并无差别,并且称为old-style casts。C++还提供四种C++ style casts:

  1. const_cast(expression) //用来将对象常量性移除
  2. dynamic_cast(expression) //用来执行“安全向下转型”,即决定某个对象是否归属继承体系中的某个类型
  3. reinterpret_cast(expression) //意图执行低级转型,实际结果取决于编译器,表示它不可移植。
  4. static_cast(expression) //用来强迫隐式转换,例如将non-const转为const,int转为double等,也可以执行反向转换。但无法将const转换为non-const-这只有const_cast能做到。
  • 如果可以尽量少做转型,特别是在注重效率的代码中避免dynamic_casts。如果需要转型,试着寻找代替设计。
  • 如果转型是必要的,请将它隐藏在某个函数背后。
  • 使用C++ style转型,不要使用旧式转型。

5. 避免返回handles指向对象内部成分

  • 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生dangling handles的可能性降至最低。

6. 为“异常安全”而努力是值得的

当异常被抛出时,带有异常安全的函数会:

  1. 不泄露任何资源
  2. 不允许数据被破坏

异常安全函数提供以下三个保证:

  1. 基本承诺:如果抛出异常程序内的任何事物任然保持在有效状态下。
  2. 强烈保证:如果异常抛出,程序状态不改变。
  3. 不抛掷保证:承诺绝不抛异常,因为它们总是能完成它们原先承诺的功能。
  • 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构被破坏。
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于所调用各个函数的“异常安全保证”中最弱者。

7. 透彻了解1inline的里里外外

  inline某个函数能免除函数调用成本,编译器也能对它执行语境相关的优化,大部分编译器不会对一个outlined函数调用动作执行此优化。但是过度依赖inline会导致程序体积太大,造成代码膨胀会导致额外的paging,降低高速缓存的命中率。以及伴随而来的效率损失。
  template的具现化与inline无关,如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline。

  • 将大多数inlining限制在小型、被频繁调用的函数身上,可使代码膨胀问题最小化,程序速度提升最大化。
  • 不要只因为function templates出现在头文件,就将他们声明为inline。

8. 将文件间的编译依存关系降至最低

  以声明的依存性替换定义的依存性,那正是编译依存最小化的本质:现实中让头文件尽可能的自我满足,万一做不到,则让它与其他文件内的声明式相依。如果使用object references或object pointer可以完成任务就不要使用objects。如果能够尽量以class声明式替换class定义式。
  C++也提供关键字export允许将template声明式和template定义式分离于不同的文件内,但是支持这个关键字的编译器目前较少。handle classes和interface classes解除了接口和实现之间的耦合关系,从而降低文件之间的编译依存性,但不论handle classes还是interface classes一旦脱离inline都无法有太大作为。

  • 支持“编译依存最小化”的一般思想是:依存于声明式,不依存于定义式,基于此的两个手段是handle classes和interface classes。
  • 程序库头文件应该以完全且仅有声明式的形式存在,不论是否涉及templates都适用。

9. 确定你的public继承塑模出is-a关系

  is-a并非唯一存在于classes之间的关系。另外两个关系是has-a和is-implementated-in-terms-of。

  • public继承意味着is-a,适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

10. 避免遮挡继承而来的名称

class Base{
public:
  virtual void mf1() = 0;
  virtual void mf1(int);
  virtual void mf2();
  void mf3();
  void mf3(double);
}
class Derived:public Base{
public:
  virtual void mf1();
  void mf3();
}
//使用时
Derived d;
d.mf1(); //ok
d.mf1(x); //error
d.mf2(); //ok
d.mf3(); //ok
d.mf3(x); //error

解决问题,你可以使用using声明式修改:

class Derived:public Base{
public:
  using Base::mf1; //让Base内的mf1 mf3
  using Base::mf3; //在Derived中可见
  virtual void mf1();
  void mf3();
}
//forwarding function
class Derived:private Base{
public:
  virtual void mf1(){ //转交函数,自动成为inline
    Base::mf1();
  }
}

  有时并不想继承base classes的所有函数可使用一个forwarding function。inline forwarding function的另一个用途是为那些不支持using声明式的老旧编译器另辟一条道路,将继承而得的名称汇入derived class作用域内。

  • derived classes内的名称会遮掩base classes内的名称,public继承下没人希望如此。
  • 为了名称不被遮掩,可使用using声明式或转交函数。

11. 区分接口继承和实现继承

  成员函数的接口总会被继承;声明一个pure virtual函数的目的是为了让derived classes只继承函数接口;声明impure virtual函数的目的是让derived classes继承被该函数的接口和缺省实现;声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

  • 接口继承和实现继承不同,在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

12. 考虑virtual函数以外的其他选择

virtual函数的替代方案:

  1. 使用non-virtual interface(NVI)手法,那是template method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。
  2. 将virtual函数替换为函数指针成员变量,这是strategy设计模式的一种分解表现形式。
  3. 以tr1::function成员变量替换virtual函数。
  4. 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是strategy设计模式的传统实现手法。
  • virtual函数的替代方案包括NVI手法以及strategy设计模式的多种形式。NVI手法自身是一个特殊形式的template method设计模式。
  • 将机能从成员函数移到class外部,带来的一个缺点是非成员函数无法访问class的non-public成员。
  • tr1::function对象的行为就像一般函数指针,这样的对象可接纳与给定的目标签名式兼容的所有可调用物。

13. 绝不重新定义继承而来的non-virtual函数

  为多态基准类声明virtual析构函数解释了为什么多态性base class内的析构函数应该是virtual,如果你违反了它你也就违反了本条款,因为derived classes绝不该重新定义一个继承而来的non-vvirtual函数(此处指的是base class析构函数)。

14. 绝不重新定义继承而来的缺省参数值

  virtual函数是动态绑定(dynamically bound)。即后期绑定,而缺省参数值是静态绑定(statically bound),即前期绑定。对象的静态类型就是它在程序中被声明时采用的类型,对象的动态类型是指目前所指对象的类型。

  • 绝不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-你唯一应该覆写的东西却是动态绑定。

15. 通过composition或is-implemented-in-terms-of塑模出has-a

  复合(composition)是类型之间的一种关系,当某种类型的对象内包含它种类型的对象就是这种关系,例如:

class person{
private:
  std::tring name;
  Address address;
  ...
}

  public继承是is-a的意义,复合意味着has-a或is-implemented-in-terms-of,当复合发生在应用域内的对象之间表现出has-a的关系;当它发生在实现域内则是表现is-implemented-in-terms-of的关系。

16. 明智而审慎地使用private继承

  private继承意味着is-implemented-in-terms-of,和复合的意义相同,如何在两者之间取舍?答案是:尽可能使用复合,必要时才使用private继承(当protected成员或virtual函数1牵扯进来时)。

  • private继承意味着is-implemented-in-terms-of,它通常比复合的级别更低,但当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计才是合理的。
  • 和复合不同,private继承可以造成empty base最优化。

17. 明智而审慎地使用多重继承

  多重继承是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致要命的“钻石型多重继承”。使用virtual base classes(相当于virtual继承)时非必要不使用virtual bases,平时使用non-virtual继承;如果必须使用virtual base classes,尽可能避免在其中放置数据。

  • 多重继承比单一继承复杂,它可能新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化和复制的复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  • 多重继承一个情节涉及public继承某个interface class和private继承某个协助实现的class的两种组合。

18. 了解隐式接口和编译器多态

  • classes和templates都支持接口和多态。
  • 对classes而言接口是显式的,以函数签名为中心,多态则是通过virtual函数发生于运行期。
  • 对template参数而言,接口是隐式的,基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

19. 了解typename的双重意义

以下template声明式中,class和typename有什么不同?

template<class T>class widget;
template<typename T>class widget;

  答案:没有不同,当我们声明template类型参数,class和typename的意义完全相同,然而C++并不总是把class和typename视为等价,有时一定得使用typename。任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字typename。typename不可以出现在base classes list内的嵌套从属类型名称前,也不可以在member initialization list中作为base class修饰符。

  • 声明template参数时,前缀关键字class和typename可互换。
  • 使用关键字typename标识嵌套从属类型名称,但不得在base class lists或member initialization list内以它作为base class修饰符。

20. 学习处理模板化基类内的名称

  • 可在derived class templates内通过this->指涉base class templates内的成员名称,或由一个明白写出的base class资格修饰符完成。

21. 将于参数无关的代码抽离templates

  • templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

22. 运用成员函数模板接受所有兼容类型

  所谓智能指针(smart pointer)是行为像指针的对象,并提供指针没有的机能,例如std::auto__tr和tr1::shared_ptr如何能够被用来在正确时机自动删除heap-based资源。STL容器的迭代器几乎总是智能指针。
  member function templates的作用不限于构造函数,他们通常也支持赋值操作。如果程序需要一个copy构造函数,但没有声明它,编译器会为你生成一个,在class内声明泛化copy构造函数并不会阻止编译器生成他们自己的copy构造函数。

  • 请使用member function templates生成可接受所有兼容类型的函数。
  • 如果你声明member template用于泛化copy构造或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符。

23. 需要类型转换时请为模板定义非成员函数

  对于之前说过的唯有non-member函数才有能力在所有实参身上实施隐式类型转换。此处以Rational class的operator*函数为例:

template<typename T>
class Rational{
public:
  Rational(const T& num=0, const T& deno=1); //passed by reference
  const T num() const;
  const T deno() const;
}
template<typename T>
const Rational<T>operator*(const Rational<T>& lhs, const Rational<T>& rhs){ ... }
//使用
Rational<int> one(1,2); //ok
Rational<int> r = one * 2; //error

修改如下:

...
//将operator* 函数本体合并至其声明中
friend const Rational operator*(const Rational& lhs, const Rational& rhs){
  return Rational( ... );
}
  • 当我们编写一个class template,而它所提供的与此template相关的函数支持所有参数的隐式类型转换时,请将那些函数定义为class template内部的friend函数。

24. 请使用traits classes表现类型信息

  STL主要由用以表现容器、迭代器和算法的templates构成,但也覆盖若干工具性templates。STL共有5种迭代器分类,对应于他们支持的操作:input迭代器、output迭代器、forward迭代器、bidirectional迭代器、random access迭代器。
  traits允许让你在编译期间取得某些类型信息。traits广泛用于标准程序库,但它并不是C++关键字或一个预先定义好的构件,它是一种技术,也是一个C++程序员共同遵守的协议。他要求之一是,它对内置类型和用户自定义类型的表现必须一样好(traits能够施行于内置类型如纸张身上)。意味着“类型内的嵌套信息”这种东西出局了,因为无法将信息嵌套于原始指针内,因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一个或多个特化版本中。

  • traits classes使得“类型相关信息”在编译期可用。他们以templates和templates特化完成实现。
  • 整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if…else测试。

25. 认识template元编程

  所谓template metaprogram(模板元程序)是以C++写成、执行于C++编译期内的程序。TMP主要是个functional language,他有两大作用:1.它让某些事物更容易,如果没有它,那些事物将非常困难甚至不可能。2.由于template metaprogram执行于C++编译期。因此可将工作从运行期转移到编译期。

  • template metaprograming可将工作由运行期迁移到编译期,可实现早期错误侦探和更高的执行效率。
  • TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成某些特殊类型不适合的代码。

26. 了解new-handler的行为

  operator new和operator delete只适用于分配单一对象,arrays所用的内存由operator new[]来分配,由operator delete[]归还。STL容器所使用的heap内存是由容器所拥有的分配器对象(allocator objects)管理,不是被new和delete直接管理。
当operator new抛出异常以反映一个未满足的内存需求之前,他会先调用一个客户指定的错误处理函数,一个所谓的new-handler(这其实并非全部事实,operator new做的事情更复杂),为了指定这个用以处理内存不足的函数,客户必须调用set_new_handler,那是声明于的标准库函数:

namespace std{
  typedef void (*new_handler*)();
  new_handler set_new_handler(new_handler p) throw();
}

一个设计良好的new-handler函数必须做以下事情:
1.让更多内存可被使用。
2.安装另一个new-handler。
3.卸除new-handler。
4.抛出bad_alloc(或派生自bad_alloc)的异常。
5.不返回,通常调用abort或exit。

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • nothrow new是一个很局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常。

27. 了解new和delete的合理替换时机

  对某些应用程序而言,将旧有的new和delete替换为定制版本是获得重大效能提升的办法之一。何时可在全局性的或class专属的基础上合理替换缺省的new和delete?
1.为了检测运用错误。
2.为了收集动态分配内存的使用统计信息。
3.为了增加内存分配和归还的速度。
4.为了降低缺省内存管理器带来的空间额外开销。
5.为了弥补缺省分配器中的非最佳齐位。
6.为了将相关对象成簇集中。
7.为了获得非传统的行为。

  • 有很多理由需要写个自定义的new和delete,包括改善性能。对heap运用错误进行调试、收集heap使用信息。

28. 编写new和delete是需固守常规

  • operator new应该内含一个无限循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。他也应该有能力处理0 bytes申请。class专属版本则还应该处理比正确大小更大的申请。
  • operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理比正确大小更大的申请。

29. 写了placement new也要写placement delete

  • 当你写一个placement operator new,请确定也写出了对应的placement operator delete。
  • 当你声明placement new和placement delete请确定不要无意识地遮掩了它们的正常版本。

30. 不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息,努力在你的编译器的最严苛警告级别下争取无任何警告。
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事物不同。一旦移植到另一个编译器上,你原来依赖的警告信息可能消失。

31. 熟悉包括TR1在内的标准程序库

C++98列入的标准程序库有:

  1. STL(standard template library),覆盖容器(containers如vector,string,map)、迭代器(iterators)、算法、函数对象、各种容器适配器和函数对象适配器。
  2. iostreams,覆盖用户自定义缓冲功能、国际化I/O以及预先定义好的对象cin、cout、cerr、clog。
  3. 国际化支持、数值处理、异常阶层体系、C89标准程序库。
    TR1新组件都放在std命名空间内,更正确的说是嵌套命名空间tr1内,例如shared_ptr的全名是std::tr1::shared_ptr。
    1). 智能指针,tr1::shared_ptr和tr1::weak_ptr。
    2). tr1::function。
    3). tr1::bind,它能做STL绑定器bind1st和bind2nd所做的每一件事,tr1::bind可以和const及non-const成员函数协同运作。
    4). hash tables,用来实现sets,multisets,maps和multi-maps。
    5). 正则表达式。
    6). tuples(变量组)这是标准程序库中的pair template的新一代制品。
    7). tr1::array,本质是个STL化数组,即一个支持成员函数如begin和end的数组,不过tr1::array的大小固定,并不使用动态内存。
    8). tr1::mem_fn,这是个语句构造上与成员函数指针一致的东西。
    9). tr1::reference_wrapper,一个让references的行为更像对象的设施。
    10). 随机数生产工具。
    11). 数学特殊函数。
    12). C99兼容扩充。
    13). type traits,一组traits classes用以提供类型的编译期信息。
    14). tr1::result_of,这是个template,用来推导函数调用的返回类型。
  • C++标准程序库的主要机能由STL,iostreams,locales组成。
  • TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外10个组件支持。
  • TR1自身只是一份规范。

32. 让自己熟悉Boost

References:

  • 《Effective C++》改善程序与设计的55个具体做法,第三版
原创文章 38 获赞 13 访问量 4037

猜你喜欢

转载自blog.csdn.net/qq_36287943/article/details/103838285