• 《Effective C++》笔记


    《Effective C++》笔记

    序言

    声明:告诉编译器某个东西的名称和类型,但略去细节。
    定义:提供编译器一些声明所遗漏的细节,对对象而言,定义是编译器为其分配内存的地方。
    初始化:给予对象初值的过程。

    条款01:视C++为一个语言联邦

    初看题目,对这个语言联邦甚是不解,看完条款1,恍然大悟。所谓语言联邦,即C++由多种编程范式组成,包括:

    1. C。C++说到底还是在C基础上发展的,但是在C++中依然可以使用C,但是C中没有模板、异常、重载。
    2. Object-Oriented C++。也称之为C with classes。简而言之,封装、继承、多态。
    3. Template C++。泛型编程,STL中常用的手法。再往外延伸一点就是模板元编程。
    4. STL。标准模板库,包括容器、算法、迭代器、适配器、仿函数、分配器。

    C++高效编程守则视状态而变化,取决于你是使用C++的哪一部分。

    条款02:尽量以const、enum、inline替换#define

    请记住:

    • 对于单纯常量,最好以const对象或者enum替换#define
    • 对于形似函数的宏,最好改用inline函数替换#define
    1. “宁可用编译器替换预处理器”。宏定义出错溯源困难的原因是:宏定义所使用的名称可能并未进入符号表。宏定义编译时报错,给的错误提示是它所替代的真实值,如果这个宏定义在其他头文件中,查找错误无疑是耗时的。#define就无法做到创建类专属常量。

    2. 以常量替换#define有两种特殊情况:

    • 定义常量指针:由于常量定义通常放在头文件中,所以有必要将指针声明为const
    • class专属常量:通常类专属常量要被static const修饰,因为要保证这个常量在所有类对象中只存在一份,所以用static修饰,而又要求是常量,所以用const修饰;只要不取这个常量的地址,无需提供定义,只需在类中声明,例如static const int num = 5;,但是,如果坚持要获得这个常量的地址,那么需要在类外提供定义,例如const int Base::num;,由于class常量已经在声明的时候获得初值,所以在类外定义的时候无需再设初值。
    1. 在类中还可以使用enum来完成第2点中的任务。且enum#define很相似,因为都无法取地址。

    2. 通常不同编译器的规则不太一样,所以使用static const还是enum要根据编译器来定。

    条款03:尽可能使用const

    1. 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。例如,如果返回类型没有const,那么可能出现(a*b)=c;的情况。一个“良好的用户自定义类型”的特征是他们避免无端地与内置类型不兼容。

    2. 如果两个成员函数只是常量性不同,可以被重载。将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象。

    3. bitwise constness和logical constness

    • bitwise constness:成员函数只有在不更改对象的任何成员变量时才可以说是const的
    • logical constness:一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦察不出的情况才能如此操作。想要让客户端侦察不出,就需要对可以接受修改的变量加上mutable修饰

    请记住:

    • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
    • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”
    • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

    条款04:确定对象被使用前已先被初始化

    请记住:

    • 为内置型对象进行手工初始化,因为C++不保证初始化他们
    • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在类中的声明次序相同
    • 为避免“跨编译单元初始化次序”问题,使用local static对象替换non-local static对象(还有一种技术是:在新建一个类,专门用于初始化)
    1. 需要着重强调的是:在构造函数中,初值列中才叫初始化,函数体内是叫赋值!这么也就可以理解,为什么使用初值列比在函数体中“初始化”高效了!而且,对于成员变量是const或者reference的,不能进行复制操作,只能在初值列初始化。

    2. C++有着十分固定的“成员初始化次序”,次序总是相同的:基类早于派生类初始化,类中的成员变量总是以声明的次序被初始化。


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

    1. 如果你自己没有声明,编译器会为一个类声明一个copy构造函数、copy赋值操作符、析构函数,如果你没有声明任何构造函数,编译器会为你声明一个默认构造函数。这些编译器声明的函数都是public inline的。不过,唯有当这些函数被需要,或者说是被调用时,他们才会被编译器创造出来。

    2. 编译器产出的析构函数默认是non-virtual的,除非这个类的基类自身声明有虚析构函数。

    3. 如果你打算在一个内含引用成员的类内支持赋值操作,你必须自己定义一个拷贝赋值操作符。因为,如果你不指定,编译器生成的默认拷贝赋值操作符会导致你的引用被另一个对象所修改。

    请记住:

    • 编译器可以暗自为class创建默认构造函数,copy构造函数,copy赋值操作符,以及析构函数。

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

    请记住:

    • 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。继承Uncopyable这样的基类也是一种做法。

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

    请记住:

    • 带多态性质的基类应该声明一个虚析构函数。如果类带有任何virutal函数,他就应该拥有一个virtual析构函数
    • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不应该声明为virtual析构函数
    1. 对于请记住中的第二点,因为类中存在虚函数就会生成对应的虚函数表,对象实例的时候就会有一个虚函数指针,这个指针是占用内存的,对于一些简单的类,如果有这个虚函数指针,可能导致寄存器不能一次性地取到整个类对象实例内存,所以会影响效率。

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

    请记住

    • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下他们或结束程序
    • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作
    1. 所谓的不吐出异常,指的是,在析构函数中就把该异常给处理掉了。

    2. 为什么不能让异常逃离析构函数?举个例子,对于一个vector的vector,vector负责析构容器中的每个Widget,当析构第一个元素期间抛出异常,析构函数没有处理,第二个元素析构又抛出异常,此时有两个异常,会导致程序不明确的行为。

    条款09:绝不在构造和析构过程中调用virtual函数

    请记住:

    • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类
    1. 无论是在构造还是析构过程,当前对象内并不能看到派生类的信息(构造先构造父类,析构先析构派生类)。

    2. 对于在构造或者析构中间接调用的虚函数危害比较大,因为它通常不会引起任何编译器和连接器抱怨。

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

    请记住:

    • 令赋值操作符返回一个reference to *this
    1. 可以实现x = y = z = 15;这样的语句

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

    请记住:

    • 确保当对象自我赋值时,operator=有良好行为。其中技术包括比较“对象来源”和“目标对象”地址、精心周到的语句顺序、以及copy-and-swap。
    • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确
    1. 自我赋值可能存在的问题是:在停止使用资源之前意外地释放了该资源。

    2. 证同测试

    Widget& Widget::operator=(const Widget& rhs){
    	if(this == rhs) return *this;
    	delete pb;
    	pb = new Bitmap(*rhs.pb);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 复制所指东西之前别删除
    Widget& Widget::operator=(const Widget& rhs){
    	Bitmap* pOrig = pb;
    	pb = new Bitmap(*rhs.pb);
    	delete pOrig;
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. copy-and-swap
    Widget& Widget::operator=( Widget rhs){ // 注意,是值传递
    	Wodget tmp(rhs);
    	swap(tmp);	// 交换rhs和this的数据
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    条款12:复制对象时别忘其每一个成分

    请记住:

    • copy函数应该确保复制“对象内的所有成员变量”及“所有基类成分”
    • 不要尝试以某个copy函数实现另一个copy函数。应该将共同机能放进一个第三方函数中(init),并由两个copy函数共同调用。
    1. 如果你为class添加一个成员变量,你必须同时修改copy函数。

    条款13:以对象管理资源

    请记住:

    • 为防止资源泄露,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源
    • 两个常被使用的RAII类分别是tr1::shared_ptr和auto_ptr。前者通常是最佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null。
    1. 现在auto_ptr已被弃用,取而代之的是unique_ptr。unique_ptr不支持复制、赋值操作,使用move进行所有权转让。

    2. RAII:Resource Acquisition Is Initialization。资源获取即初始化。

    条款14:在资源管理类中小心copy行为

    请记住:

    • 复制RAII对象必须一并复制它所管理的资源,所以资源的copy行为决定RAII对象的copy行为
    • 普通而常见的RAII类copy行为是:抑制copy行为、施行引用计数法。不过其他行为也都可能被实现
    1. 所以,智能指针的使用是看使用场景的:例如我们只希望有一个指针指向一个对象,那么我们可以使用unique_ptr,允许复制就是用shared_ptr

    2. shared_ptr可以指定析构方法,我们在自定义的资源管理类中,可以引入shared_ptr帮我们完成一些任务。

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

    请记住:

    • APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理资源”的方法
    • 对原始资源的访问可能经由显示转化或隐式转换。一般而言,显示转换比较安全,但隐式转换对客户比较方便
    1. 隐式转换函数:如下面的代码,不需要返回类型,当发生隐式类型转换的时候自动调用。
    class Font{
    public:
    	...
    	operator FontHandle() const {
    		return f;
    	}
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    请记住:

    • 如果你在new表达式中使用[],必须在相应的delete表达式中也是用[];如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
    1. 如果你偏不听建议,非得反着写,编译器会给一个warning。

    2. delete最大的问题在于:即将被删除的内存之内究竟存在多少对象。

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

    请记住:

    • 以独立语句将new对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
    1. 如果不这样做,比如func(shared_ptr(new Widget), priority());,在某些编译器下,可能会把,new,priority(),智能指针的构造顺序搞混。当priority()置于其他两者中间时,且发生异常,new动作遗漏指针,发生内存泄漏。

    2. 使用建议中的方案,不会发生重新排列。


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

    请记住:

    • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质,
    • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
    • “阻止误用”的办法包括:建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
    • shared_ptr支持定制删除器。这可防范DLL问题,可被用于自动解除互斥锁

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

    请记住:

    • class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论的问题
    1. 新type的对象应该如何被创建和销毁?构造和析构函数
    2. 对象的初始化和对象的赋值该有什么样的差别?构造和赋值操作符
    3. 新type的对象如何被以值传递?拷贝构造
    4. 什么是新type的“合法值”?
    5. 你的新type需要配合某个继承图系吗?继承方法,virtual
    6. 你的新type需要什么样的转换?是否explicit构造函数,对应的转型操作符重载
    7. 什么样的操作符合函数对此type而言是合理的?
    8. 什么样的标准函数应该驳回?private
    9. 谁该取用新type的成员?访问控制
    10. 什么是新type的“未声明接口”?
    11. 你的新type有多一般化?模板
    12. 你真的需要一个新type吗?

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

    请记住:

    • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
    • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。
    1. pass-by-reference-to-const之所以高效,是因为不需要在传参的时候调用copy构造函数,对于一些类来说,调用copy构造是相当耗时的,不高效的。

    2. 对象小并不意味着它的copy构造函数不昂贵;自定义的小对象,其他小容易发生变化。

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

    请记住:

    • 绝不要返回pointer或reference指向一个local stack对象;或返回reference指向一个heap-allocated对象;或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
    1. 首先,以值传递返回,也会调用一次拷贝构造方法。如果你没有在编译时使用-fno-elide-constructors参数,那么编译器会给你做优化,通过调整栈对象的构造顺序,使得返回值时,可以啥都不用做,直接用函数中构造的变量。
    A funConference(A& a){
        A tmp(a);
        return tmp;
    }
    
    int main(){
        A a;
        A b = funConference(a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 别让operator*返回reference。

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

    请记住:

    • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可以为划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
    • protected并不比public更具封装性

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

    请记住:

    • 宁可拿non-member、non-friend函数替换member函数。这样可以增加封装性、包裹弹性和机能拓展性
    1. non-member、non-friend函数不会增加成员变量数据的访问函数数量,这一点来看,是提升了类的封装性的。

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

    请记住:

    • 如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转化,那么这个函数必须是non-member的
    1. 只有当参数位于参数列表时,这个参数才是隐式类型转换的合格参与者
    2. member函数的反面是non-member,而不是friend函数

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

    请记住:

    • 当std::swap对你的类型效率不高的时候,提供一个swap成员函数,并确定这个函数不抛出异常
    • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者
    • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”
    • 为“用户定义类型”进行std template全特化是好的,但千万别尝试在std内加入某些对std而言全新的东西
    1. 一个pImpl实现手法的类的swap实现:
    class Widget{
    public:
    	...
    	void swap(Widget& other){
    		using std::swap;
    		swap(pImpl, other.pImpl);
    	}
    	...
    }
    
    
    namespace std{
    	template<>
    	void swap<Widget>(Widget&a, Widget&b){
    		a.swap(b);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. C++允许对模板类进行偏特化,不允许对模板函数进行偏特化。

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

    请记住:

    • 尽可能延后变量定义式的出现时间。这样可增加程序的清晰度并改善程序效率
    1. 你不应该只延后变量的定义,指导非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。

    2. 如果一个变量只在循环内使用,那么把它定义于循环外并每次给他赋值比较好,还是把它定义于循环内较好呢?这个问题值得思考。

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

    请记住:

    • 如果可以,尽量避免转型,特别是在注重效率的代码中使用dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

    • 如果转型是必要的,试着将它隐藏域某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。

    • 宁可使用C++-style转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。

    1. 旧式类型转化
    (T) expression
    T(expression)
    
    • 1
    • 2
    1. 新式类型转化
    const_cast<T>(expression)
    dynamic_cast<T>(expression)
    reinterpret_cast<T>(expression)
    static_cast<T>(expression)
    
    • 1
    • 2
    • 3
    • 4
    1. const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++-style转型操作符;reinterpret_cast意图执行低级转型,实际动作由编译器决定,这也就意味着它不可移植,很少使用;static_cast用来强迫隐式转换,一般用在内置类型的一些转换。

    2. dynamic_cast主要用于“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作。通常一个继承体系下,如果有虚函数,一般不用dynamic_cast。一般用在那种不能设置为虚函数的函数调用。dynamic_cast执行速度慢,是因为他会使用RTTI机制,使用type_id找类型,耗时。

    3. 有一种情况:

    virtual void onResize() {
    	static_cast<Window>(*this).onResize();
    }
    
    • 1
    • 2
    • 3

    这段代码,static_cast中传值方法是值传递,值传递肯定要一个copy的动作,所以会产生一个副本,所以,修改这个副本,不会影响this指针。

    使用

    virtual void onResize() {
    	Window::onResize();
    }
    
    • 1
    • 2
    • 3

    消除这个问题。

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

    请记住:

    • 避免返回handles(包括reference、指针、迭代器)指向对象内部成分。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊handles”的可能性将至最低。
    1. 首先,返回handle指向一个对象私有成员变量,那么,可以通过这个handle去修改这个私有成员变量,这就将私有成员变量降级为public了;其次,把函数返回的handle设为const,可能可以缓解上述问题,但是还是会引发虚吊handle的情况(即,临时对象的销毁)。

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

    请记住:

    • 异常安全函数即使发生异常也不会泄露资源或者允许任何数据额结构破坏。这样的函数区分为三种可能得保证:基本型、强烈型、不抛异常型
    • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
    • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者,短板效应
    1. 带有异常安全的函数会:1.不泄露任何资源;2.不允许数据败坏

    2. 异常安全函数提供一下三种保证之一:

      • 基本承诺:如果异常被抛出,程序内的任何事物任然保持在有效(但是无法预期)的状态下
      • 强烈保证:异常被抛出,程序状态不改变,相当于数据库事务的原子性,要么全做完,要么全没做完
      • 不抛出异常:但是并不是说不能抛出异常,一旦抛出异常就是fatal error,程序会crash
    3. copy-and-swap:为你打算修改的对象做出一份副本,然后在副本身上进行一切必要修改,然后将修改后的数据和原件置换。

    条款30:透彻理解inline的里里外外

    请记住;

    • 将大多数inline限制在小型、被频繁调用的函数身上。这可使得日后的调试过程和二进制升级更容易,也可使潜在代码膨胀问题最小化,使程序的速度提升机会最大化
    • 不要只因为function template出现在头文件,就将他声明为inline
    1. 编译器如果要对函数做inline,那么它必须知道inline函数长什么样子。这句话说明,inline得在头文件中,发生在编译期。

    2. 虚函数、构造、析构函数不适合inline。

    3. inline会引发的问题有:代码膨胀,修改inline导致全重编译。

    4. 记住中第二点说:不要只因为function template出现在头文件,就将他声明为inline。因为,template本身就会代码膨胀,inline也会导致这个问题,如果两个在一起,那就是膨胀+膨胀。

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

    请记住:

    • 支持”编译依存性最小化“的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle class和Interface class
    • 程序库头文件应该以”完全且仅有声明式“的形式存在。这种做法不论是否涉及template都适用
    1. pimpl手法:在一个类中存放具体实现类的指针。这样,指针大小在32位系统上固定是4字节,无需知道具体实现类的大小。这解决了”接口与实现的分离“。是一种Handle class。

    2. 降低编译依存关系:

      • 如果使用object reference或object pointers可以完成任务,就不要使用objects
      • 如果能够,尽量以class声明替换class定义
      • 为声明式和定义式提供不同的头文件
    3. 抽象基类是interface class。

    4. Handle class和Interface class可降低编译依存性。


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

    请记住:

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

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

    请记住:

    • derived class内的名称会遮掩base class内的名称(一般是一个类中同名函数多个重载的情况)。在public继承下从来没有人希望如此。
    • 为了让被遮掩的名称重见天日,可使用using声明式转交函数(有一点理解,using)。
    1. 如果只想继承父类的部分函数,就可以使用private继承,然后设置一个转交函数,转交函数中使用父类作用域调用父类函数。

    2. 条款中,父类有多个重载函数的的情况,不论是虚函数还是非虚函数,都会发生遮掩。

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

    请记住:

    • 接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口。
    • pure virtual函数只具体指定接口继承
    • 普通的虚函数具体指定接口继承及缺省实现继承
    • 非虚函数具体制定接口继承以及强制性实现继承
    1. 纯虚函数一般没有定义,但是仅仅是一般,你想要定义他,也是可以的。

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

    请记住:

    • virtual函数的替代方案包括NVI手法以及Strategy设计模式的多种形式。NVI手法自身是一种特殊形式的Template Method设计模式
    • 将机能从成员函数移动到class外部函数,带来一个缺点是,非成员函数无法访问class的non-public成员
    • tr1::function对象的行为就像一般的函数指针。这样的对象可接纳”与给定之目标签名式兼容“的所有可调用之物。
    1. 所以,替换虚函数的方案有:

      • 非虚函数调用私有虚函数,这是NVI手法,模板方法
      • 使用函数指针成员变量,这是策略设计模式的方法
      • 使用tr1::function替换虚函数
    2. NVI手法:non-virtual interface

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

    请记住:

    • 绝不重新定义继承而来的non-virtual函数
    1. 为什么?因为,重新定义non-virtual函数会打破is-a的关系。

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

    请记住:

    • 绝不重新定义继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-你唯一应该覆写的东西-却是动态绑定
    1. 为什么C++要这么做?因为,如果把缺省值也动态绑定,编译器要想出办法在运行期为virtual函数给出适当的缺省值,这是不高效的。所以,是因为效率,才让C++这么做的。

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

    请记住:

    • 复合的意义和public继承完全不同
    • 在应用领域,复合意味着has-a。在实现领域,复合意味着is-implemented-terms-of(根据某物实现出)
    1. 复合有一些同义词:分层、内含、聚合、内嵌

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

    请记住:

    • private继承意味着implemented-in-terms-of(根据某类实现出)。他通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

    • 和复合不同,private继承可以造成empty base最优化(EBO,空白基类最优化)。这对致力于”对象尺寸最小化“的程序库开发人员而言,可能很重要。

    1. 总的来说,private继承,可以理解为has-a的关系。但这么说不全对。

    2. 如果class之间的继承关系是private,编译器不会自动将一个derived class对象转化为一个base class对象。

    3. 如果你让D继承B,你的用意应该是:为了采用B内已经备妥的某些特征,不是因为B对象和D对象有任何观念上的关系。

    4. private继承在软件设计层面上没有任何意义,其意义只在于软件实现层面。

    5. 继承空白基类的情况有:NoCopyAble、unary_function、binary_function

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

    请记住:

    • 多重继承比单一继承复杂。他可能导致新的歧义性,以及对virtual继承的需要
    • virtual继承会增加大小、速度、初始化以及赋值复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况
    • 多重继承的确有正当用途。其中一个情节涉及public继承某个Interface class和private继承某个协助实现的class的两相结合。
    1. 之所以用virtual继承,是因为在菱形继承体系中,基类的一个变量可能会导致孙类里面有两个副本,这就乱套了。

    2. 虚继承的原理:实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。


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

    请记住:

    • class和template都支持接口和多态
    • 对class而言,接口是显示的,以函数签名为中心。多态则是通过virtual函数发生于运行期
    • 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
    1. 对函数重载解析的一点理解:具现化一个模板函数,那么就是给T指定了类型,根据C++的编译法则,这个函数后面会跟上这个类型来描述这个函数名,这会引发多态。

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

    请记住:

    • 声明template参数时,前缀关键字class和typename可互换
    • 请使用关键字typename标识嵌套从属类型名称;但不得在base class list或member initialization list内以他作为base class的修饰符
    1. typename必须作为嵌套从属类型名称的前缀词

    2. C++有个规则:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是一个类型。所以,得使用typename

    3. 从属名称、嵌套从属名称、非从属名称

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

    请记住:

    • 可在继承模板类通过this->指涉模板基类内的成员名称,或藉由一个明白写出的”基类资格修饰符“完成
    1. 所谓基类资格修饰符:using、作用域说明符::

    2. 这种情况:继承模板类调用模板父类的函数,如果像普通继承体系下那么调用,编译器是找不到父类的这个被调用函数的。这种情况出现的原因是:当编译器看到继承模板类时,模板父类并没有被具现化,那么编译器就不知道父类看起来像什么,也就没办法找到父类的成员函数。

    3. 模板全特化:类型参数被全部定义,没有其他模板参数可定义

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

    请记住:

    • Template生成多个class和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生相依关系(比如说非类型参数)
    • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
    • 因参数类型造成的代码膨胀,往往可降低,做法是带有完全相同的二进制表述的具现类型共享实现码(比如说类型int和类型long作为模板类的类型时,应该共享代码)
    1. 在模板类具现化的时候,如果类中一个成员函数没有被调用,那么这个成员函数在具现化的时候,在类中只有声明,没有定义。
      在这里插入图片描述

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

    请记住:

    • 请使用成员函数模板生成”可接受所有兼容类型“的函数
    • 如果你生命成员函数模板用于”泛化copy构造“或”泛化assignment“操作,你还是要声明正常的copy构造函数和copy assignment操作符。
    1. 一般成员模板函数用于copy构造的时候,都要声明为non-explicit,因为需要允许隐式转换的功能。

    2. 对于第二点,如果是泛化的copy构造,没有具现化这个构造函数,那么模板类中就没有拷贝构造函数,那就会生成默认的拷贝构造函数,但是这个可能不是我们想要的,所以需要自己定义一个非泛化的拷贝构造。

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

    请记住:

    • 当我们编写一个class template,而它所提供的”与此template“相关的函数支持”所有参数之隐式类型转换“时,请将那些函数定义为class template内部的friend函数
    1. 为什么?因为在template实参推导过程中,从不将隐式类型转换函数纳入考虑。意思是,你想要在一个地方隐式类型转换,但是template没意识到这个事。

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

    请记住:

    • traits calss使得类型相关信息在编译期可用。他们以template和template特化完成实现
    • 整合重载技术后,tarits class有可能在编译期对类型执行if…else测试
    1. 对于第二点,运行期的工作,通过模板转交到了编译期,感觉这种设计很优雅

    2. 五种类型的迭代器:

      • Input迭代器:只能一步一步向前移动,只读
      • Output迭代器:只能一步一步向前移动,只写
      • Forward迭代器:只能一步一步向前移动,可读可写
      • Bidirectional迭代器:双向移动
      • Random Access迭代器:双向随机跳跃

    条款48:认识template元编程

    请记住:

    • TMP(template metaprogramming)模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦察和更高的执行效率
    • TMP可被用来生成”基于政策选择组合“的客户定制代码,可用来避免生成对某些特殊类型并不适合的代码(没太懂这个
    1. 先感叹一句,太特么牛逼了!

    2. TMP是被发现的,而不是被发明的。

    3. TMP优点:1.他让某些事情更容易;2.将工作从运行期转到编译期(a。错误能在编译期侦察;b。更高效);3.较小的可执行文件,较短的运行期,较少的内存需求;TMP缺点:编译时间变长,代码膨胀

    4. TMP已被证明是个”图灵完全“机器,意思是它的威力大到足以计算任何事物

    5. TMP的体现:上一个条款的编译期if…else预测,矩阵乘法优化(不会产生临时变量)

    6. 其他的,有点没理解透,还需要进一步哟~


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

    请记住:

    • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
    • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;猴急的构造函数调用还是可能抛出异常的
    1. new-handler是分配内存异常的处理函数,set-new-handler是设置这个处理函数的函数

    2. new-handler必须做以下事情:

      • 让更多的内存可被使用
      • 安装另一个new-handler
      • 卸除new-handler
      • 抛出bad_alloc
      • 不返回

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

    请记住:

    • 有许多理由需要些个自定的new和delete,包括改善效能、对heap运用错误进行调试,收集heap使用信息
    1. 总的来说,这个条款讲的是,何时重载operator new()和operator delete()
      • 为了检测运用错误:比如delete时失败,抛异常
      • 为了收集动态分配内存之使用统计信息:比如想知道已经分配了多少内存
      • 为了增加分配和回收速度:默认的分配方案可能是中庸的
      • 为了降低缺省内存管理器带来的空间额外开销
      • 为了弥补缺省分配器中的非最佳齐位:相当于是自己定义内存对齐位
      • 为了将相关对象成簇集中:减少缺页中断次数
      • 为了获得非传统的行为:即自定义行为

    条款51:编写new和delete时需固守常规

    请记住:

    • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果他无法满足内存需求,就该调用new-handler。它也应该有能力处理0-byte申请。class专属版本则还应该处理”比正确大小更大的错误申请“
    • operator delete应该收到null指针时不做任何事情。class专属版本则还应该处理”比正确大小更大的错误申请“
    1. 第一条的伪代码:
    void* operator new(size_t size) throw(bad_alloc){
    	using namespace std;
    	if(size != sizeof(BaseClass)){	//这其实包含了size=0的情况
    		return ::operator new(size);
    	}
    	while(true){
    		尝试分配size byte;
    		if(分配成功)
    		return (一个指针,指向分配得来的内存)
    
    		// 分配失败;找到目前的new-handler函数
    		new_handler globalHandler = set_new_handler(0)// 因为无法直接获取new_handler
    		set_new_handler(globalHandler);
    
    		if(globalHandler) (*globalHandler)();
    		else throw std::bad_alloc();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    请记住:

    • 写了placement new也要写placement delete。如果没有写,你的程序可能会发生隐微的时断时续的内存泄漏
    • 当你声明placement new和placement delete,请确保不要无意识地掩盖了他们的正常版本
    1. 对于第一点,placement new对应的placement delete版本通常(不是通常,是一定)在placement new发生错误的时候才会被调用。正常的delete是会调用常规的delete版本。如果不提供placement new对应版本的placement delete,那万一在对象构造函数调用的时候失败了,就无法去释放operator new申请的空间,这就内存泄漏了。

    2. C++在global作用域内提供以下形式的operator new:

    void* operator new(size_t) throw(bad_alloc);	// normal new
    void* operator new(size_t, void*) throw();		// placement new
    void* operator new(size_t, nothrow_t&) throw();	// nothrow new
    
    • 1
    • 2
    • 3

    所以,针对第二点,要避免无意识地覆盖全局operator new和operator delete就要在类中声明定义对应对operator new和operator delete来分别调用全局的operator new和operator delete

    1. 所谓placement new,就是携带额外参数的operator new

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

    请记住:

    • 严肃对待编译器发出的警告信息。努力在你的编译器的最高警告级别下争取”无任何警告“的荣誉
    • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。

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

    请记住:

    • C++标准程序库的主要机能由STL、iostreams、locales组成
    • TR1添加了智能指针一般化函数指针(function)、bind、hash-table容器、正则表达式等
    • TR1自身只是一份规范,为了获得TR1提供的好处,你需要一份实物。一个好的实物来源是Boost
    1. 一般化函数指针比函数指针更富弹性,他能接受任意可调用物,函数指针只能指函数

    条款55:让自己熟悉Boost

    请记住:

    • Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序开发库。Boost在C++标准化过程中扮演深具影响力的角色。
    • Boost提供许多TR1组件实现品,以及其他许多库。
  • 相关阅读:
    TypeScript9
    5道面试题,拿捏String底层原理!
    【JavaEE进阶序列 | 从小白到工程师】JavaEE中的二维数组详细介绍与应用
    Mybatis01
    基于Java的在线考试系统(附:源码和课件)
    Netty系列(四):源码解读 backlog 参数作用
    LeetCode 3. 无重复字符的最长子串
    靶向多肽cRGD/血管肽Angiopep/细胞穿膜肽-SiO2复合物(TAT-SiO2)
    STM32F407串口助手无法识别到串口
    构建Springboot项目docker容器 时区的设置
  • 原文地址:https://blog.csdn.net/weixin_42065178/article/details/126414866