Effective C++学习笔记

Posted Karthus_冲冲冲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective C++学习笔记相关的知识,希望对你有一定的参考价值。

目录

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

    1. 尽可能延后变量定义式的出现时间:有时调用的函数中可能会发生异常并向上抛出或者提前返回。如果该函数把函数中将所有可能用到的变量均在最前面进行声明/初始化,那么异常抛出/提前返回后,处于异常/返回代码后面的第一次使用部分变量的代码将会执行不到。此时,函数需要为一些未曾使用到变量进行构造和析构,降低了效率。同时,定义和使用变量不在彼此附近的位置,影响代码阅读性。
    1. 定义循环中的变量有两种情况:最终选择A or B要看对象w的“构造+析构”成本是否小于“赋值成本”。

条款27. 尽量少做转型动作

    1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计
    1. 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们的代码里。
    1. 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易识别出来,而且也比较有着分门别类的职掌。
    1. 转型需要注意的几点情况:
      (1)一个base class 指针指问一个derved class对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量(offset),在运行期被施行于Derived * 指针身上,用以取得正确的 Base *指针值。
      (2)应避免对指针类型转型后再进行算术操作,这种行为无定义
      (3)对于多态情况中基类与派生类指针使用,当调用函数为虚函数时,到底调用的是基类对象的函数还是派生类对象的函数,与指向对象的虚函数指针有关。
    1. 新式转型内容:
      (1)const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C+±style转型操作符。
      (2)dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作
      (3)reinterpret_cast意图执行低级转型,例如将一个pointer to int转型为一个int。实际动作可能取决于编译器,这也就表示它不可移植
      (4)static_cast用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const一这个只有const_cast才办得到。

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

    1. 避免返回handles(包括指针,引用,迭代器)指向对象内部部分,这样可以增加分装性。
      (1)成员变量的封装性最多只等于“返回其handles”的函数的访问级别。函数调用者可以轻松修改对象数据。
      (2)如果const成员函数传出去一个本类对象handles,违背了const成员函数应有的不修改对象数据的性质。
      (3)绝不应该使得一个成员函数返回一个指向“访问级别较低”的成员函数指针(少见)。
      解决方法:可以在返回值上加上const属性,使得调用者只许读,不许改
      (4)空悬的号码牌(dangling handles):handles指向东西不存在。下例中函数返回一个temp临时对象,调用语句结束后,temp会被析构。

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

    1. 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型
      (1)基本型:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
      (2)强烈型:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到“调用函数之前”的状态
      (3)不抛异常型:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。
    1. “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义(下述第3点)。
      原因:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
    1. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。如果系统(函数)内有一个函数不具备异常安全性,那么整个系统(函数)就不具备异常安全性。

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

    1. 将大多数inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
    1. 不要只因为function templates 出现在头文件,就将它们声明为inline。
      注意:
      (1)inline只是对编译器的一个申请,不是强制命令.隐喻内联如下;

(2)inline函数和函数模板通常一定被置于头文件内,因为在编译过程中,需要将函数调用替换成内联函数体/实例化函数模板,编译器需要知道它们长什么样;
(3)编译器通常不对“通过函数指针而进行的函数调用”实施内联,因为编译器没有能力为内联函数生成一个outlined函数本体,无法取得函数地址;

(4)构造函数和析构函数往往是inlining的糟糕候选人,因为两者会自动调用父类的构造和析构。

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

    1. 支持“编译依存性最小化”的一-般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和 Interface classes。
      (1)Handle classes:也常被称为pimpl idiom(pointer 同implementation),建立一个管理类,其中有一个智能指针指向被管理类对象(底层操作对象),将客户与被管理类对象分离。当被管理类发生迭代修改后,客户端无需重新编译自己的代码,只需拷入新实现类的编译好的文件。
      (2)Interface classes:制作一个抽象基类作为接口类,虚函数作为函数接口,其余实现类继承该接口类。客户端只需使用接口类指针(基类指针)指向继承类对象即可实现多态功能。只要接口函数不变,客户端就无需重新编译。
    1. 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates 都适用。实现接口与实现分离

effective c++学习笔记

目录

导读

术语

  • 声明式(declaration)是告诉编译器某个东西的名称和类型(type),但略去细节
  • size_ 只是一个typedef

让自己习惯c++

条款1:将c++视为一个语言联邦

c++是多重范型编程语言,视c++包括4种次语言: 1:C; 2:Object-Oreinted C++;3:Template C++;4:STL(template程序库,包括容器、迭代器、算法和函数对象)。

条款2:尽量以const和inline取代#define

const:

  • #define直接替换导致名称从未被编译器看到
  • const定义常量也可能比#define导致较小量的码
  • #define不重视作用域,故不提供封装性

enum:

  • 取一个const的地址是合法的,但取一个enum的地址不合法

inline:

  • #define定义函数可能招致误用,最好用inline函数替换

注:对于单纯常量,最好以const对象或enums替换#defines;对于形似函数的宏,最好改用inline函数替换#defines。

条款3:尽可能使用const

令函数返回一个常量值,可以预防无意义的赋值动作(例:p19)
const成员函数:

1.const对象只能访问const成员函数,而非const对象可以访问任意的成员函数
2.const成员函数不能修改对象的数据成员,const对象的成员变量不可以修改(mutable修饰的数据成员除外)

注:
两个成员函数如果只是常量性不同,是可以被重载的
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)

条款4:确认对象被使用前已先被初始化

  • 为内置型对象进行手工初始化;内置类型以外,构造函数负责初始化责任
  • 构造函数最好使用成员初值列 ,而不使用赋值操作 ;最好总是以声明次序为其次序(参数列表初始化避免了赋初值再修改的过程,效率更高)
  • 不同编译单元的non-local static对象初始化相对次序并无明确定义,以local对象替换得以免除问题,例:

构造/析构/复制运算

条款5:了解c++默默编写并调用哪些函数

如果自己不声明, 编译器就会暗自为class创建一个default构造函数、一个copy构造函数、一个copy assignment操作符(代码合法有意义时编译器才会生成),以及一个析构函数

  • base class如果把copy构造函数或copy assignment操作符设置为private,derived class将拒绝生成copy构造函数或copy assignment操作符

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

  • 防止编译器自动生成默认构造函数,可将相应的成员函数声明为private并且不予实现。使用Uncopyable这样的base class也是一种可行方案

条款7:为多态基类声明virtual析构函数

  • 多态性质的base classes应该申明一个virtual析构函数。如果class带有任何virturl函数,他就应该拥有virtual的析构函数
  • 如果classes设计目的不是作为base classes使用(不是为了具备多态性)比如string等,就不该声明virtual析构函数

条款8:别让异常逃离析构函数

  • 析构函数绝对不能吐出异常,如果一个被析构函数调用的的函数可能抛出异常,必须捕捉任何异常,吞下他们并结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常进行反应,那么class应该提供一个普通函数(不在析构函数中执行)执行

条款9:绝不再构造和析构函数钟调用virtual函数

在构造和析构期间不要调用virtual函数,因为这类调用不下降到派生类

条例10:令operator= 返回一个reference to *this

  • 令赋值操作符返回一个referenc to *this (返回自身引用)

条款11:在operator=中处理自我赋值

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”的地址,语句顺序
  • 确定任何函数如果操作一个以上的对象,其中多个对象是同一个对象时其行为仍然正确

条款12:赋值对象时勿忘每一个成分

  • 当编写一个copying函数,确保复制所有local成员变量,确保调用所有base classes内的适当的copying函数
PriorittyCustomer::PriorittyCustomer(const PriorittyCustomer&rhs):Customer(rhs),priority(rhs.priority)
//使用派生类的copy函数掉用相应基类的函数

PriorittyCustomer& PriorittyCustomer::operator=(const PriorityCustomer&rhs)
Customer::operator=(rhs);//对基类成分进行赋值动作
priority =ths.priority;
return *this;
  • 不能使用copy assignment操作符调用 copy构造函数,同样copy构造调用copy assignment毫无作用,assignment操作符只实施在已经初始化的对象上。取消上述拷贝赋值和拷贝赋值相似的行为的方法是建立一个新的成员函数给两者调用。

资源管理

条款13:以对象资源管理资源

  • 用智能指针来避免函数提前退出导致的内存泄露

  • 不能让多个auto_ptr指向同一对象,不然可能导致指向的同一对象被删除两次

  • 为了防止内存泄漏,应该使用RAII对象,他们在构造函数重获得资源并在析构函数中释放资源,两个常用的RAII classes分别是 shared_ptr和auto_ptr。

条款14 :资源关联类中小心copying行为

  • RAII对象很多时候被复制并不合理,可以选择讲copying操作赋值为private来禁止复制
  • 对底层资源使用“引用计数法”。类似shared_ptr
  • 复制底部,使用深拷贝,对象内含的指针及指针指向的区域都制作出复件
  • 复制RAII对象必需一并复制它管理的资源,所以资源的copying行为决定RAII对象的copy行为

条款15: 在资源管理类中提供对原始资源的访问

  • APIs往往需要访问原始资源,所以RAII class应该提供一个“取得底层管理资源”的方法
  • 对原始资源的访问可能需要显式转化或者隐式转化,一般而言显式转换比较安全,但是隐式转化对客户方便

条款16:成对使用new和delete时要采用相同的形式

  • 如果在new表达式中使用[],必须在相应的delete表达式中也使用[]

条款17: 以独立语句将newed对象置入智能指针

  • 将newed对象存入智能指针,不然在种中途出现异常抛出可能导致内存泄漏

设计和申明

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

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

  • 好的types有自然的语法,直观的语义,以及一或多个高效实现品,设计时考虑所面对的问题

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

  • 尽量以pass-by-reference-to-const替换pass-by-value,比较高效,并可避免切割问题
  • 对于内置类型,以及STL的迭代器和函数对象pass-by-value往往更高效

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

  • 绝不要返回pointer或reference指向一个local stack对象(在函数退出前被销毁)
  • 不要返回pointer或reference指向一个heap对象(用户不知道如何delete)
  • 不要返回pointer或者reference指向local static对象而有可能需要多个这样的对象(同一行不能调用多次该函数,static只有一份)

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

  • 切记将成员变量申明为private,这可具有语法的一致性、更精确的访问控制、封装、提供class作者充分的实现弹性等优点
  • protected并不比public更有封装性

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

  • 愈多函数可访问它,数据的封装性就愈低,故member函数封装性差
  • 将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,降低了编译依存性,这正是STL的做法

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

  • 如果某个函数的所有参数都进行类型转换,那这个函数必需是non-member类型,member函数无法对自身this指针的值进行隐式转化

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

  • 当std::swap对自定义类型效率不高时(例如深拷贝),提供一个swap成员函数,并确定不会抛出异常
  • 如果提供一个member swap,也该提供一个non-member swap用来调用前者 (对class而言,需特化std::swap;对class template而言,添加一个重载模板到非std命名空间内)
  • 不可以添加新的东西到std内
  • 调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何”命名空间修饰”

实现

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

  • 考虑定义变量式在循环外还是循环内部,考虑是一次构造+一次析构的成本高还是赋值操作的成本高
  • 尽可能延后变量定义式的出现时间,可以增加程序清晰度和改善程序效率

条款27:尽量少做转型操作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast;试着发展无需转型的替代设计
  • 如果转型是必要的,试着将它隐藏于某个函数后
  • 宁可使用C++-style转型,不要使用旧式转型(新式转型很容易辨识出来,而分门别类)

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

  • 避免返回handles(包括references、指针、迭代器)指向对象内部(包括成员变量和不被公开的成员函数),否则会破坏封装性,使const成员函数的行为矛盾,以及发生“空悬虚吊号牌码”

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

  • “异常安全函数”即使发生异常也不会有泄漏资源或允许任何数据结构败坏,区分为以下三种保证:
    基本承诺:异常抛出,程序内的任何事物仍然保持在有效状态下
    强烈保证:异常抛出,程序状态不改变,回复到调用函数之前的状态(往往能够以copy-and-swap实现出来)
    不抛掷保证:绝不抛出异常(如内置类型)

  • 可能的话提供“nothrow保证”,当“强烈保证”不切实际时,就必须提供“基本保证”

  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

条款30:透彻了解inline函数的里里外外

  • virtual代表需要等到运行期才能确定调用哪个函数,inline代表执行前调用动作替换为调用函数本体。
  • 将大多数inlining限制在小型、被频繁调用的函数身上
  • Template的具现化与inlining无关(Template放在头文件只是因为一般在编译器完成具现化动作)
  • inline只是给编译器的建议,大部分的编译器拒绝将太过复杂的函数inlining,隐喻方式是将函数定义于class定义式内
  • 构造函数和析构函数往往是inlining的糟糕候选人
  • 随着程序库的升级,inline函数需要重新编译,而non-inline函数只需重新连接

条款31:将文件间的编译依存关系降至最低

  • Interface classes由一个虚析构函数和一组纯虚函数构成,用来描述派生类的接口
  • 支持”编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes
  • Handle classes:在.h文件中用class 声明代替include头文件,把成员变量替换为指针的形式,理解的实现方式大致为:
// Person.h
#include <string>
using namespace std;

class PersonImp;
class Date;
class Address;

class Person
   
public:
    Person(const std::string& name,const Date& birthday,const Address& addr);
    string Name() const;
    string Birthday() const;
    string Address() const;

private:
    //string Name;            之前的定义方式,并且以include头文件实现
    //Date Birthday;
    //Address Address;
    std::tr1::shared_ptr<PersonImp> pImpl;     
    //通过提供的PersonImp接口类指针替换实现Person,起到了隔离的作用

// Person.cpp
#include "Person.h"                     //正在实现Person类
#include "PersonImpl.h"                 //使用PersonImp接口类实现Person
                                        //类,必须使用其成员函数,所以要
                                        //include接口类头文件
Person::Person(const std::string& name,const Date& birthday,const Address& addr)
:pImpl(new PersonImpl(name,birthday,addr))
 
string Person::Name() const

    return pImpl->Name();

...                                      //其余函数实现

  // PersonImp.h
#include <string>
#include "MyAddress.h"
#include "MyDate.h"
using namespace std;

class PersonImp                 //充当一个接口类,成员函数和Person相同,供
                                //Person类通过指针调用

public:
    string Name() const
    
        return Name;
   
   ...                          //其余成员函数定义

private:
    string Name;                //放置了所需的外来类对象
    MyAddress Address;
    MyDate Birthday;
;
  • 总之,此时任何接口类头文件产生的变化只会导致接口类头文件的变化而重新编译,以及Person实现文件由于include了接口类的头文件也要重新编译;而Person类头文件由于只使用了类的声明式,所以并不会重新编译,因此所有使用Person类的对象的文件也都不需要重新编译了,这样就大大降低了文件之间的编译依存关系
  • 另外,用Interface Classes也可以降低编译的依赖,实现方法大致是父类只提供虚方法,而将实现放置在子类中,再通过父类提供的一个特别的静态函数,生成子类对象,通过父类指针来进行操作;从而子类头文件的改动也不会导致使用该类的文件重新编译,因为用的是父类指针,客户include的是只是父类头文件,该静态方法实现如下:
std::tr1::shared_ptr<Person> Person::Create(const std::string& name,                    
                                            const Date& birthday, 
                                            const Address& addr)

    return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));

  • 对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编
  • 编译依存最小化的设计策略:
    1、如果使用object references或object pointers可以完成任务,就不要用objects
    2、如果能够,以class声明式替换class定义式
    3、为声明式和定义式提供不同的头文件

继承与面向对象设计

  • 继承可以是单一继承或者多重继承,每一个继承连接(link)可以是public,protected或private,也可以是virtual或者non-virtual
  • virtual函数意味着”接口必须被继承“,non-virtual函数意味着”接口和实现都必须被继承“

条款32:确定的你的public继承塑模出is-a关系

  • public继承意味is-a(是一个),适用于base classes身上的每一件事情一定适用于派生类上,因为每一个派生类也都是一个基类对象

条款33: 避免遮掩继承儿来的名称

  • 派生类的作用域被内嵌在基类的作用域内,如下图查找名称mf2,必须估计它指向(refer to)什么,编译器的作用域查找过程:local作用域(派生类)-》基类作用域-》包含基类的namespace-》global作用域
  • 派生类中 ”名称遮盖规则“存在,会覆盖基类同名函数,哪怕参数不一样。
    可以通过 using声明using Base::mf1来推翻名称遮盖,也可以用inline转交函数virtual void mf1()Base::mf1();

条款34:区分继承接口和实现接口

  • pure virtual函数使derived class只继承函数接口
  • impure virtual函数使derived class继承函数接口和缺省实现
  • non-virtual函数使derived class继承函数的接口和一份强制性实现

条款35:考虑virtual函数以外的其他选择

  • Non-Virtual Interface手法实现Template Method模式(带有模板功能的模式,在父类中定义处理流程的框架,在子类事项具体处理的模式):令客户通过public non-virtual成员函数间接调用private virtual函数,得以在一个virtual函数被调用之前设定好场景,并在调用结束之后清理场景。简而言之就是用public non-virtual成员函数包裹较低访问性的(private或者protected)的virtual函数。
  • 将virtual函数替换为”函数指针成员变量“,这是Strategy设计模式的一种分解表现。用一个函数指针实现strategy模式,可以由构造函数接受一个指针,指向一个提供的函数
class GameCharacter;                               // 前置声明
int defaultHealthCalc(const GameCharacter& gc);    //缺省函数
class GameChaaracter
public:
    typedef int(*HealthCalcFunc)(const GameCharacter&);//typedef简化函数声明
    explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
        :healthFunc(hcf)
    
    int healthValue()const
     return healthFunc(*this); 
    ……
    private:
        HealthCalcFunc healthFunc;           //函数指针
;
  • 使用tr::function完成Strategy模式:改用一个类型为tr1::function的对象,这样的对象可以保存任何可调用物(callable entity,即函数指针、函数对象、成员函数指针),只要其签名式兼容于需求端,typedef语句修改为:typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
  • 古典Strategy模式:将继承体系内的virtual成员函数

条款36:绝对不重新定义继承来恶的non-virtual的函数

  • 绝对不要重新定义继承而来的non-virtual函数,因为non-virtual是静态绑定,比如父指针pb指向ps时调用的fm()非虚方法一定时pb定义的,而virtual函数时动态绑定,用pb指向ps时调用的一定时ps定义的fm()方法

条款37:绝对不重定义继承而来的缺省参数

  • virtual函数 是动态绑定,而virtual函数的缺省参数值是静态绑定,所以可能导致 调用位于派生类中的virtual函数的同时 使用base class指定的缺省参数值
  • 使用NVI手法(令public non-virtual函数调用private virtual函数)可以防止缺省参数值被重新定义

条款38:通过复合塑模出has-a或者”根据某物实现出”

  • 当复合发生于应用域内的对象之间,表现has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系
  • 复合的意义和public继承完全不同

条款39:明智而审慎地使用private继承

  • private继承意味着只有实现被继承,它比复合的级别低
  • 尽可能使用复合,必要时才使用private继承(当derived class想访问base class的protected成分时,或为了重新定义virtual函数时,还有造成EBO(empty base optimization)节省内存时才为必要)
  • private继承规则:编译器不会自动将一个derived class对象转换为一个base class对象,不是is-a;所有成员都会变成private属性(主要因为成员的访问属性改变)

条款40:明智而审慎地使用多重继承

  • 多重继承可能从多个base class继承相同名称,也可能导致要命的“钻石型多重继承”(base class被多次构造,可以使用virtual继承解决)
  • 使用virtual继承导致对象体积大,访问成员变量速度慢等问题;因此,非必要不要使用virtual bases,如果要使用,尽可能避免在其中放置数据(相当于对virtual继承)
  • 多重继承的一个正当用途是“复合+继承”技术,单一继承更受欢迎

模板与泛型编程

条款41: 了解隐式接口和编译期多态

  • 面向对象编程一般是显式接口和运行期多态,而泛型编程更多的是隐式接口和编译期多态。
  • classe和template都支持接口和多态
  • 对class而言接口是显式的,由函数签名式构成;多态是通过virtual函数发生于运行期
  • 对template而言接口是隐式的,由有效表达式组成;多态是通过template具现化和函数重载解析发生于编译期

条款42:了解typename的双重意义

  • 在template声明式中,class和typenmae意义完全相同,但是在template内部涉及refer to可能只能用typename。在typename中指涉一个嵌套从属类型名称,就必须在临近他的前一个位置放上typename
  • “typename必需作为嵌套从属类型名称的前缀词”的例外,typename不可以存在base classes list(基类列)内的嵌套从属类型名称之前,也不可在member initialization list(成员初始值列)中作为base class 修饰符。

条款43:学习处理模板化基类内的名称

  • 可以用typedef typename xxxx 来代替 嵌套从属类型名称
  • 可在derivaed class templates内通过this> 指涉base class templates内的成员名称,或者由一个明白携程基类资格修饰符完成

条款44:将参数无关的代码抽离templates

  • Templates生成多个classes和多盒函数,所以任何template代码都不该与某个造成膨胀的template产生相依关系。
  • 因为非类型模板参数导致的代码膨胀,往往可以消除——》用函数参数或者class变量替换template参数
  • 因为类型参数造成(例如int和long共享二进制表示,有些链接器(linkers)可能合并完全相同的函数实现)的代码膨胀,可以降低,做法是带有完全相同二进制表述的具现类型共享代码

条款45:运用成员函数模板接受所有兼容类型

*使用成员函数模板生成“可接受所有兼职类型”的函数


temmplate<typename T>
class SmartPtr
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)     //泛化copy构造函数
        : heldPtr(other.get())...       //只有当“存在某个隐式转换可将U*                      
                                           //转换为T*”时才能通过编译
    //使用初始化列表将U中指针赋给T(两者之间必需存在可以隐式转化)
    T* get() const return heldPtr;
    ...
private:
    T* heldPtr;                            //持有的内置指针
;

*如果声明member templates用于“泛化copy构造”或者“泛化assignment操作”,还是需要声明正常的copy构造函数和copy assignment操作符(泛化操作存在时也需保留普通操作的声明)

条款46 :需要类型转化时请为模板定义非成员函数

*当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”,例如:

template <typename T>
class Rational
public:
    …
    friend const Rational<T> operator* (const Rational<T>& lhs, 
                                        const Rational<T>& rhs)
                                                               //定义体
        return Rational (lhs.numerator() * rhs.numerator(),
                         lhs.denominator() * rhs.denominator());
    
    …

*在一个class template内,template名称可被用来作为“template”的简略表达方式(混合命名)

  • template实参推导时不考虑隐式类型转化,而class template并不依赖template实参推导,在生成模板类时就可推导出函数而非函数模板(类模板在生成时就推导出函数)

条款47:使用traits classes表现类型信息

*STL迭代器根据操作分为5类,如图,这些structs的继承关系都是有效的is-a关系,所有的forward都是继承的input迭代器

  struct input_iterator_tag;//只能向前移动 一次一步 客户只可读取 类似为输入文件的读写指针 代表为istream_iterators
  struct output_iterator_tag;//类似 只涂写 一次一步
  struct forward_iterator_tag:public input_iterator_tag ;//一次一步 可读可写 hashed容器
  struct bidirectional_interator_tag:public forward_iterator_tag;// 双向  set map等
  struct random_access_iterator_tag:public bidirectional_interator_tag;// 任意步数移动 vector dequeue string
  • 设计一个traits class:1.确认需要将来可取的的类型的相关信息 2. 为该信息选择一个名称 3.提供一个template和一组特化版本
  • 建立一组重载函数或函数模板,彼此之间差异只在于各自的traits参数,令每个函数实现码
    *建立一个控制函数或函数模板,调用上述函数并传递traits class所提供的模板信息

条款48:认识模板元编程

  • 模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦测和更高的执行效率,可能导致较小的可执行文件,较短的运行期,较少的内存需求,可以解决不少问题

定制new和delete

条款49:了解new-handler的行为

  • operator new无法满足某一内存分配需求时,它会抛出异常;抛出异常之前,也可以先调用一个客户指定的错误处理函数(new-handler),调用set_new_handler可以指定该函数
  • Nothrow(在无法分配足够内存时返回NULL)是一个颇为局限的工具,它只适用于内存分配,后继的构造函数调用还是可能抛出异常

条款50:了解new个delete的合理替换时机

  • 有许多理由需要写个自定的new和delete,包括检测错误、改善效能,以及收集使用上的统计数据等等

条款51:编写符合常规的new和delete

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

条款52:写了placement new也要写placement delete

  • new operator/delete operator就是new和delete操作符,而operator new/operator delete是函数,其中new operator:调用operator new分配足够的空间,并调用相关对象的构造函数,并且不可以被重载;operator new(1)调用operator new分配足够的空间,并调用相关对象的构造函数,当无法满足所要求分配的空间时,则
    ->如果new_handler,则调用new_handler,否则
    ->如果没要求不抛出异常(以nothrow参数表达),则执行bad_alloc异常,否则
    ->返回0
    (2)可以被重载,重载时返回类型必须声明为void*,第一个参数类型必须表达为要求分配空间的大小,类型为size_t;并且重载时可以带其他参数
#include <iostream>
#include <string>
using namespace std;

class X

public:
    X()  cout<<"constructor of X"<<endl; 
    ~X()  cout<<"destructor of X"<<endl;

    void* operator new(size_t size,string str)
    
        cout<<"operator new size "<<size<<" with string "<<str<<endl;
        return ::operator new(size);
    

    void operator delete(void* pointee)
    
        cout<<"operator delete"<<endl;
        ::operator delete(pointee);
    
private:
    int num;
;

int main()

    X *px = new("A new class") X;
    delete px;

    return 0;

  • new operator与delete operator的行为是不能够也不应该被改变,这是C++标准作出的承诺。而operator new与operator delete和C语言中的malloc与free对应,只负责分配及释放空间。但使用operator new分配的空间必须使用operator delete来释放,而不能使用free,因为它们对内存使用的登记方式不同。反过来亦是一样。你可以重载operator new和operator delete以实现对内存管理的不同要求,但你不能重载new operator或delete operator以改变它们的行为。
  • 为什么有必要写自己的operator new和operator delete?答:为了效率。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此
  • placement new是重载operator new 的一个标准、全局的版本,它不能够被自定义的版本代替(不像普通版本的operator new和operator delete能够被替换)。void *operator new( size_t, void * p ) throw() return p; placement new的执行忽略了size_t参数,只返还第二个参数。其结果是允许用户把一个对象放到一个特定的地方,达到调用构造函数的效果。和其他普通的new不同的是,它在括号里多了另外一个参数.比如:Widget * p = new Widget; //ordinary new pi = new (ptr) int; pi = new (ptr) int; //placement new 括号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new将在这个缓冲器上分配一个对象。Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。placement new主要适用于:在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定的;长时间运行而不被打断的程序;以及执行一个垃圾收集器 (garbage collector)。
  • new 、operator new 和 placement new 区别:

(1)new :不能被重载,其行为总是一致的。它先调用operator new分配内存,然后调用构造函数初始化那段内存。

new 操作符的执行过程:

  1. 调用operator new分配内存 ;
  2. 调用构造函数生成类对象;
  3. 返回相应指针。

(2)operator new:要实现不同的内存分配行为,应该重载operator new,而不是new。

operator new就像operator + 一样,是可以重载的。如果类中没有重载operator new,那么调用的就是全局的::operator new来完成堆的分配。同理,operator new[]、operator delete、operator delete[]也是可以重载的。

(3)placement new:只是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,但需要调用对象的析构函数。

如果你想在已经分配的内存中创建一个对象,使用new时行不通的。也就是说placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void* p实际上就是指向一个已经分配好的内存缓冲区的的首地址。

  • Placement new 存在的理由:
  1. 用placement new 解决buffer的问题
    问题描述:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。
  2. 增大时空效率的问题
    使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。
  • new表达式先后调用operator new和default构造函数
  • 当你写一个placement operator new,请确定也写出了对应的placement operator delete.如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏(运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete,如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用)
  • 当你声明placement new和placement delete,请确定不要无意识地遮掩了它们的正常版本

9 杂项讨论

条款53:不要忽略编译器的警告

  • 严肃对待编译器发出的警告信息,努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉(在你打发某个警告信息之前,请确定你了解它意图说出的精确意义)
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失

条款54:让自己熟悉包括TR1在内的标准程序库

  • 标准委员会于1998年核准了C++ standard,该标准程序库包括:STL(容器、迭代器、算法、函数对象等)、iostreams、国际化支持、数值处理、异常阶层体系以及C89标准程序库
  • TR1详细叙述了14个新组件,放在std命名空间内(std::tr1)包括:智能指针、tr1::function、tr1::bind、Hash tables(用来实现sets、multisets、maps和multi-maps)、正则表达式、Tuples(变量组)、tr1::array、tr1::mem_fn(语句构造上与成员函数指针一致)、tr1::reference_wrapper(使容器“犹如持有references”)、随机数生成工具、数学特殊函数、C99兼容扩充以及Type traits(一组traits classes)、tr1::result_of(推导函数调用的返回类型)

条款55:让自己熟悉包括TR1在内的标准程序库

  • Boost提供许多TR1组件实现品,以及其他许多程序库

以上是关于Effective C++学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

《Effective C++ 》学习笔记——条款11

《Effective C++》学习笔记

Effective C++学习笔记

Effective C++学习笔记

《Effective C++》读书笔记汇总

学习日记之模板方法模式和 Effective C++