美文网首页C++
Effective C++

Effective C++

作者: Tommmmm | 来源:发表于2018-12-05 11:14 被阅读34次

    Effective C++是世界顶级C++大师Scott Meyers的成名之作,初版于1991年。在国际上,这本书所引起的反响之大,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍的推荐名单上,这部专著都会位于前三名。

    该篇为我在实习期间学习《Effective C++》的一些整理,会每一两天定期更新。

    insert new line 代码的自动补全问题
    Ctrl + . : 参数提示
    Re-Indent 格式化代码

    零、术语

    第0章代码及注释:

    
    #ifndef shuyu_h
    #define shuyu_h
    
    
    #endif /* shuyu_h */
    
    typedef int NUM[100];//声明NUM为整数数组类型,可以包含100个元素
    NUM n;//定义n为包含100个整数元素的数组,n就是数组名
    
    
    typedef struct  //在struct之前用了关键字typedef,表示是声明新类型名
    {
        int month;
        int day;
        int year;
    } TIME; //TIME是新类型名,但不是新类型,也不是结构体变量名
    
    #include <iostream>
    
    class Shuyu{
        
    public:
        Shuyu();  //default 构造函数
    
        //    explicit可以抑制内置类型隐式转换,
        //    所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.
        //    显示转换如 new Shuyu(10);
        explicit Shuyu(int number);
        
        //copy构造函数
        Shuyu(const Shuyu &copy);
        
        /*
         如果 Shuyu s1 = s2; (s2 已经被定义)
         那么这个时候会有构造函数被调用,而不是赋值操作
         
         如果是单纯的 s1 = s2 那么此时为赋值操作
         */
        Shuyu& operator=(const Shuyu& copy);
        /*
         运算符重载的部分说明:
         加const是因为:
         ①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
         ②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
         
         用引用是因为:
         这样可以避免在函数调用时对实参的一次拷贝,提高了效率。
         copy构造函数是一个比较重要的函数,因为它定义了一个对象如何Pass By Value  即值传递
         */  
    };
    

    explicit可以抑制内置类型隐式转换,
    所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.

    1、typedef:为一种数据类型定义一个新名字。
    在平台一上使用typedef long double REAL;,平台二如果不支持Long Double类型,就改为typedef float REAL,这样在别的用到REAL的地方就不需要修改了。

    • 理解复制声明的技巧:从变量名看起,先往右,再往左
    int (*func[5])(int *);
    

    func 右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针(注意这里的 * 不是修饰func,而是修饰 func[5]的,原因是[]运算符优先级比 * 高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指 针,它指向的函数具有int * 类型的形参,返回值类型为int。

    • const char * p 的意思是p指向的目标空间的内容不可变化
      char * const p 的意思是指针p的值不可变,但它指向目标的值可变。

    2、#define
    #define 指令将标识符定义一个程序在编译的时候会将相同的字符进行替换,也不作正确性检查,当替换列表中含有多个字符的时候,最好的将替换列表用圆括号括起来。宏定义不是说明或者语句,在行末尾不必添加分号

    • 在大规模的开发过程中,特别是跨平台和系统的软件里,define最重要的功能是条件编译。
    #ifdef  windows
    ...
    #else
    ...
    #endif
    #ifdef debug
    ...
    ...
    #endif
    
    • define与Typedef的区别
      #define是预处理指令,在编译预处理时进行简单的替换,不作正确性检查;
      而typedef是在编译时处理的,它在自己的作用域内给一个已经存在的类型一个别名,

    本书启示一:避免不明确(未定义)行为。

    其它补充:
    char name[] = " hello"
    注意name数组大小为6,别忘了最后的null


    一、联邦语言C++

    C++是一种支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。

    • a、C:C++是以C为基础的,block、预处理器、数组、指针等都来自于C

    • b 面向对象 :封装、继承、多态、封装

    • c Template

    -d STL:STL是整个Template程序库

    第一章练习代码:
    .h文件:

    /*
     #define 不被视为语言的一部分  尽量不要用
     #define 不能提供任何的封装性 即不存在 private #define 一类的东西
     */
    //大写名称通常用于宏
    const double Ratio = 1.65;
    
    //由于常量通常在头文件内部(会被不同的源码调用) 因此需要将指针声明为const
    const char* const authorName = "wushuohan";
    
    #include <iostream>
    
    
    class GamePlayer{
    private:
        
        //    头文件内常量声明
        static const double Ratio;
        
        //a=1是一个声明式定义
        /*
         通常C++需要一个定义式
         但是如果这个该常量既是static又是整s数类型 可以忽略
         */
        static const int a = 1;
        
        std::string name;
        std::string age;
        
    public:
        /*
         const在*左边,被指物是常量
         const在*右边,指针是常量
         */
        void func1(const int * a);
        void func2(int const* a );
        
        //赋值说明  见函数的实现
        GamePlayer(const std::string &name);
        
        //构造函数的最佳写法
        GamePlayer(const std::string &name, const std::string &age);
        
        /*
         尽量用local-static代替non-local static
         构造顺序之Non-local static
         
         函数内的static对象为local-static对象 其余均为non-local
         static对象的析构函数会在main()方法结束时自动调用
         
         C++对non-local static的构造顺序没有规定,
         如有需要,可以把他们搬到自己的专属函数内,在函数内部是static,用函数返回一个reference;
         函数内static对象会在函数被调用期间、首次遇上该对象定义时被初始化。
         */
        int test(){
            static int a = 6;
            return  a;
        }
        
        
    };
    
    
    /*
     取一个const的地址是合法的
     但取enum的地址是非法的  指针指不到
     
     单纯对于常量  尽量用 const  enum 替换define 可以h降低对预处理器的需求
     对于函数形式的宏,用inline函数来替换
     */
    
    template<typename T>
    inline void callWithMax(const T& a , const T& b){
        f(a>b?a:b);
    }
    
    /*
     内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。
     
     为什么inline能取代宏?
     1、 inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
     2、 类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查。这样就消除了它的隐患和局限性。
     3、 inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。
     
     
     宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的
     */
    
    
    
    
    class textBlock{
    public:
        //成员函数length()不该动对象内的任何一个Bit
        std::size_t length() const;
        //当const和Non-const有着相同的实现时,让non-const调用const
        
    private:
        
        //mutable定义的成员变量总是可能会更改,即使是在Const函数中
        mutable bool lengthIsValid;
        mutable std::size_t textLength;
    };
    

    内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。

    为什么inline能取代宏?
    1、 inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高
    2、 类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查。这样就消除了它的隐患和局限性。
    3、 inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。

    .cpp文件

    #include "part1.h"
    #include <iostream>
    
    //实现文件常量定义
    const double GamePlayer::Ratio = 1.5;
    
    GamePlayer::GamePlayer(const std::string &name){
        //注意:这里是赋值  不是对Name的初始化
        //初始化应该在进入构造函数之前就发生了
        (*this).name = name;
    }
    
    //构造函数的最佳写法 这样 构造函数不需要执行赋值操作
    //先设新值再赋值太浪费了
    /*注意:C++两个成员的初始化顺序也有差异  先base 再derived
     这里先name,再age
     */
    
    GamePlayer::GamePlayer(const std::string &name,
                           const std::string &age):name(name),age(age){}
    
    std::size_t textBlock::length()const{
        //具体略
        return NULL;
    }
    

    二、构造/析构/赋值运算

    2.1、构造

    编译器会自动为一个类声明一个copy构造函数、一个copy assignment操作符、一个析构函数。如果没有声明任何构造函数,那么编译器会声明一个default 构造函数

    注意:C++ 不允许让reference改指向不同的对象
    本章练习代码

    #include <iostream>
    
    
    template <class T>
    class NamedObject{
    public:
        NamedObject(std::string &name,const T & value);
        
        
    private:
        
        //注意:C++ 不允许让reference改指向不同的对象
        //因此如果需要用 object1 = object2 时 需要自己定义一个copy assignment操作符
        std::string& nameValue;
        
        //同时如果类内 内置了 const成员 编译器不会生成赋值函数 因为修改const是不合法的
        const T objectValue;
        
        
        //在private里声明 copy构造和copy assignment可以防止编译器自行创建
        //而只声明不定义 是为了防止friend可以调用他们
        //这样就阻止了编译器自动创建这些函数了
        NamedObject(const NamedObject&);
        NamedObject& operator=(const NamedObject&);//没有定义
    };
    

    2.2、析构

    补充:C++的三种访问权限

    三种访问权限

    • public:可以被任意实体访问
    • protected:只允许子类及本类的成员函数访问
    • private:只允许本类的成员函数访问

    三种继承方式:public、private、protected
    1、public继承不改变基类成员的访问权限
    2、private继承使得基类所有成员在子类中的访问权限变为private
    3、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
    4、基类中的private成员不受继承方式的影响,子类永远无权访问
    带有多态性质的base-class必须声明一个virtual的析构函数,如果class的设计目的不是base-class,即不声明。

    若 TimerKeeper * pw = new AtomicClock();
    而此时 基类TimerKeeper的析构函数是non-virtual的
    那么derived-class会经由based-class的析构函数销毁
    那么derived-class的derived部分就不会被销毁
    这种局部销毁会导致资源泄露

    本章代码:

    class TimerKeeper {
    private:
        
    public:
        TimerKeeper();
        
        //错误的写法  ---如果用作继承的话
    //    ~TimerKeeper();
        
        virtual ~TimerKeeper();
    };
    
    
    /*
     若 TimerKeeper *pw = new AtomicClock();
     而此时 基类TimerKeeper的析构函数是non-virtual的
     那么derived-class会经由based-class的析构函数销毁
     那么derived-class的derived部分就不会被销毁
     这种局部销毁会导致资源泄露
     
     解决方案:析构函数前+virtual关键字
     */
    class AtomicClock:public TimerKeeper{
    public:
            void close();
        
    };
    
    
    /*如果class不带virtual,通常说明它不被意图用作一个基类
     这时不应将它的析构函数声明为virtual
     
     因为会多些带一个virtual tavle pointer 来决定哪个方法被调用
     会增加对象的体积
     */
    
    
    
    class AbstractTest{
    public:
        
        //声明为抽象类  即该类不能创建对象
        //最深层的derived-class的析构会先被调用,其次是每一个based-class的析构
        //因此除了声明外,还需要对这个抽象类的析构函数创造一个定义。
        virtual ~AbstractTest()=0;
    };
    
    
    
    
    //析构函数绝对不要吐出异常
    //如果需要对某个异常的情况作出反应,那么可以在class内写一个普通函数执行该操作
    class DBCon {
    private:
        AtomicClock ac;
        bool closed;
        
    
    public:
        //有效的异常处理方法  定义自己的close 让异常 有处可寻
        void close(){
            ac.close();
            closed = true;
        }
        
        ~DBCon(){
            
            if(!closed){   //如果客户端不关闭的g话
                try {
                    ac.close();//可能会抛出异常
                } catch (int e) {
                    std::abort();
                    /*
                     abort强迫结束程序
                     阻止异常从析构函数传播出去
                     
                     当然可以不使用abort,在catch后吞下异常,让程序在遭遇错误后继续执行
                     */
                }
            }}
    };
    
    • 析构函数绝对不要吐出异常

    • 如果需要对某个异常的情况作出反应,那么可以在class内写一个普通函数执行该操作

    • 条款9:在析构和构造时不要调用virtual函数,因为这类调用从不下降至derived-class这一层。

    • 令赋值操作符返回一个reference to this *

    2.3、自动赋值出现的问题

    class BitMap {
        
    };
    
    
    //解决自我赋值的问题
    //可能会出现指针指向一块已经被释放过的地址
    class WidGet {
    private:
        BitMap *bp;
        
    public:
        WidGet& operator=(const WidGet &rhs){
           /*
            如果是同一个对象
            delete bp删除的既是this的bp又是rhs的bp
            因此需提前加一个证同测试
            */
    //        if(this == &rhs)return *this;
            
            
            //更好的方法是不去理会 而创建一个新的拷贝
            BitMap * bptemp = bp;
            
        
            bp = new BitMap(*rhs.bp);  //拷贝构造
            delete bptemp;
            return *this;
        }
    };
    

    三、资源管理

    3.1、以对象管理资源

    如果在……发生了异常、return、goto等语句,那么delete将不会执行。
    解决方法是将资源放进对象,对象的析构函数会自动释放这些资源。
    本章代码1:
    两种智能指针 以及 隐式转换Operator

    //智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,
    //当程序退出栈的作用域范围后,由于栈上的变量自动被销毁,智能指针内部保存的内存也就被释放掉了
    
    //智能指针会自动销毁它指向的对象,所以不能同时指向同一个对象
    
    //为防止资源泄露 请使用智能指针对象  它们将在构造函数中获得资源并在析构函数中释放资源
    class Investment{
    public:
        int daysHeld(const Investment*);
        bool isTaxFree();
        
        Investment* createInvestMent(){
            /*在堆中分配*/
            Investment *invest = new Investment();
            return invest;
        }
        
        
        /*
         如果在……发生了异常、return、goto等语句,那么delete将不会执行。
         解决方法是将资源放进对象,对象的析构函数会自动释放这些资源。
         */
        void func1(){
            Investment *pInv = createInvestMent();
            /*
             调用指针……
             */
            delete pInv;
        }
        
        
        //许多动态资源被分配后都用于单一的区块和函数中
        //auto_ptr 智能指针
        void funcAdvice(){
            //createInvestment()的返回结果会作为智能指针的初值
            std::auto_ptr<Investment> pInv(createInvestMent());
            
            /*
             像原来一样调用指针……
             */
        }//会由智能指针的析构函数释放掉资源
        
        
        void funcAdviceSecond(){
    //        shared_ptr也是一个智能指针,使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。
    //        每使用一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。
    //        相比之前的auto_ptr多了拷贝构造  但不能打破环状引用
            std::shared_ptr<Investment> pInv1(createInvestMent());
            std::shared_ptr<Investment> pInv2(pInv1);
    //        ……………
        }//pInv1 pInv2被销毁了
        
        int test(){
            //shared_ptr不会进行隐式转换 需用构造
            std::shared_ptr<Investment> pInv(createInvestMent());
            
    //        直接传原始指针
            int days = daysHeld(pInv.get());
            
            bool tax = pInv->isTaxFree();
            bool tax1 =(*pInv).isTaxFree();
            if(tax==tax1){
                return days;
            }
            return 0;
        }
        
    //    隐式转换 operator + 返回结果 可以根据需要自动改变类型 但是不太安全
        operator double()const{
    //        …………
            return 0;
        }
        
    private:
    };
    

    本章代码2:
    定义删除器

    //每个人的地址有四行,每行是一个string
    //在delete中也要使用delete[]
    typedef std::string AddressLines[4];
    
    
    /*
     注意:
     auto_ptr 与 shared_ptr都在其析构函数内做delete  而不是delete[]
     */
    class Lock{
    public:
        int priority();
        void processWidget(std::shared_ptr<int> pw,int priority);
        
        void test1(){
            
            // int 类型指针的赋值
            int *a;
            *a= 5;
            
            int b = 5;
            a = &b;
            
    //        注意  构造函数里面为指针类型
            //使用分离语句 务必以独立的new 语句将对象存入智能指针
            //如果不这么做,将下面两句写成一句 一旦异常发生 可能会有难以察觉的内存泄漏
            std::shared_ptr<int> pw(a);
            processWidget(pw,priority());
            
            
            
        }
        
        void lock(int *);
        static void unLock(int *);
        
        //注意初始化 与 赋值 的区别 ptr先变成pm 然后再上锁
        //shared_ptr的第二个参数是删除器,当引用计数为0时就会调用
        explicit Lock(int *pm):sharedPt(pm,unLock){
            lock(sharedPt.get());
        }
        
        
    private:
        //智能指针的复制很有可能不合理
        //方法1  把copy构造放在private里 不去定义
        Lock(const Lock &);
        Lock& operator=(const Lock &);
        
        
        //方法二 使用shared_ptr  并且指定 删除器
        std::shared_ptr<int> sharedPt;
    };
    

    四、设计与声明

    注意:绝对不要返回一个reference 指向一个 local-栈 对象 或者返回reference指向 堆-allocated 对象

    本章代码如下:

    #include <iostream>
    
    //类定义里面的成员变量和函数默认都是private型 类本身默认为public型
    class Window {
        
    public:
        std::string name()const;
        virtual void display()const;
        /*
         Window的copy构造会被调用 w会被初始化
         当isOK返回时w会被销毁
         */
        bool isOK(Window w);
        
    //    可以用pass-by reference to const来避免这些构造和析构
        bool isOkAdvice(const Window& w);
        
    };
    
    class WindowWithScrollBars:public Window {
        
    public:
        void display()const;
        
        void printAndDisplay(const Window& w){
            std::cout<<w.name();
            w.display();
        }
        
        //对于内置类型 如(int),以及STL  通常用pass—By-value比较合适
    };
    
    
    /*
     注意:绝对不要返回一个reference 指向一个 local-栈 对象
     或者返回reference指向 堆-allocated 对象
     */
    class Rational {
    private:
        int n,d;
        
        /*
         错误的写法
         result是一个local对象,会在函数退出前被销毁
         因此返回的reference指向旧的Rational
         */
    //    const Rational& operator *(const Rational & lhs,
    //                               const Rational & rhs){
    //        Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    //        return result;
    //    }
    //
    
        /*
         注意区别:
         上面的Rational没有使用new 关键字
         它在栈空间创建对象
         函数退出时栈内存会被回收
         
         而下面的Rational采用了new 只要是new就在堆空间分配
         记住一个死规则 只要是new 就需要delete
         */
        
        
        /*
         更垃圾的写法
         而这里new了以后,内存无人来释放delete
         
         */
    //    const Rational& operator *(const Rational & lhs,
    //                               const Rational & rhs){
    //        Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    //        return  result;
    //    }
        
        //友元函数可以在类内的任何地方声明 不受域的影响
        friend const Rational operator * (const Rational & lhs,
                                          const Rational & rhs);
        
    public:
        Rational(int a= 0, int b =0);
    };
    

    从封装性来看,只有两种封装性,private和其它。
    如果有一个public或者protected的成员变量被更改,会有不可预知的大量代码被更改。

    切记将成员变量声明为private,这可以提供客户访问的一致性。
    如果某些东西被封装,它们便不再可见。它可以使我们改变事物而只影响有限客户。愈多的函数可以访问数据,它的封装性越差


    偏特化:

    函数模板没有偏特化,因为有函数重载的概念,C++根据参数的类型来判断重载哪一个函数,如果还进行偏特化,这就与重载相冲突。但是,我们可一个对模板进行重载,从而实现偏特化。

    模板的实例化类型确定是在编译期间

    练习代码:

    /*
     模板的实例化类型确定是在编译期间
     
     全特化一般用于处理有特殊要求的类或者函数,此时的泛型模板无法处理这种情况。
     
     模板为什么要特化,因为编译器认为,
     对于特定的类型,如果你对某一功能有更好地实现,那么就该听你的。
     
     C++只允许对class template偏特化
     对function-template的偏特化是不合法的
     
     模板实例化只会实例化用到的部分,没有用到的部分将不会被实例化
     */
    
    
    //原始的模版
    template <typename T,typename  T1>
    class Test{
        
    public:
        bool compare(T& a,T &b){
            return (a<b)?true:false;
        }
    };
    
    
    //全特化  可以看作是一种重载
    template <>//参数都指定了 所以参数列表为空
    class Test<int,char> {
    public:
        bool compare(int &a,char &b){
            return false;
        }
    };
    
    
    //偏特化  只指定部分的参数类型
    template <typename T>
    class Test<int,T>{
    public:
        bool compare(int & a,T& b){
            return false;
        }
    };
    
    
    //注意 函数没有偏特化  只有全特化
    
    //函数模板没有偏特化,因为有函数重载的概念,C++根据参数的类型来判断重载哪一个函数,如果还进行偏特化,这就与重载相冲突。但是,我们可一个对模板进行重载,从而实现偏特化。
    
    

    相关文章

      网友评论

        本文标题:Effective C++

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