添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 50.理解何时替换 new 和 delete 有意义
  • 51.写 new 和 delete 时遵循惯例
  • 52.写了 placement new 也要写 placement delete
  • 9.杂项讨论
  • 53.注意编译器警告
  • 54.熟悉包括 TR1 在内的标准库
  • 55.熟悉 Boost
  • 1.让自己习惯 C++

  • 使用成员初始化列表列替换赋值动作,前者效率更高,后者先设初值再赋值
  • 可使用无参数构造函数来初始化
  • 对于多个构造函数,可添加私有成员函数,接收初始化参数,在函数内部使用赋值操作给成员变量“初始化”
  • 初始化顺序
  • 先基类再衍生类
  • 类内部,按照声明的顺序初始化,与成员初始化列表列操作顺序无关
  • 最好按照声明顺序初始化
  • 不同编译单元内的 non-local static 对象的初始化顺序未定义
  • static 对象包括全局对象、定义于命名空间作用域内的对象、类内、函数内,以及在文件作用域内被声明为 static 的对象
  • 函数内的 static 对象称为 local-static 对象,其他的则是 non-local static 对象
  • 程序结束时 static 对象会被自动销毁,即在 main 函数结束时调用他们的析构函数
  • 编译单元是产出单一目标文件的源码
  • 将每个 non-local static 对象移到自己的专属函数内,改函数返回对该对象的引用,保证该函数被调用期间,首次遇到该对象的定义时被初始化,即以函数调用替换直接访问 non-local static 对象
  • class Uncopyable {
    protected: // allow constructor and destructor for derived object
    Uncopyable() {}
    ~Uncopyable() {}
    private:
    Uncopyable(const Uncopyable&); //avoid copying
    Uncopyable& operator=(const Uncopyable&);
    

    07.声明多态基类析构函数为虚函数

  • 包含虚函数的类需要额外的信息来实现虚函数:vptr(virtual table pointer)指向一个由函数指针构成的数组,称为 vtbl(virtual table),每个有虚函数的类都有一个相应的 vtbl
  • 析构顺序:先父类再子类,构造函数的调用顺序相反
  • 带有多态性质的基类应声明一个虚析构函数
  • 如果一个类带有任何虚函数,就声明一个虚析构函数
  • 类的设计目的不是作为基类使用,或者不是为了多态性,不应该声明虚析构函数
  • 08.别让异常逃离析构函数

  • 确保对象自我赋值时,operator= 行为良好,包括比较源对象和目标对象的地址、精心周到的语句顺序(先复制源对象,再执行删除),以及icopy-and-swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,行为仍然正确
  • 12.复制对象的所有部分

  • 拷贝构造函数和拷贝赋值操作符都是 copying 函数
  • copying 函数应该确保复制“对象内的所有成员变量”和“所有基类成分”
  • 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将相同的东西抽象成一个函数,二者都调用这个函数
  • 3.资源管理

  • 为防止内存泄漏,建议使用 RAII(Resource Acquisition Is Initialization,资源取得时机就是初始化时机) 对象,它们在构造函数中获得资源并在析构函数中释放资源
  • 常用的 RAII 类是 shared_ptr 和 auto_ptr。前者的拷贝行为比较直观,后者的复制动作会转移资源的所有权:shared_ptr 有引用计数,但是无法打破环装引用
  • 参考智能指针一文
  • 14.在资源管理类中小心复制行为

  • 复制 RAII 对象必须一并复制它锁管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为
  • 一般情况下,RAII 类的 copying 行为是:阻止 copying、实行引用计数法
  • 15.在资源管理类中提供对原始资源的访问

  • APIs 往往要求访问原始资源,所以每一个 RAII 类应该提供一个接口可以获得其管理的资源
  • 对原始资源的访问可以是显示转换或隐式转换:一般显示转换比较安全,隐式转换对客户比较方便
  • 16.在对应的 new 和 delete 采用相同形式

  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
  • “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任
  • shared_ptr 支持自定义删除器,可以防止 DLL 问题,可被用来自动解除互斥锁
  • 19.把类设计看作类型设计

  • 对象的初始化和对象的赋值该有什么样的差别:区分构造函数和赋值操作符的行为
  • 新类型的对象如果以值传递,意味着什么:取决于拷贝构造函数
  • 什么是新类型的“合法值”:确定需要做的错误检查工作
  • 新类型需要配合某个继承图系吗:受继承类的约束,如果允许被继承,析构函数是否为虚函数
  • 新类型需要什么样的转换:显示类型转换和隐式类型转换
  • 什么样的操作符和函数对此新类型是合理的:确定需要声明的函数,哪些是成员函数,哪些不是成员函数
  • 谁该调用新类型的成员:确定成员的属性(public/protected/private),也确定类之间的关系(所属,友元)
  • 什么是新类型的未声明接口
  • 新类型有多一般化:是否需要定义一个模板类
  • 真的需要一个新类型吗:是否可以为已有类添加非成员函数或模板来实现
  • 20.常量引用传递优于值传递

  • 值传递效率低,而且可能造成对象切割(slicing):值传递一个衍生类对象时,如果函数声明的是基类,那么调用的是基类的拷贝构造函数
  • C++ 编译器底层使用指针实现,不同情形使用不同的方式
  • 内置类型(如 int)采用值传递
  • STL 的迭代器和函数对象使用值传递
  • 其他的采用常量引用传递
  • 21.必须返回对象时,不要返回引用

  • 绝不要返回指针或引用指向一个 local stack 对象
  • 绝不要返回引用指向一个 heap-allocated 对象
  • 绝不要返回指针或引用指向一个 local static 对象而有可能同时需要多个这样的对象
  • 22.声明数据成员为私有的

  • 提供一个 public swap 成员函数,在函数内高效地置换两个对象值
  • 在类或模板所在的命名空间提供一个非成员的 swap 函数,在函数内调用上述 swap 函数
  • 如果正在编写一个类或类模板,让该类特化 std::swap,另其调用上述的 swap 函数
  • 如果调用 swap,确定包含using std::swap,然后不加任何 namespace 修饰符,直接调用 swap,编译器就会查找适当的 swap 函数并调用
  • 警告:成员函数 swap 不可抛出异常
  • const_cast<T>( expression )用来移除对象的常量性,唯一可以实现这个目的的 C++ 风格的转换操作符
  • dynamic_cast<T>( expression )用于执行“安全向下转换”,用于确定某对象是否归属继承体系中的某个类型,可能耗费重大运行成本,唯一一个 C 风格无法实现的转换操作
  • reinterpret_cast<T>( expression )意图执行低级转换,实际动作和结果可能取决于编译器,即不可移植
  • static_cast<T>( expression )用于强迫隐式转换,例如 non-const 转换为 const,或者 int 转 double 等
  • 倾向使用 C++ 风格的转换操作,不要使用 C 风格的转换
  • 易被辨识,因而得以简化查找类型被破坏的过程
  • 各转换工作有各自的局限,便于编译器诊断错误的运用
  • 如果可以,尽量避免转换操作,特别是在注重效率的代码中避免 dynamic_cast,如果有需要,尝试改成无需转换的设计
  • 使用类型安全容器,确定是哪种衍生类或基类
  • 将虚函数放在父类,然后添加空实现
  • 如果必须转换,试着用函数封装,可以调用函数,而无需将转换操作引入代码
  • 28.避免返回指向对象内部的句柄

  • 避免返回 handles(包括引用、指针、迭代器)指向对象内部。一遍增加封装性,帮助 const 成员函数的行为像个 const,并将发生 dangling handles 的可能性降至最低
  • 29.努力写异常安全的代码

  • 异常安全函数即使发生议程也不会内存泄漏或破坏任何数据结构。这样的函数分为三种可能的保证:基本型、强烈型、不抛异常型
  • “强烈保证”往往以 copy-and-swap 实现,但“强烈保证”并非对所有函数都可实现或具备现实意义
  • 函数提供的“异常安全保证”通常最高只等于其调用的各个函数的“异常安全保证”中的最弱者
  • 30.了解内联的细节

  • pimply idiom(pointer to implementation):将一个类分为两个,一个提供接口,一个负责实现接口,前者在类内包含一个后者的 shared_ptr,做到“接口与实现分离”
  • 使用接口类、衍生类和工厂模式进行实现
  • 分离的关键在于“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,万一做不到,则使用前置声明
  • 尽量使用对象引用或对象指针,而不是对象:可以在头文件中使用前置声明
  • 尽量使用 class 声明式而不是 class 定义式
  • 为声明式和定义式提供不同的头文件
  • 程序头文件应该以“完全且仅有声明式”的形式存在
  • 6.继承与面向对象设计

  • 如果继承基类并加上重载函数,又希望重新定义或覆盖其中一部分,必须为那些原本会被隐藏的名称引入一个 using 声明式,否则继承的名称会被隐藏
  • 为了让隐藏的名称仍然可见,可使用 using 声明式或 forwarding 函数
  • 内置的 forwarding 函数的另一个用途是为那些不支持 using 声明式的编译器而用
  • 34.区分接口继承和实现继承

  • 接口继承和实现继承不同。在 public 继承时,衍生类会继承基类的接口,即成员函数
  • 声明纯虚函数的目的是让衍生类只继承函数接口
  • 声明非纯虚函数的目的是让衍生类继承该函数的接口和缺省实现
  • 声明非虚函数的目的是让衍生类继承函数的接口和一份强制性实现
  • 35.考虑虚函数的替代

  • 使用 non-virtual interface(NVI)手法,是 Template Method 设计模式的一种特殊形式。以 public non-virtual 成员函数包裹较低访问性的虚函数
  • 将虚函数替换为“函数指针成员变量”。是 Strategy 设计模式的一种分解表现形式
  • 以 function 成员变量替换虚函数,因而允许使用任何可调用实体(callable entities)搭配一个兼容与需求的签名式。这也是 Strategy 设计模式的某种形式
  • 将继承体系内的虚函数替换为另一继承体系的虚函数。这是 Strategy 设计模式的传统实现手法
  • 将功能从成员函数移到类外部,缺点是非成员函数无法访问类的 non-public 成员
  • function 对象的行为就像一般函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用实体
  • 36.绝不重定义继承的非虚函数

  • 动态类型可在程序执行过程中改变
  • 可以使用 NVI 手法:另基类内的一个 public 非虚函数调用 private 虚函数,后者可被衍生类重新定义。让非虚函数知道缺省参数,虚函数负责真正的工作
  • 38.通过组合对”has-a”或”is-implemented-in-terms-of”建模

  • 复合是类型间的一种关系,当某种类型的对象内包含其他类型的对象,就是复合关系
  • 在应用域,复合意味着 has-a(有一个)。在实现域,复合以为着 is-implemented-in-terms-of(根据某物实现出)
  • 39.慎重使用私有继承

  • private 继承意味着 is-implemented-in-terms-of。通常比复合的级别低,但是当衍生类需要访问基类的 protected 成员,或需要重新定义继承而来的虚函数时,private 继承是合理的
  • private 继承时,编译器不会自动将一个衍生类对象转换为一个基类对象
  • 由 private 继承而来的所有成员,在衍生类中都是 private 属性
  • private 继承是一种实现技术,意味着只有实现部分被继承,接口部分应忽略
  • 与复合相比,private 继承可以使得空白基类最优化(EBO, empty base optimization)。对致力于“对象尺寸最小化”的程序库开发者比较重要
  • 尽可能使用复合,必要时采用 private 继承
  • 当想要访问一个类的 protected 成员,或需要重新定义该类的一个或多个虚函数
  • 当空间更加重要,衍生类的基类可以不包含任何 non-static 成员变量
  • “独立(非附属)”对象的大小一定不为零,不适用于单一继承(多重继承不可以)衍生类对象的基类
  • 40.慎重使用多重继承

  • 虚继承:防止多重继承时,基类之间又有基类,从而上层的基类的成员变量被父类复制
  • 虚继承的类产生的对象体积更大,访问虚基类的成员变量速度慢,增加初始化(及赋值)的复杂度
  • 如果虚基类不带任何数据,是具有使用价值的情况
  • 多重继承比单一继承复杂,可能导致新的歧义性,以及对虚继承的需要
  • 多重继承的用途:涉及“public 继承某个接口类”和“private 继承某个协助实现的类”
  • 7.模板与泛型编程

  • 类和模板都支持接口和多态
  • 对类而言接口是显式的,以函数签名为中心。多态则是通过虚函数发生于运行期
  • 对模板参数而言,接口是隐式的,基于有效表达式。多态则是通过模板具体化和函数重载解析,发生于编译期
  • 42.理解 typename 的双重定义

  • 模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的模板参数产生依赖关系
  • 因非类型模板参数造成的代码膨胀,往往可以消除,做法是以函数参数或类成员变量替换模板参数
  • 因类型参数造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表示的具体类型实现共享代码
  • 45.使用成员函数模板来接受“所有兼容类型”

  • 具有基类-衍生类关系的两个类型分别具体化某个模板,生成的两个结构并不带有基类-衍生类关系
  • 使用成员函数模板生成“可接受所有兼容类型”的函数
  • 如果声明成员模板用于“泛化拷贝构造”或“泛化赋值操作”,必须声明正常的拷贝构造函数和拷贝赋值操作符
  • 声明泛化拷贝构造函数和拷贝赋值操作符,不会阻止编译器生成默认的拷贝构造函数和拷贝赋值操作符
  • 46.需要类型转化时在模板内定义非成员函数

  • input 迭代器:只能向前移动,一次异步,只可读取(不能修改)所指的东西,且只能读取一次。模仿了指向输入文件的读指针。如 C++ 的 istream_iterator
  • output 迭代器:只能向前移动,一次一步,只可修改所指的东西,且只能修改一次。模仿了指向输出文件的写指针。如 C++ 的 ostream_iterator
  • input 和 output 迭代器都只适合“单步操作算法(one-pass algorithms)”
  • forward 迭代器:既能完成上述两种迭代器的工作,且可以读或写所指对象一次以上。使得可以实施“多步操作算法(multi-pass algorithms)”。如单向链表的迭代器
  • bidirectional 迭代器:既能完成 forward 迭代器的工作,还支持向后移动。STL 的 list/set/multiset/map/multimap 迭代器就属于这一分类
  • random access 迭代器:可以执行“迭代器运算”,即可以在常量时间内向前或向后跳跃任意距离。如 array/vector/deque/string 提供的都是随机访问迭代器
  • 如何设计一个 traits 类
  • 确认若干希望将来可取得的类型相关信息。例如迭代器希望取得分类(category)
  • 为该信息选择一个名词。如迭代器是 iterator_category
  • 提供一个模板和一组特化版本,其中包含希望支持的类型相关信息
  • traits 类的名称常以”traits”结束
  • 如何使用一个 traits 类
  • 建立一组重载函数(类似劳工)或函数模板,彼此间的差异只在于各自的 traits 参数。令每个函数实现与其接受的 traits 信息相对应
  • 建立一个控制函数(类似工头)或函数模板,调用上述的函数并传递 traits 类所提供的信息
  • traits 类使得“类型相关信息”在编译期可用。它们以模板和一组“模板特化”完成实现
  • 整合重载技术后,traits 类可在编译期对类型执行 if…else 测试
  • 48. 认识模板元编程

  • 让某些事情更容易
  • 可将工作从运行期转移到编译期。使得原本在运行期才可以侦测的错误在编译期被找到
  • TMP 的 C++ 程序在每一方面可能更加高效:较小的可执行文件、较短的运行期、较少的内存需求
  • 缺点:导致编译时间变长
  • TMP 主要是函数式语言,可以达到的目的
  • 确保度量单位正确:在编译期确保程序所有度量单位的组合是正确的
  • 优化矩阵运算:使用 expression template,可能会消除中间计算生成的临时对象并合并循环
  • 可生成用户自定义设计模式的实现品。设计模式如 Strategy/Observer/Visitor 等都可以多种方式实现
  • 语法不直观
  • 支持工具不充分,如没有调试器
  • 8.定制 new 和 delete

  • newdelete只适合分配单一对象;new []delete []用来分配数组
  • STL 容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,而不是 new 和 delete 管理
  • 49.理解 new-handler 的行为

  • 可以用是set_new_handler设置该函数
  • 参数是个指针,指向 new 无法分配足够内存时该调用的函数
  • 返回值是个指针,指向set_new_handler被调用之前正在执行的 new_handler 函数
  • new_handler 是个 typedef,定义一个指针指向函数,函数没有参数也没有返回值
  • 设计良好的 new-handler 函数
  • 让更多内存可被使用:程序一开始执行就分配一大块内存,而后第一次调用 new-handler,将该内存释放给程序使用
  • 设置另一个 new-handler:如果已知哪个 new-handler 可以获得更多可用内存,调用时设置该 new-handler 替换自己。比如令 new-handler 修改“会影响 new-handler 行为”的静态数据、命名空间数据或全局数据
  • 取消设置 new-handler:即将 null 指针传给set_new_handler,内存分配不成功时就会抛异常
  • 抛出 bad_alloc 或派生自 bad_alloc 的异常:该异常不会被 new 操作捕获,但会传播给请求内存的代码
  • 不返回:通常调用 abort 或 exit
  • nothrow new是一个有局限性的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常
  • 50.理解何时替换 new 和 delete 有意义

  • 检测运用上的错误:自定义 new 操作,可超额分配内存,以额外空间放置特定的 byte patterns(即签名,signature)。对应的 delete 操作可以检查上述签名是否原封不动,若否表示在分配区的某个声生命时间点发生了 overrun(写入点在分配区块尾端之后) 或 underrun(写入点在分配区块起点之前)。此时 delete 可以日志记录该时间和发生错误的指针
  • 强化效能:编译器的 new 和 delete 无法解决碎片问题,导致程序可能无法申请大区块内存。通常来说这种自定制的性能更好
  • 收集使用上的统计数据:先收集软件如何使用动态内存,包括分配区块的大小分布、寿命分布、分配和释放的次序(FIFO/LIFO/随机)、任何时刻内存分配上限
  • 增加分配和释放的速度:当定制型分配器专门针对某特定类型的对象设计时,往往比泛用型分配器更快
  • 降低缺省内存管理器带来的空间额外开销:泛用型内存管理器往往使用更多内存
  • 弥补缺省分配器中的非最佳对齐:缺省的分配器一般是 4 字节对齐,但是对于 x86 最好是 8 字节对齐
  • 将相关对象成簇集中:将往往被一起使用某个数据结构放在一起创建,可以减少 page fault 的错误
  • 获得非传统的行为:比如添加数据初始化工作
  • 51.写 new 和 delete 时遵循惯例

  • 如果自己实现一个 placement operator new,也要写出对应的 placement operator delete。否则会发生隐蔽时断时续的内存泄漏
  • 当声明 placement new 和 placement delete,确定不要无意识地遮掩它们的正常版本
  • 9.杂项讨论

  • 严肃对待编译器发出的警告信息。努力在编译器的最高(最严苛)警告级别下争取“无任何警告”
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度不相同。一旦移植到另一个编译器上,原本依赖的警告信息有可能消失
  • 54.熟悉包括 TR1 在内的标准库