关闭

《Effective C++》读书笔记

标签: c++
264人阅读 评论(0) 收藏 举报

前言:鄙人又无耻的弄C++去了,自从比赛后就再也没碰ACM,打算暑假再弄,放假前先让自己的C++更NB。因为比赛中一定会用到C++STL,于是我再比赛前认真的看了STL的使用,于是比赛中真的用到了,但是大部分时间都在Debug让我很不爽,我就在之后借了本C++书好好弄弄STL,结果发现以前很多C++都忘了,模板封装多态继承设计模式等等。所以就有了这本书的阅读。对于某些条款并没有写也不可能都写,因为会在其他条款中有意无意带出来。

Item 5、6 关于编译器自动生成的函数

编译器会自动为没有声明过构造函数的类自动生成一个默认构造函数,并且当用一个对象赋值给另一个对象需要一个 = 运算符,以一个对象生成另一个对象时需要一个默认拷贝构造函数,对对象进行从头到脚的复制简称浅拷贝。当对象声明周期结束后需要销毁对象,还需要一个析构函数;就这些啦。

编译器提供的默认函数都是 Public 的,当不想使用这些编译器给写好的函数时可以将之声明为private并不予实现,上面的四个函数中构造函数和析构可以通过声明出来随便一个构造函数令编译器不自己实现。如下代码禁止了 = 符号和拷贝构造函数的默认生成:

class Uncopyable {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable &);
    Uncopyable &operator=(const Uncopyable &);
};

Item 7-9 关于virtual析构函数

书中举了一个例子,说创建完了一个派生类对象通过基类指针删除之,但是这个基类的析构函数并非virtual。先说结论:只要拥有一个virtual函数,那么最好耶配上一个virtual析构函数;如果class不含virtual函数,通常并不是要用它来做一个基类;当class不企图当做基类,那么声明其析构函数virtual是一个馊主意。
分析其内涵,如果一个并不准备作为基类的类被派生,它没有声明virtual析构函数,通过基类指针或引用调用析构函数将不会实现多态性,则销毁的就是基类之部分。而声明为virtual后则可以实现其多态性质,自然体现了基类与派生类的区别。

析构函数中可能会有回收资源以及其他的操作,当回收资源前所谓的其他的操作运行并且抛出异常,那么就会导致资源的泄露,通常在析构函数中捕捉一切发生的异常,不让异常退出到更大的作用域,并且也不要在析构函数中自己跑出异常。

绝不在构造和析构过程中调用virtual函数
多态性质要在对象构造完成之后才能通过对指针和引用的调用中体现,而在没有真正完成对对象的构造时virtual函数并不是virtual函数。如果在基类的构造函数中调用了virtual函数(不会在派生类中调用),此时对象没有构造完成,仅仅完成基类的构造,在基类中就调用virtual函数,是找不到这样的被覆盖的virtual函数的。更根本的解释是在没有完成构造时对象的类型是base而不是derived。

Item 10-12 关于 = 运算符

令operator= 返回一个 reference to *this
当重载了 = 号运算符后返回一个*this用于连续的赋值,如果不返回则相当于一次函数调用其返回为void但是的的确确是赋值了的,加上去虽然在一个单一的赋值上体现不出来,但如果如下:

x = y = z = (a = b = 1);

就可以正常运作了。


还有一个很神奇且复杂的 swap 函数,设计到后面的 pImpl,等我弄懂了再来写笔记。


Item 13 以对象管理资源

书中举例了一个函数如下:

// 该函数是一个工厂方法,用来返回一个Investment对象指针
// 在这个方法中使用了 new 关键词用意分配空间创建对象
Investment *createInvestment();
// 此函数仅仅管理创建问题而没用管理删除delete该对象
void f() {
    Investment *pInv = createInvestment();
    ...
    // 在此客户使用 delete 删除对象
    delete pInv;
}

这里面还有一个更加沉重的话题,如果在 … 处的代码抛出了异常则会中断该处的运行,从而导致客户的 delete 没有办法执行而导致资源泄露。

更加通用的方式是用对象管理资源,在对象的构造函数中完成分配资源,当对象构造完成之后资源即分配完成。而在客户调用中创建该对象资源已经分配结束,程序控制流经过以及对象声明周期结束后对象会调用析构函数。在析构函数中结束资源的分配即delete该资源避免资源的泄露,达到内存的 new 和 delete 成对出现以及资源的分配和回收。

另一种方法使用 std::auto_ptr 模板函数完成指向的资源分配和回收,这是一种独占式智能指针,类似的 C++ 11 新标准中还有 shared_ptr 以及 wake_ptr 和 unique_ptr。

可以如下的声明一个只能指针管理资源对象

void f() {
    std::auto_ptr<Investmeng> pInv(createInvestment());
    ...
}

书上提示了示范“以对象管理资源”的想法:
获得资源后立刻放进管理对象
上述代码将返回的指针立即用于初始化一个智能指针对象,而智能指针设计的根本目的是防止资源泄露,以刚刚得到的资源指针初始化只能指针就是将之放入管理对象之中。

管理对象运用析构函数确保资源被释放
对象的声明周期是程序控制流,只要用不到这个对象和资源时,对象管理的资源也被立即回收,在管理对象类的析构函数中释放资源。但这个标题是确保资源被释放,涉及到了在析构函数中顺利释放资源,例如在释放资源的途中出现的异常而导致资源无法释放,但是析构函数的确执行了。Item 8 提供了解决之道。完全捕捉析构函数中出现的异常,即 catch(…)。

文中还提到了 auto_ptr 的独占指向,仅仅有一个指针可以真正的指向一个资源,若其他指针指向那么前一个指针则会被置为 NULL。鄙人以为进入了C++11时代那么这么老的 auto_ptr 是不是应该淘汰了。而且书上也介绍了 TR1 中的 shared_ptr,但各有各的用处,还是使用更加新的标准来撰写程序。

Item 14-17 资源管理方面的其他细节

可能 shared_ptr 以及 auto_ptr会不适用于某些资源的管理,需要自己建立资源管理类,面对RAII对象复制,书上给出的两种可能:

void lock(Mutex *pm);
void unlock(Mutex *pm);
class Lock {
public:
    // 声明explicit禁止隐式转换构造
    explicit Lock(Mutex *pm) : mutexPtr(pm) {
        lock(mutexPtr);
    }
    // 释放资源,关于unlock异常详见Item8
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

Mutex m;
...
{
    Lock ml(&m);
    ...
}

Lock ml1(&m);
// 讲ml1复制到ml2身上,会发生什么事?
Lock ml2(ml1);

禁止复制
说明这个RAII对象的复制并不合理,比如Lock!没事复制Lock干嘛,如果锁可以复制,先假设是浅copy,在copy构造函数构造对象时会再次对Mutex对象锁一次,这样可能导致错误并且也是毫无意义,即使成功上锁,当有一个对象析构会解锁,造成逻辑不通且另外对象再次解锁造成错误。如若是深copy则创建的新对象以及一个新的Mutex *资源,但是这个互斥指针被赋值为之前对象的指针也就是说通浅copy相同,或许也不涉及什么深浅拷贝,但逻辑说明这样的复制并不合理。
要做到禁止复制可以使用Item 6提到的 Uncopyable 对象然后继承之,便无法复制了。这样不光没法拷贝构造,就连 = 都给禁了。

对底层资源祭出“引用计数法”
shared_ptr就是通过对资源使用计数,当最后一个资源销毁时删除指针所指向之物。

class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm, unlock){
        // 此处见原书Item 15处提供对原始资源的访问
        lock(mutexPtr.get());
    }
private:
    shared_ptr<Mutex> mutexPtr;
};

mutexPtr(pm, unlock)指定了删除器。


有如下代码

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

有概率发生这样的情况:processWidget函数需要一个shared_pt指针对象作为参数,于是调用 new Widget 构造Widget对象并返回其指针,并由一个 non-explicit (implicit)的构造函数通过隐式转换直接构造对象传入之。但可能出现这样的顺序:

  1. 执行 new Widget
  2. 调用priority()
  3. 调用 shared_ptr构造函数

    如若 priority() 的调用导致了一个异常的抛出或者return将会出现 只new 不 delete的情况出现,所以书中明确指示请将 new 独立的构造完成再放入智能指针内。

shared_ptr<Widget> pw(new Widget);
processWdiget(pw, priority());

还指出的一条是承兑使用new和delete时需要采取相同形式
就是 new 一个array就用 delete[] 删除之,如果 new 一个对象就用 delete,这就是所谓的相同形式。

Item 20 宁以pass-by-reference-to-const替换pass-by-value

这条貌似会是读完这本书后即使其他条目不记得也会记得这条的。
主要是为了效率来pass-by-reference-to-const,对于函数的参数来说,这样仅仅传入一个引用大小的数据,如果使用pass-by-value则会调用类的构造函数以及析构函数造成对象构造的开销,而且要给传入的对象分配空间,不仅仅是时间上的浪费而且也对空间进行了一定的浪费。
其次如果希望应用多态性质那么也是必须传入引用的,因为仅仅可以通过对象的引用或者指针才能发挥动态解析对象动态类型的目的。如果传入了对象的副本则函数在编译期间就可以确定到底调用的是基类还是派生类的方法无法做到动态绑定。如下代码:

class Window {
public:
    std::string name() const;
    virtual void dispaly() const;
};

class WindowWithScrollBars : public Window {
public:
    virtual void display() const override;
};

void printNameAndDisplay(Window w) {
    std::cout << w.name();
    w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

很明显 printNameAndDisplay(wwsb) 这句传入的是 pass-by-value 而非const引用,故不可能实现多态性,调用的是基类Window的display() 方法。不仅如此,传入的派生类对象由于这个函数仅仅在编译时期为这个参数(Window对象)分配了基类大小的空间,但是明显这个派生类中有额外的东西故其空间使用大于Window造成了空间的分配不足够导致了对象生成不完全成为了切割。解决之道就是传入const引用,在编译期间分配引用大小的空间,不论传入何种对象,引用站的空间就那么大,不会造成切割的产生。

Item 30 透彻了解 inlining 的里里外外

inline函数在编译时被直接固化到代码中而并非以函数调用的方式进行编译,以增加目标码大小来空换时。书中提到了由于代码过大而导致的高速缓存命中降低和内存缺页换页带来的效率损失。
inline仅仅是对编译器的申请,不是强制命令。如果直接放在class中的成员函数定义那么这个成员函数就被inline。
一个 inline template 往往可以替代宏定义

template<typename T>
inline const T &std::max(const T &a, cosnt T &b) {
    return a < b ? b : a;
}

当需要取到inline函数的地址时就需要给inline函数生成一个真实的函数本体,但编译器通常不对“通过函数指针调用的函数”生成inline。

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

Item 49 了解 new-handler 的行为

其实这个new-handler并没有什么卵用,主要是对于 new 分配内存产生异常后调用的函数,相当于一个回调函数,但目前鄙人遇到的 new 分配基本不出问题。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:10446次
    • 积分:483
    • 等级:
    • 排名:千里之外
    • 原创:30篇
    • 转载:1篇
    • 译文:0篇
    • 评论:2条
    最新评论