<<Effective C++>> 第一章~第三章读书笔记


第一章 让自己习惯C++

条款2 尽量以const, enum, incline 替换#define

这个条款也可以叫做 "宁可以编译器替换预处理器"

1. 以 const 替换 #define

    问题引入: 当你使用如下宏定义语句

#define ASPECT_RATIO 1.653  // #define 语句发生在编译期间

可能会发生这样的问题: 当你运用此常量但发生了编译错误, 而上述的#define语句又被定义在了一个不是你所写的头文件中,你会因为追踪他而浪费时间. 归根结底是因为: 这个名称可能从未进入到记号表中.

解决方法:以常量替换上面的宏, 即

const double Aspect_Ratio = 1.653

作为语言常量, Aspect_Ratio肯定会被编译器看到, 当然也会记录到符号表中.

2. 以 incline 替换#define

问题引入: #define 宏定义 通常也会用来实现函数的功能, 比如:

#define CALL_WITH_MAX(a,b)    f((a) > (b) ? (a) : (b)c

这种宏有着很多缺点: 你需要为所有的实参加上小括号, 即使这样, 你在调用的时候也会发生很多事情, 比如现在有如下调用:

int a = 5, b = 0; CALL_WITH_MAX(++a, b);   // a 被累加两次, 运行完 a = 7 CALL_WITH_MAX(++a, b+10); // a 被累加一次,  运行完 a = 6

奇怪的事情是 a 的累加次数竟然和其他参数有关, 这种事情我们是不想看到的.

解决方法: 以 template incline函数 替换 #define

template <typename T> incline void callwithmax(const T& a, const T& b){  f(a > b ? a : b); }

这样的内联函数则可以完全避免之前提到的问题.

小结

  1. 对于单纯常量, 最好以const 对象或enums 替换#defines

  2. 对于形似函数的宏, 最好改用incline 函数替换#defines

条款3 尽可能使用const

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

小结

  1. 永远在使用对象之前先将他初始化, 对于无任何成员的内置类型, 必须手工完成此事.

  2. 在构造函数中, 尽量用参数初始化列表去替换普通的赋值操作.

  3. C++ 对"定义于不同的编译单元内的non-local static 对象"的初始化相对次序并无明确定义

问题引入: 其中前两条比较好理解, 下面着重说一下第三条,

先说一下 什么是non-local static 对象, 那些被定义在函数内的static 对象被称为local static 对象, 剩余的则为non-local static 对象.

编译单元是指产出单一目标文件的那些源码.

说回第三条问题, 考虑现在有以下源码

class FileSystem{ public:     std::size_t num_Disks() const; // 众多成员函数之一 }; extern FileSystem tfs;     // non-local static 对象   class Directory{  public:     Directory(params);     // 构造函数 }  Directory::Directory(params){     std::size_t disks = tfs.num_Disks(); // 使用tfs 对象 }  // 调用Directory 类 Directory tempDir(params);   

现在, 会有一个问题: 除非tfs 是在tempDir 之前先被初始化, 否则tempDir的构造函数会用到未被初始化的tfs.

解决方法: 将每个non-local static 对象搬回到自己的专属函数内(该对象在此函数内被声明为static), 这些函数返回一个reference 指向他所含的对象, 然后用户调用这些函数, 而不直接指涉这些对象.

方法原理: 函数内的local static 对象会在"该函数被调用期间" "首次遇上该定义式"时被初始化, 使用该方法后你可以保证你所获得的reference 对象指向一个历经初始化的对象.

使用该技术以后, 原来的代码变成如下:

class FileSystem{...}; //同前 FileSystem & tfs(){     static FileSystem fs;     return fs; }  class Directory{...} // 同前 Directory::Directory(params){     ...     std::size_t disks = tfs().num_Disks(); // 同前, 原来的tfs 对象改为调用tfs 函数 }  Directory& tempDir(){     static Directory td;     return td; }

这样修改代码以后, 这个程序的客户完全可以像以前一样的使用他, 唯一不同的是现在使用的tfs() 和 tempDir() 而不是tfs 和 tempDir.

第二章 构造/析构/赋值运算

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

如果你打算在一个内含"reference 成员" 或者 "const 成员" 的class 内支持赋值操作, 你必须自己定义 操作符重载函数.

小结

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

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

问题引入: 现在有如下类, 你希望他是唯一的, 即不希望他支持拷贝构造函数和操作符重载, 同时也不希望编译器为这个类自动的生成拷贝构造函数和操作符重载, 即阻止一些copying 行为!!!

class HomeForSale{ };

解决方法1: 将拷贝构造函数和赋值操作符重载声明为private, 这样你明确了一个成员函数, 同时你也阻止了编译器为其创建其专属版本, 而令这些函数为private ,也使得你成功阻止人们调用它, 具体代码如下:

class HomeForSale{
public:
...
private:
...
    HomeForSale(const HomeForSale&);  // 只有声明
    HomeForSale& operator=(const HomeForSale&);
}

解决方法2: 设计一个专门为了阻止copying 动作的base class.

class Uncopyable{
protected:
    Uncopyable() {}  // 只允许派生类对象构造和析构
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&); //  但阻止copying
    Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable{   // 派生类不再声明拷贝构造函数和赋值操作符重载
    ...
};

调用过程: 当成员函数或者友元函数尝试拷贝HomeForSale对象, 编译器便试着生成一个拷贝构造函数和赋值操作符重载, 这些函数的"编译器生成版" 会尝试调用其base class 的对应兄弟, 但是那些调用会被编译器拒绝, 因为base class 的拷贝函数是private.

条款7 为多态基类声明virtual 函数

小结

  1. 带有多态性质的基类应该声明一个virtual 析构函数, 如果class 带有任何virtual 函数, 他就应该拥有一个virtual 析构函数

  2. class 的设计目的如果不是为了基类使用,或不是为了具备多态性, 就不该声明为virtual 析构函数

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

问题引入: 考虑一个利用RAII机制的数据库管理类如下:

class DBConnection{
    public:
    static DBconnection create();
    
    void close();
};

// RAII  机制
class DBconn{
 public:
    ...
    ~DBconn(){
        db.close();
    }
 private:
    DBconnection db;
};

如上的类正常情况下可以很好的防止资源泄露, 但是如果close 函数发生异常, DBconn 析构函数便会传播异常, 也就是允许他离开这个析构函数,这样会造成问题.

传统的解决方法:

  1. 在析构函数中,捕捉异常, 遇到异常后直接利用abort函数结束程序

  2. 吞下异常, 即只记录异常, 但是程序正常跑, 这样的处理方法很可能会造成未定义的行为.

较好的解决方法: 重新设计DBconn 接口, 使其用户有机会对可能出现的问题做出反应

DBconn 自己提供一个close 函数, 因而赋予客户一个机会得以处理"因该操作失败而发生的异常", 同时DBconn 也可以追踪其所管理值DBconnection是否已经被关闭, 并在答案为否的情况下由其析构函数关闭之. 然后如果DBconnection 析构函数调用close 失败的话, 我们也可以退回"强迫结束程序" 或者吞下异常的老路.

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

小结

  1. 析构函数绝对不要吐出异常, 如果一个被析构函数调用的函数可能抛出异常, 析构函数应该捕捉任何异常, 然后吞下他们或者结束程序

  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应, 那么class 应该提供一个普通函数(而非在析构函数中)执行该操作

条款9 绝不在构造函数和析构函数中调用virtual 函数

小结

  1. 在构造和析构函数中不要调用virtual ,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)

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

条款11 在operator= 中处理"自我赋值"

问题引入: 考虑以下代码

class Bitmap{...};
class Widget{
...
private:
    Bitmap* pb;
};

Widget& 
Widget::operator=(const Widget& rhs){     // 一份不安全的bitmap
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

上面的赋值操作符重载函数存在的问题是: operator函数内的*this 和rhs 有可能是同一个对象, 这样delete 就不止销毁当前对象的bitmap, 也销毁了rhs的bitmap, 最后函数末尾, 发现自己持有一个指针指向一个已经被删除的对象.

传统做法: 加上证同测试, 即

Widget& 
Widget::operator=(const Widget& rhs){     // 一份不安全的bitmap
    if(this == &rhs) return *this;   // 证同测试
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

虽然上述的代码具有了自我赋值安全性, 但是依然存在异常安全性

改进做法: 我们只需注意在复制pb 所指东西之前别删除pb即可

Widget& 
Widget::operator=(const Widget& rhs){     // 一份不安全的bitmap
    Bitmap* pOrig = pb;     // 记住原先的pb
    pb = new Bitmap(*rhs.pb); // 令pb 指向*pb 的一个复件
    delete pOrig; // 删除原先的pb
    return *this;
}

小结:

  1. 确保当对象自我赋值时operator= 有良好的行为, 其中技术包括比较"来源对象"和"目标对象的地址", 精心周到的语句顺序以及copy-and-swap

  2. 确定任何函数如果操作一个以上的对象, 而其中多个对象是同一个对象的时候, 其行为仍然正确.

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

小结

  1. copying 函数应该确保复制"对象内的所有成员变量"及"所有base class 成分"

  2. 不要尝试以某个copying 函数实现另一个copying 函数, 应该将共同机能放进第三个函数中, 并由两个copying 函数共同调用(即拷贝构造函数和赋值操作符重载之间不能相互调用, 没有意义)

第三章 资源管理

条款13 以对象管理资源

问题引入: 我们在进行获取资源以后,总要对这些资源进行释放,但是总有可能因为抛出异常或者提前返回一个return语句等原因忘记了资源的释放,会造成资源泄露的问题.

举例:

void f(){
    Investment* pInv = CreateInvestment();  //调用工厂函数
    ...
    ...
    delete pInv; //释放资源
}

比如在"..."期间抛出了异常或者提前返回了return语句,就会跳过最后的释放资源语句.

解决方法: 将资源放进对象内,依赖对象的析构函数自动调用机制,释放资源.(资源获取即初始化,RAII)

一个很好的工具就是智能指针,其内部便是使用了RAII机制,进行资源的自动释放.

所以上面的代码可以改成如下形式:

void f(){
    std::auto_ptr<Investment> pInv(CreateInvestment()) //调用工厂函数, 一如既往的使用pInv, 经由auto_ptr的析构函数自动删除pInv
    ...
    ...
    delete pInv; //释放资源
}

同时另一个智能指针shared_ptr,内部集成了引用计数机制, 可以令多个智能指针指向同一个对象.

小结

  1. 为防止泄露资源,请使用RAII对象,让他们在构造函数中获取资源并在析构函数中释放资源

  2. 两个常被使用的RAII class是shared_ptr和auto_ptr. 前者通常是较佳选择, 因为其copy行为比较直观, 若选择auto_ptr,复制动作会使他指向null.

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

问题引入: 当一个RAII 对象被复制的时候, 会发生什么事情??

//一个互斥锁对象, 结合了RAII机制
class lock(){
  public:
    explicit Lock(Mutex* pm):mutex(pm){
        lock(mutexptr); //获取资源
    }
    ~Lock(){
        unlock(mutexptr); //释放资源
    }
   private:
    Mutex* mutexptr;
}

解决方法:

  1. 禁止复制 如果复制动作不合理,便应该禁止.具体做法参考条款6: 将copying行为声明为private.

class Lock: private Uncopyable{     //禁止复制, 参考条款6
    public: 
    ...      ///如前
}
  1. 借助引用计数法: 有时候我们希望保有资源, 直到他的最后一个使用者被销毁,在何种情况应该借助引用计数进行递增.

class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock){  // 指定unlock函数为shared_ptr的第二参数
    lock(mutexPtr().get());
}
private:
    std::shared_ptr<Mutex> mutexPtr; //使用shared_ptr 替换原始指针
}

小结:

  1. 复制RAII对象必须一并复制他所管理的资源, 所以资源的copying行为决定了RAII对象的copying行为

  2. 普通而常见的RAII class copying 行为是抑制copying, 施行引用计数法, 不过其他行为也都可以实现.

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

问题引入:有时候我们需要一个函数将RAII class 对象转换为其所内含之原始资源

class Font{    // RAII class
public:
   explicit Font(FontHandle fh): f(fh){}   // 获得资源
    ~Font(){
        releaseFont(f);   //释放资源
    }
private:
    FontHandle f;   // 原始资源
}

解决方法:

  1. 显示转换: Font class 可以提供一个显示转换的函数, 例如像智能指针中的get()函数一样

// RAII class
class Font{
public:
FontHandle get() const{        // 显示转换函数
    return f;
}
};

// 客户调用过程
void changeFontSize(FontHandle f, int newsize); // API
changeFontSize(f.get(), newFontSIze);  // 显示调用get函数
  1. 隐式调用

// RAII class
class Font{
public:
operator FontHandle() const{ // 隐式转换函数
    return f;
}
};

// 客户调用过程
Font f(getFont());  // 声明f对象
changeFontSize(f, newFontSize); //  将Font 隐式转换为FontHandle

小结

  1. APIs 往往要求访问原始资源, 所以每一个RAII class应该提供一个"取得原始资源的方法"

  2. 对原始资源的访问可能经由显示转换或者隐式转换,一般而言显示转换比较安全, 但是隐式转换对客户比较方便

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

问题引入:

查看以下代码有何问题?

std::string* stringArray = new std::string[100];
...
delete stringArray; 

问题: 程序行为不明确, stringArray 所含的100个string对象中的99个不太可能被适当删除,因为他们的析构函数没有被调用

解决方法: new[] 和 delete[] 成对使用

std::string* stringArray = new std::string[100];
...
delete [] stringArray; 

小结

  1. new 和 delete 搭配使用, new [] 和delete[] 搭配使用

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

问题引入: 考虑以下调用代码的执行顺序

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

//调用
processWidget(std::shared_ptr<Widget>(new Widget), priority());

在调用过程中, 会发生三件事

  1. 调用priority();

  2. 执行 new Widget

  3. 调用shared_ptr构造函数

其中1的执行顺序不是固定的, 而事件2一定发生在事件3之前, 所以很可能会有这样一种情况

  1. 执行 new Widget

  2. 调用priority();

  3. 调用shared_ptr构造函数

那么如果事件2的过程中抛出了异常, 会导致"new Widget"返回的指针遗失, 造成内存泄露

问题的来源是资源被创建资源被转换为资源管理对象两个时间点之间可能会发生异常

解决方法: 使用分离语句: (1) 创建Widget (2) 将他置入一个智能指针之中, 然后再把智能指针传给函数, 即

std::shared_ptr<Widget> pw(new Widget);  // 在单独语句中以智能指针存储newed 对象 processWidget(pw, priority());

通过两行语句则可以避免资源泄露

小结

以独立语句将newed对象存储于智能指针内, 如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露

#C/C++#
全部评论
感谢楼主分享!
点赞 回复 分享
发布于 2022-01-12 19:35

相关推荐

ArisRobert:统一解释一下,第4点的意思是,公司按需通知员工,没被通知到的员工是没法去上班的,所以只要没被通知到,就自动离职。就是一种比较抽象的裁员。
点赞 评论 收藏
分享
1 4 评论
分享
牛客网
牛客企业服务