1. 条款29

函数要达到异常安全,需要满足两个条件:

  • 不泄露资源
  • 不允许数据败坏(至少需要数据是有效的)

异常安全函数通常可以达到如下几个等级

  • 基本承诺,异常被抛出,程序的任何事物仍然是有效地(比如计数器失效,有序数组失序),但当前程序的现实状态不确定,可能是几种状态之一。

  • 强烈保证,只有未执行和执行两种状态,失败则会返回到未执行状态。

  • 不抛掷保证,承诺不抛出异常,内置类型的所有操作都提供不抛掷保证,所以无法捕获到这些异常,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        try{
    int a = 0;
    int b = 5;
    int c = b / a;
    }
    catch(exception e){
    cout << "ztj: " << e.what();
    }
    return 0;
    }
    没有捕获到异常:
    (yolo) tangjie.zhang@moveai415:effective_cpp$ ./b
    Floating point exception (core dumped)

    达到强烈保证的一种实现,copy and swap,先创建对象副本,在副本上修改,(如果有异常,不会改变对象,如果无异常,接着处理),然后再不抛出异常的swap函数中置换。

  1. 条款30 inline

类中定义的函数隐式声明inline(具体是不是inline取决于编译器实现),包括friend函数。

加上明确的inline关键字达到显式声明。

优点:免除调用成本;利于编译器执行最优化(编译器会倾向于无outline函数执行最优化)

缺点:产生的目标码太大,降低缓存命中率,降低效率。改变inline函数后,程序必须重写编译链接。inline一般不支持调试,毕竟没法获得不存在的函数的地址。

inline函数应该放到头文件中(如果放到.c文件了,其他.c文件将无法按照inline的方式调用该inline函数。因为编译器通常是在编译过程中进行inlining,需要知道函数的实现,否则无法inlining,只能当在普通函数调用。类似的还有template,通常是编译器实例化,如果定义不在头文件,找不到定义,会报错)

inline是申请?太复杂的函数(包含递归,循环)无法inline,对virtual有调用的函数无法inline

  1. 条款31(还需要认真看下)
  • 声明式:告诉编译器某类型存在,不提供该类型的详细信息,可用于,形参或返回类型,指针,引用,模板参数

  • 定义式:定义式提供了类或函数的完整信息,定义某个对象时,置于class类中时都必须使用定义式(佐证1:编译器在编译时需要知道每个对象的大小),若只有声明式,会编译报错。

一个类如果包含了定义式,该类文件与包含的文件会形成编译依存关系,头文件改变,该类文件也要再次编译。如果只包含声明式,则不存在这些问题,毕竟该类只需要知道该类型存在就行了。

接口与实现分离:首先是一个接口类,其中包含一个指向实现类的指针。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include  <string> 
#include <memory>
class Personimpl;
class Date;
class Address;
class Person {
public:
//标准程序库组件不该被前置卢明。
//此乃为了 trl:: shared_ptr 而含入;详后。
//Person 实现类的前置声明。
//Person接口用到的 classes 的前置声明。
Person(const std::string& name, canst Date& birthday,
canst Address& addr);
std:: string name() canst;
std:: string birthDate () canst;
std:: string address() canst;
private:
std:trl::shared_ptr<Personimpl> pimpl; //指针,指向实现物;
} ;
  1. 条款34

精确指定你想要 derived classes 继承的东西:

  • 只继承接口:纯虚函数
  • 继承接口和一份缺省实现:非纯虚函数。为避免派生类无脑继承,可以用纯虚函数+实现解决,如下代码。
  • 继承接口和一份强制实现:非虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() = 0;
};
void Base::func(){
cout << "defalult" << endl;
};
class DD: public Base{
public:
void func(){
cout << " call Base" << endl;
Base::func();
};
};
int main(){
DD b;
b.func();
b.Base::func();
return 0;
};
(yolo) tangjie.zhang@moveai415:effective_cpp$ ./b
call Base
defalult
defalult
  1. 条款35

替换virtual函数的几个方案:

使用NVI(non virtual interface),在接口中包裹较低访问性(private或public)的virtual函数,有助于维持接口的一致性、实现封装等。

使用function或者函数指针等成员变量替换virtual函数。

将virtual替换为另一个继承体系的virtual函数,即strategy设计模式的传统实现手法。

软件模式 from kimi ai

创建型模式(Creational Patterns)

创建型模式主要关注对象的创建过程,试图以合适的方式创建对象。

  1. 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
  2. 工厂方法模式(Factory Method):定义创建对象的接口,让子类决定实例化哪一个类。
  3. 抽象工厂模式(Abstract Factory):创建相关或依赖对象的家族,而不需明确指定具体类。
  4. 建造者模式(Builder):构建一个复杂的对象,并允许按步骤构造。
  5. 原型模式(Prototype):通过拷贝现有的实例创建新的实例,而不是通过新建。

结构型模式(Structural Patterns)

结构型模式主要关注对象的组合,以及它们之间的关系。

  1. 适配器模式(Adapter):允许对象间的接口不兼容问题,使原本因接口不兼容而不能一起工作的类可以一起工作。
  2. 桥接模式(Bridge):将抽象部分与其实现部分分离,使它们可以独立地变化。
  3. 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
  4. 装饰器模式(Decorator):动态地给一个对象添加额外的职责。
  5. 外观模式(Facade):为子系统中的一组接口提供一个统一的高层接口。
  6. 享元模式(Flyweight):通过共享来高效地支持大量细粒度的对象。
  7. 代理模式(Proxy):为其他对象提供一个代替或占位符以控制对它的访问。

行为型模式(Behavioral Patterns)

行为型模式主要关注对象间的通信,以及它们如何相互协作。

  1. 责任链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
  2. 命令模式(Command):将请求封装为一个对象,从而使用户可用不同的请求对客户进行参数化。
  3. 解释器模式(Interpreter):定义语言的文法,并且建立一个解释器,这个解释器可以解释和执行语言中的句子。
  4. 迭代器模式(Iterator):顺序访问一个聚合对象中的各个元素,不暴露其内部的表示。
  5. 中介者模式(Mediator):定义一个中介对象来简化原有对象之间的交互。
  6. 备忘录模式(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
  7. 观察者模式(Observer):对象间的一对多依赖关系,当一个对象改变时,所有依赖于它的对象都会被通知到。
  8. 状态模式(State):允许对象在其内部状态发生改变时改变其行为。
  9. 模板方法模式(Template Method):在一个方法中定义一个算法的骨架,将一些步骤的执行延迟到子类。
  10. 访问者模式(Visitor):为一个对象结构(如组合结构)增加新能力。
  1. 条款36 && 37

non-virtual函数不应该被复写(虽然理论上可以),只有virtual才可以。

virtual函数定义是动态绑定的,而缺省参数(参数的值)是静态绑定的:派生类的virtual可以有不同的缺省参数,但默认会使用基类的参数,为避免混淆。可以使用nvi替换,public函数(一个)调用private virtual函数(一个或多个),在public用指定参数(只用写一次)调用private virtual函数,即可避免混淆。

  1. 条款38

复合意味着 has - a(应用域,遇到has a关系想到复合:人->电话号码),意味着 is-implementet-in-terms-of(实现域)。

public继承意味着 is - a(应用域,遇到is-a想到public继承:学生->人)。

  • 实际使用时,先看设计层面,如果d满足b的所有特性,d是b的特例,再进行编码,d: public b;(可以有些virtual函数,这些virtual函数并不涉及到d,b的特性层面,比如四边形的特性只有四条边,而virtual函数可以有面积,周长,这不是特性)

  • 如果d不满足b的所有特性,考虑其他方式。(比如,set,list,list可以有重复元素,set不能有,这在设计层面就不相同,虽然可以通过virtual函数弥补,但首先这不是一种好的设计,其次需要修改基类的相关函数,修改标准库是不允许的)

  1. 条款39 private继承

两个规则:

  • 不会把派生类自动转化为基类
  • 基类的所有成员,在派生类中会变成private属性。

private继承不代表is a,而是代表implemented-in-terms-of(根据某物实现出)。private继承仅仅是为了使用基类的某些特性,不是因为两者有任何关系。两者在设计领域没有意义,它的意义只在实现层面。

private继承和复合都意味着根据某物实现出,如何抉择:

  • 尽量用复合

  • 涉及到protected或者virtual时,可以用private继承。

public继承接口,private继承实现()

  1. 条款41

面对对象:显示接口,运行期多态(根据绑定的对象类型选择virtual函数)

泛型编程:隐式接口,编译期多态(根据传递的template参数选择函数)

显示接口由函数的签名组成(名称,参数类型,返回类型)。

隐式接口由有效表达式组成。(隐式接口不具体,只是粗略的限制了类型参数)

10.条款43

模板派生类一般拒绝在模板化基类中寻找继承而来的名称,因为他不确定对应的名称是否存在。三种方法:

  • 使用this->func
  • 使用using class::func
  • 使用 class::func (会导致virtual函数无法准确绑定)

这三种做法都可以向编译器承诺:基类会提供相应接口。将报错时间延后到编译期多态。

1
2
3
4
5
43.cpp:15:17: error: ‘baseFunc’ was not declared in this scope, and no declarations were found by argument-dependent lookup at the point of instantiation [-fpermissive]
15 | baseFunc(U()); // 正确
| ~~~~~~~~^~~~~
43.cpp:15:17: note: declarations in dependent base ‘Base<int>’ are not found by unqualified lookup//在依赖的base<int>类中,通过未限定寻找找不到声明。
43.cpp:15:17: note: use ‘this->baseFunc’ instead
  1. 条款46

条款24 提到如果在函数调用时,需要将所有参数进行类型转换(如 “abd” + string(“75”)),只能定义non-menber函数,这在非模板情况下是成立的。如果参数对应的类别是模板类,转换前还需要进行模板推导,参数将不支持隐式转换,上述代码将失效。可以将该函数声明在参数对应的class内,class被定义时,会自动生成函数的声明,如果只提供了声明,需要手动提供定义,为此,需要将声明定义同时放到类内,friend函数。

  1. 条款47

迭代器分类:

  • input,只能向前移动,只读,如istream_iterator
  • ouput,只能向前移动,只写, 如ostream_iterator
  • forward,只能前向移动,可读写,如单链表对应迭代器
  • bidirectional,可双向移动,可读写,如set,multiset,map,multimap
  • random,可随机访问,可读写,如vector,deque,string

这里介绍了一个方法,来讲if判断提前到编译时:将if判断的类型放到函数参数中,函数重载了若干个版本,可以在编译时用重载解析机制选择代码。

iterator_traits,提供了四分信息:iterator_category ,value—type,有 char_traits,numeric_limits。

c++有 traits classes ,包括is_fundamental (判断 T 是否为内置类型), is_array (判断 T 是否为
数组类型),以及 is_base—of<Tl,T2>(Tl 和 T2 相同,抑或 Tl 是 T2 的 base class)

  1. 条款48 模板元编程

其可以实现声明变量,执行循环,编写和调用函数。

循环由递归实现,递归模板具体化。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
120(yolo) tangjie.zhang@moveai415:effective_cpp$ cat 48.cpp
using namespace std;
template <unsigned n>
class Factorial{
public:
enum {value = n * Factorial<n -1>::value};
};
template <>
class Factorial<0>{
public:
enum {value = 1};
};
int main(void){
cout << Factorial<5>::value;
return 0;
};

作用:

  • 确保度量单位正确。
  • 优化矩阵运算
  • 生成定制之设计模式。比如智能指针
  1. 条款49

operator new无法满足内存申请要求时,会调用new-handler,在其中,可以完成如下事情:

  • 分配更多内存,比如程序启动时先分配一大块内存,需要时再返还。
  • 安装另外的new-handler,自己无法处理时,可以在setnewhandler替换newhandler,下次operter new分配失败时,会调用新的。
  • 卸载new-handler,传null,operator分配不成功会直接报异常。
  • 抛出bad_alloc异常
  • 不返回,调用abort或exit

CRTP(Curiously Recurring Template Pattern)

15.条款50

为什么要替换opertaor new,delete,三个理由:

  • 检测很多错误,比如地址越界
  • 强化性能,默认newdelete对每个人都适度的好,但不发达到最佳表现
  • 收集使用数据分配大小,寿命,FIFO or LIFO or 随机归还,
  1. 条款51 编写new时的规则

当编译器遇到用于分配类型为 T 的对象的 new 运算符时,它会发出对 T::operator new( sizeof(T) ) 的调用。(sizeof貌似是编译器自动加的,这印证了无法从类转换为size_t)

  1. 条款52

写下new时,调用了operator new 和 构造函数。

  1. 条款54

c++ 98 包含:

  • STL C Standard Template Library, 标准模板库),覆盖容器 (containers 如 vector,
    S七ring,rnap) 、迭代器 (iterators)、算法 (algorithms 如 find, sort, transform) 、
    函数对象 (function objects 如 less, greater) 、各种容器适配器 (container
    adapters 如 stack, priority_ queue)和函数对象适配器 (function object adatpe氏
    如 rnern_fun,notl) 。
  • Iostreams,覆盖用户自定缓冲功能、国际化 1/0,以及预先定义好的对象 cin, cout,
    cerr和 clog 。
  • 国际化支持,包括多区域 (multiple active locales) 能力。像 wchar_t (通常是
    16 bits/char)和 wstring(由 wchar_七s 组成的 strings)等类型都对促进 Unicode
    有所帮助。
  • 数值处理,包括复数模板 (complex) 和纯数值数组 Cvalarray) 。
  • 异常阶层体系 (exception hierarchy) ,包括 base class excep巨on及其 derived
    classes logic error和 runtime error, 以及更深继承的各个 classes 。
  • C89 标准程序库。 1989 C 标准程序库内的每个东西也都被覆盖千 C++内。

TR1 新组件(14个,对98的拓展,后被收入c++11)

  • 智能指针,
  • function,可以得到任何可调用物
  • bind,
  • 其他(第一组)杂项
    • hash 表,用来实现 sets, multisets, maps 和 multi-map
    • 正则表达式
    • tuples ,pair可以用有两个对象,而tuples可以有任意个
    • array,stl化的数组,
    • mem_fn,
    • reference_wrapper,
    • 随机数
    • 数学特殊函数
    • c99兼容扩充
  • 其他(第二组)由更精巧的 template 编程技术构成
    • type traits,提供类型的编译期信息,
    • result_of,推导函数调用的返回类型