美文网首页记录C++的学习
《Effevtive c++ 》学习笔记

《Effevtive c++ 》学习笔记

作者: 嵌入式魔法师 | 来源:发表于2020-02-15 13:09 被阅读0次

    《Effective c++》 笔记

    \color{blue}{1 让自己习惯c++}

    \color{Fuchsia}{条款01:视c++为一个语言联邦}

    • c:说到底c++仍是以c为基础
    • object-Oriented C++。 这部分也是C with Classed所诉求的。
    • Template C++。这是c++的泛型编程部分,也是大多数程序员经验最少的部分。
    • STL。STL是个template程序库。

    \color{Fuchsia}{条款02:尽量以const,enum,inline替换\#define}

    \color{red}{请记住: }

    • 对于对于单纯常量,最好以const对象或enums替换#defines。
    • 对于形似函数的宏(macros),最好改用inline函数替换#define。

    \color{Fuchsia}{条款03:尽可能使用const}

    const成员函数

    • const成员函数不可以更改对象内任何non-static成员变量
    • 关键字<font color=blue>mutable</font>可以释放掉成员变量的bitwise constness约束;既此时const成员函数可以更改non-static成员变量。

    代码:

    
        class CTextBlock
        {
           public:
                  ...
                std::size_t length() const;
           private:
                 char*pText;
                 mutable std::size_t textLength;      //这些成员变量可能总是
                 mutable bool lengthIsValid;          //会被改变,即使在
        };                                            //cosnt 成员函数内。
        std::size_t CTextBlock::length() const
        {
            if(!lengthIsValid){
            textLength=std::strlen(pText);            //现在,可以这样,
            lengthIsValid=true;                       //也可以这样。
            }
            return textLength;
        }
    
    

    const和non-cosnt成员函数中避免重复

    • 当类中有函数重载,重载仅仅是const的区别那么会导致代码的大量的重复此时促使我们将常量性转移(casting away constness)

    代码

    
        class TextBlock
        {
            public:
                    ...
            const char& operator[](std::size_t postion) const//一如即往
            {
                ...
                ...
                ...
                return text[postion];
            }
                char& operatot[](std::size_t postion)//现在只调用const op[]
            {
                return 
                    cosnt cast<char&>(
                        static_cast<const TextBlock&>(*this)[postion]
                );
            }
            ...
        };
    
    

    \color{Plum}{代码解析:}

    如你所见,这份代码有两个转型动作,而不是一个。我们打算让non-cosnt operator[]调用其const兄弟,但non-const operator[]内部若只是单纯的调用operator[],会递归调用自己。那会大概......悟......进行一百万次。为了避免无穷递归,我们必须明确指出调用的是const operator[],但c++缺乏直接的语法可以那么做。因此这里将*this从其原始类型TextBlock&转型为const TextBlock。是的,我么使用转型操作符为它加上const!所以这里共有两次转型:第一次用来为*this添加const(这使接下来调用operator[]时得以调用const版本),第二次是从const operator[]的返回值中移除const。

    \color{red}{请记住:}

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

    \color{Fuchsia}{条款04:确定对象使用前已被初始化}

    \color{red}{请记住:}

    • 为内置型对象进行手工初始化,因为c++不能保证初始化他们。
    • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
    • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

    \color{green}{名词解释:}

    • local static对象:函数内的static对象。
    • non-local static对象:其他static对象。
    • c++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。

    \color{blue}{2 构造/析构/赋值运算}

    \color{Fuchsia}{条款05:了解C++默默编写并调用那些函数}

    \color{red}{请记住:}

    • 编译器可以暗自为class创建default构造函数copy构造函数copy assignment操作符,以及析构函数

    \color{red}{有些情况编译器拒绝生成以上函数:}

    1. 如果你打算在一个"内含reference成员(引用成员)"的class内支持赋值操作,你必须自己定义copy assignment操作符。
    2. 面都"内含const成员的classes,编译器的反应也一样,更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。
    3. 如果某个base classes 将copy assignemt操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

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

    \color{red}{请记住:}

    • 为驳回编译器(暗自)提供的机能,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class 也是一种做法。

    代码

    
        class Uncopyable
        {
         protected:
                Uncopyable() {};
                ~Uncopyable() {};
         private:
                Uncopyable(const Uncopyable&);
                Uncopyable& operator=(const Uncopyable&);
        };
        为阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
        class HomeForSale:private Uncopyable{
         ...
        }
    
    

    \color{Fuchsia}{条款07:为多态基类声明virtual析构函数}

    \color{red}{请记住:}

    • polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
    • Classes 的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明vitrual析构函数。

    \color{Fuchsia}{条款08:别让异常逃离析构函数}

    \color{red}{请记住: }

    • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

    代码

    
        //如果close抛出异常就结束程序。通常通过调用abort完成:
        DBConn::~DBConn()
        {
         try{db.close(); }
         catch(...){
            制作运转记录,记下对close的调用失败;
            std::abort();
         }
        }
        //吞下因调用close而发生的异常:
        DBConn::~DBConn()
        {
         try{db.close();}
         catch(...){
            制作运转记录,记下对close的调用失败;
         }
        }
    
    
    • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

    代码

    
        class DBConn{
        public:
        ...
        void close()
        {
         db.close();                             //供客户使用的新函数
         closed=true;
        }
        ~DBConn()
        {
         if(!closed){
            try
            {
                db.close();                      //关闭连接(如果客户不那么做的话)
            }
            catch(...){
                制作运转记录,记下对close的调用失败;//记录下来并结束程序
                ...                              //或吞下异常
            }
         }
        }
        }
    
    

    \color{Fuchsia}{条款09:绝不在构造和析构过程中调用virtual函数}

    \color{red}{请记住:}

    • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

    \color{Plum}{解决方案:}

    (P51)一种做法是在class Transaction 内将 logTransaction 函数改为non-virtual,然后要求derived class 构造函数传递必要信息给Transaction 构造函数,而后那个构造函数便可以安全的调用 non-virtual logTransaction。


    \color{Fuchsia}{条款10:令operator= 返回一个reference\ to *this}

    \color{red}{请记住: }

    • 令赋值(assignment)操作符返回一个reference to *this。

    代码

    
        class Wideget{
         public:
         ...
         Widget& operator=(const Widget &ths)
         {
            ...
            return* this;
         }
         ...
        };
    
    

    \color{Fuchsia}{条款11:operator= 中处理"自我赋值"}

    \color{red}{请记住: }

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

    \color{Fuchsia}{条款12:复制对象时勿忘其每一个成分}

    \color{red}{请记住: }

    • Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
    • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

    \color{blue}{3 资源管理}

    \color{Fuchsia}{条款13:以对象管理资源}

    • 重要思想:\color{DodgerBlue}{“以对象管理资源”}
    • 智能指针:auto_ptr和shared_ptr均为智能指针,可以自动管理new 分配的内存。但两者有区别(自行百度)。

    \color{red}{请记住: }

    • 为防止资源泄露,请使用RAII(“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机” Resource Acquisition Is Initialization; RAII)对象->(我将其称为资源管理类对象)。
    • 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

    \color{Fuchsia}{条款14:在资源管理类中小心*copying*行为}

    \color{green}{提问:}当一个RAII对象被复制,会发生什么事?大多数时候会选择一下几种可能:

    • 禁止复制。许多时候允许RAII对象被复制并不合理。对一个像lock这样的class这是有可能的,因为很少能够合理拥有“同步化基础器物(synchronization primitives)”的复件(副本)。如果复制动作对RAII class并不合理,你便应该禁止之。条款6告诉你怎么做:将copying操作声明为private。对lock而言看起来是这样的:

    代码

    
        class Lock:private Uncopyable{
        public:
            ...
        }
    
    
    • 对底层资源祭出“引用计数法”(reference-count)。 有时候我们希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将资源的“被引用次数”递增。tr1::shared_ptr便是如此。 通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可实现出reference-counting copying 行为。如前述的Lock打算使用reference counting,它可以改变mutexPtr的类型,将它从Mutex*改为tr1::shared_ptr<Mutex>。然而很不幸tr1::shared_ptr的缺省行为是“当引用次数为0时删除其所指物”,那不是我们所要的行为。当我们用上一个Mutex,我们想要的释放动作是解除锁定而非删除。 幸运的是tr1::shared_ptr允许指定所谓的“删除器”(deleter),那是一个函数或函数对象(function object),当引用次数为0时便被调用(此机能并不存在于auto_ptr——它总是将其指针删除)。删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来像这样:

    代码

    
        class Lock{
        public:
         explicit Lock(Mutex* pm)               //以某个Mutex初始化                     
          : mutexPtr(pm,unlock)                 //并以unlock函数为删除器
         {
            lock(mutexPtr.get());               //条款15谈到"get"
         }
        private:
         std::tr1::shared_ptr<Mutex> mutexPtr;  //使用shared_ptr
        }                                           //替换raw pointer
    
    

    \color{red}{注意:} 本例中Lock class不再声明析构函数。因为没有必要,条款5说过,class析构函数(无论是编译器生成的,还是用户自定的)会自动调用其non-static成员变量(本例为mutexPtr)的析构函数。

    • 复制底部资源。有时候只要你喜欢,可以针对一分资源拥有其任意数量的复件(副本)。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包含的资源。也就是说,复制资源管理对象时,进行的是“深度拷贝”。
    • 转移底部资源的所有权。 某些罕见的场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源(new resource),即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。一如条款13所述,这是auto_ptr奉行的复制意义。

    \color{red}{请记住: }

    • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
    • 普遍而常见的RAII class copying行为是:抑制copying,施行引用计数法(reference counting)。不过其他行为也都可能被实现。

    \color{Fuchsia}{条款15:在资源管理类中提供对原始资源的访问}

    \color{red}{请记住: }

    • APIs往往要求访问原始(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的方法。
    • 对原始资源的访问可能经由显示转换和隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

    \color{green}{名词解释:}

    • 显示转换:RAII class(资源管理类)应该提供一个类似auto_ptr类中的get()方法一样的函数,用于取得RAII class管理的资源。
    • 隐式转化:将RAII class隐式转换为其管理的资源的类型。代码实现需要运算符重载。

    代码

    
        //给类添加隐式转换的代码
        class Font{
        public:
           ...
           operator double()const{return f;}//将类隐式的转换为double类型
           ...
        };
        eg.Font ft(); double ftt=ft;
    
    

    \color{Fuchsia}{条款16:成对使用new和delete时要采取相同形式}

    \color{red}{请记住: }

    • 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

    \color{Fuchsia}{条款17:以独立语句将newed对象置入智能指针}

    \color{red}{请记住: }

    • 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。(详细原因见书p76)

    代码

    
        std::tr1::shared_ptr<widget> pw(new Widget);    //在单独语句内以智能指针存储newed所得对象
        processWidget(pw,priority());                   //这个调用动作绝不至于造成泄露
        tr1::shared_ptr的构造函数是一个explicit构造函数
    
    

    \color{blue}{4 设计与声明}

    \color{Fuchsia}{条款18:让接口容易被正确使用,不易被误用}

    • 假设为一个用来表现日期的class设计构造函数:
    
        class Date {
        public:
         Date(int month,int day,int year);
         ...
        }
    
    

    这段代码乍见之下同情达理,但它的客户很容易犯下至少两个错误:

    1. 他们也许会以错误的次序传参数:
      • Date d(30,3,1995); //哇哦!应该是"3,30"而不是"30,3"
    2. 他们可能传递一个无效的月份或天数:
      • Date d(2,30,1995); //哇哦!应该是"3,30"而不是"2,30"

    解决方案:

    
        class Date{
        public:
         Date(const Month& m,const Day& d,const Year& y);
         ...
        }
    
    

    令Day,Month,和Year成为成熟且经充分锻炼的classes并封装数据。

    • 一旦正确的类型就定位,限制其值有时候是通情达理的。例如一年只有12个有效月份,所以Month应该反映这一事实。办法之一是利用enum表现月份,但enmus不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用,比较安全的解法是预先定义所有有效的Months:
    
        class Month{
        public:
         static Month Jan() {return Month(1);}      //函数,返回有效月份。
         static Month Feb() {return Month(2);}      //稍后解释为什么。
         ...                                            //这些是函数而非对象
         static Month Dec() {return Month(12);}     
         ...                                            //其他成员函数
        private:
         explicit Month(int m);                     //阻止生成新的月份(构造函数声明为private)
         ...                                            //这是月份专属数据
        };
    
        Date d(Month::Mar(),Day(30),Year(1995));
    
    

    如果“以函数替换对象,表现某个特定的月份”让你觉的诡异,或许是因为你忘记了non-locas static对象的初始化次序有可能出问题。建议阅读一下条款四。

    • 预防客户错误的另一个方法,限制类型内什么事可做,什么事不能做。常见的限制是加上const。例如条款3曾经说明为什么“以const修饰operator*的返回类型”可阻止客户因“用户自定义类型”而犯错:if(a*b=c)//哦,愿意其实是要做比较动作!

    • 条款13表明客户如何将createInvestment的返回值存储于一个智能指针如auto_ptr或tr1::shared_ptr内,因而将delete责任推给智能指针。但万一客户忘记使用智能指针怎么办?许多时候,较佳接口的设计原则是先发制人,就如factory函数返回一个智能指针:std::tr1::shared_ptr<Investment> crateIvestment();(shared_ptr可以自己指定“删除器”,它的构造函数的第二个参数)

    • tr1::shared_ptr有一个特别好的特性是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的"cross-DLL-problem"。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。

    \color{red}{请记住:}

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

    \color{Fuchsia}{条款19:设计class犹如设计type}

    设计class就是设计一个新的type(类型),设计class时应该考虑以下的问题:(P84)

    • 新的type的对象应该如何被创建和销毁?
    • 对象的初始化和对象的赋值有什么样的差别?
    • 新type的对象如果被passed by value(以值传递),意味着什么?
    • 什么是新type的“合法值”?
    • 你的新type需要配合某个继承图系(inheritance graph)吗?
    • 你的新type需要什么样的转换?
    • 什么样的操作符和函数对新type而言是合理的?
    • 什么样的标准函数应该驳回?
    • 谁该取用新type的成员?
    • 什么是新type的"未声明接口"(undeclared interface)?
    • 你的新type有多么一般化?
    • 你真的需要一个新type吗?

    \color{red}{请记住:}

    • class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条覆盖的所有讨论主题。

    \color{Fuchsia}{条款20:宁以pass-by-reference-to-const替换pass-by-value}

    • 采用pass by reference to const的效率高。
    • 以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。这实在不怎么让人惊讶,因为正是base class构造函数建立了它。
    • pass by reference通常意味着真正传递的是指针。因此如果你有一个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高。对内置类型而言,当你有机会选择采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。
    • “小型的用户自定义类型不必然成为pass-by-value优良候选人”的另一个理由是,作为一个用户自定义类型,其大小容易有所变化。
    • 一般而言,你可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL 的迭代器和函数对象。

    \color{red}{请记住:}

    • 尽量以pass-by-reference-to-const 替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
    • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

    \color{Fuchsia}{条款21:必须返回对象时,别妄想返回其reference}

    \color{red}{请记住:}

    • 绝不要返回pointerreference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointerreference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

    \color{Fuchsia}{条款22:将成员变量声明为private<}

    \color{red}{请记住:}

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

    \color{Fuchsia}{条款23:宁以non-member,non-friend,替换member函数}

    • 类的封装性:能后访问class内之private成分的函数数量。
    • 如果你要在memeber和一个non-memeber,non-friend函数之间做抉择,而且两者提供相同机能,那么,导致较大封装性的是non-memeber non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量。
    • 有两点值得注意:
      1. 这个论述只适用于non-member non-friend函数。friends函数对class private成员的访问权力和member函数相同,因此两者对封装的冲击力道也是相同的。从封装的角度看,这里选择的关键并不在memeber和non-member函数之间,而是在member和non-member non-friend函数之间。
      2. 封装性让函数“成为class的non-member”,并不意味着它“不可以是另一个class的memebr”。例如我们可以有一个工具类,可以让non-member non-friend函数成为这个工具类的static member函数。

    \color{red}{请记住:}

    • 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性,包裹弹性和机能扩充性

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

    • 不能够只因为函数不该成为memeber,就自动让它成为friend;

    \color{red}{请记住:}

    *如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。


    \color{Fuchsia}{条款25:考虑写出一个不抛异常的swap函数}

    • 一般而言,重载function templates没有问题,但std是一个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的templates,但不可以添加新的templates(或classes或functions或其他任何东西)到std里头。std的内容完全有c++标准委员会决定。
    • 如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。
    • 如果swap缺省实现版的效率不足(那几乎总是意味你的class或template使用了某种pimpl手法),试着做以下事情:
      1. 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。这个函数绝不该抛出异常。
      2. 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
        3.如果你正在编写一个class(而非class template),为你的class特化std::swap,并令它调用你的swap成员函数。
      3. 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。

    \color{red}{请记住:}

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

    相关文章

      网友评论

        本文标题:《Effevtive c++ 》学习笔记

        本文链接:https://www.haomeiwen.com/subject/gqpefhtx.html