C++

Effective C++

2018-12-05  本文已影响34人  Tommmmm

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。

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

#ifdef  windows
...
#else
...
#endif
#ifdef debug
...
...
#endif

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

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


一、联邦语言C++

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

-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、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后吞下异常,让程序在遭遇错误后继续执行
                 */
            }
        }}
};

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++根据参数的类型来判断重载哪一个函数,如果还进行偏特化,这就与重载相冲突。但是,我们可一个对模板进行重载,从而实现偏特化。

上一篇 下一篇

猜你喜欢

热点阅读