Effective C++阅读笔记

目录


条款01-04——让自己习惯C++

条款01:视C++语言为一个语言联邦

C++视为一个由多种语言组成的联邦。在某个次语言中,各种守则都简单易懂;从一个次语言转换为另一个次语言时守则可能改变。有以下四种次语言:
1.C:区块、语句、预处理器、内置数据类型、数组、指针等。
2.C with Class:classes:封装、继承、多态、虚函数。
3.Template C++:泛型编程。
4.STL:容器、迭代器、算法、函数对象。

条款02:尽量以const,enum,inline替换#define

使用#define的好处:
1. 用宏定义一个函数的时候不会导致因函数调用导致的额外开销,会有一定的效率提升
2. 不会导致非必要的内存分配。


使用#define的坏处:
1.调试:当使用#define时记号名称在编译之前就被预处理器替换掉了,可能并未进入记号表中,发生错误时很难追踪。
因此用

const double AspectRatio = 1.653;

替换

#define ASPECT_RATIO 1.653;

2.作用域问题:我们是无法用#define来创建一个class的专属常量,一旦宏被定义,其后的编译过程都会有效,没有任何的封装性。
3. 导致函数不安全

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5,b = 0;
CALL_WITH_MAX(++a,b);   //a被累加2次
CALL_WITH_MAX(++a,b+10);   //a被累加1次

用常量替换#define时注意事项
1.定义常量指针时有必要将指针(而不是指针所指之物)声明为const,如

const char* const authorName = “Scott Meyers”;

使用string更好些:

const std::string authorName(“Scott Meyers”);

2.使用class专属常量时,让常量成为class成员以限制其作用域。

class GamePlayer {
private:
    static const int NumTurns = 5; //常量声明式 static确保常量只有一个实体
    int scores[NumTurns];   //使用该常量
    ...
};

以上NumTurns是声明式而非定义式,C++还要求提供定义式:

Const int GamePlayer::NumTurns;//声明时已获得初值定义时不可以再赋值

旧式编译器也许不支持static成员在声明式上获得初值,只能将初值放在定义式。然而在上述GamePlayer的数组声明式中,编译器必须在编译期间知道数组的大小,而又不允许在声明式上获得初值,可以使用enum

class GamePlayer {
private:
    enum { NumTurns = 5 };
    int scores[NumTurns];
    ...
};

enum无法取到地址,这样做可以避免被别人取到指向这个整型变量的指针。enum#define一样不会导致非必要的内存分配。

条款03:尽可能使用const


条款05-12——构造/析构/赋值运算

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

编译器可以自动为class创建一个default构造函数、copy构造函数、copy assignment操作符以及析构函数,所有这些函数都是publicinline
因此你写下:

class Empty { };

就等同于你写下这样的代码:

class Empty {
public:
    Empty() { ... }
    Empty(const Empty& rhs) { ... }
    ~Empty() { ... }

    Empty& operator=(const Empty& rhs) { ... }
};

default构造函数和析构函数主要是编译器用来放置调用base classnon-static成员变量的构造函数和析构函数。注意,编译器产出的析构函数是个non-virtual,除非这个classbase class自身声明有virtual析构函数。
copy构造函数和copy assignment操作符,编译器只是将来源对象的每一个non-static成员变量拷贝到目标对象。考虑这样一个类:

template<typename T>
class NamedObject {
public:
    NamedObject(const char* name, const T& value);
    NamedObject(const std::& name, const T& value);
    ...
private:
    std::string nameValue;
    T objectValue;
}

NamedObject<int> no1("hello world", 2);
NamedObject<int> no2(no1);

编译器生成的copy构造函数必须以no1.nameValueno1.objectValue为初值设定no2.nameValueno2.objectValue。其中nameValue的类型是string,而标准的string有个copy构造函数,所以no2.nameValue的初始化方式是调用string的copy构造函数并以no1.nameValue为实参,另一个成员NameObject::objectValue的类型是int,是内置类型,所以会拷贝no1.objectValue内的每个bits完成初始化。
编译器所生的copy assignment操作符其行为和copy构造函数一致,但如果生成的代码是不合法的,编译器会拒绝为class生成operator=。例如成员变量是referenceconst,或者base classcopy assignment操作符声明为private

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

通常如果不希望class支持某一项特定的功能,只要不声明对应的函数即可,但这个策略对copy构造函数和copy assignment操作符却不起作用,因为编译器会默认声明。那么如何达到目的呢?
关键是所有的编译器产出的函数都是public,为阻止这些函数被创建出来,需要自行声明它们,因此可以将copy构造函数和copy assignment操作符声明为private。这样既阻止了编译器创建它,又阻止了人们调用它。
这被用在C++的iostream程序库中阻止拷贝行为。
一般而言这种做法并不是绝对安全,因为member函数和friend函数还是可以调用private函数。当然你可以不去定义它们(即不实现他们),如果不慎调用了任何一个,就会获得一个连接错误(linkage error)。
将连接期错误移到编译期是可能的(而且是好事,越早侦测出错误越好),设计一个专门阻止copying动作的base class类:

class Uncopyable {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};
//为阻止HomeForSale 对象被拷贝,使其继承Uncopyable:
class HomeForSale : private Uncopyable  {
};

这样当member函数和friend函数尝试拷贝 HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment操作符,编译器会尝试调用base class,由于其base class的拷贝函数是private所以会被拒绝。这样就将连接期错误移到了编译期。
Uncopyable class的实现和运用颇为微妙,不一定以public继承它,Uncopyable的析构也不一定得是virtual

条款07:为多态基类声明virtual析构函数

当基类被派生(即带有virtual函数,因为通常一个class被意图用作基类时含有virtual函数)且基类的析构函数不为虚析构函数时,如果用一个基类指针指向派生类对象,该对象经由基类指针删除时,会出现对象的派生成分没被销毁,基类成分却被销毁的“局部销毁”对象,这将造成资源泄露等后果。
因此,当基类被派生时,请务必将它的析构函数设置为虚析构函数。

当需要抽象类但并没有纯虚函数时,可以将析构函数声明为纯虚析构函数。但此时必须为纯虚析构函数提供一份定义,不然连接器会报错。

class AWOV {
public:
    virtual ~AWOV() = 0;   //声明纯虚析构函数
};
AWOV::~AWOV() { }   //纯虚析构函数的定义

class如果不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数。例如标准stringSTL容器。

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

避免析构函数抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常然后吞下它们或者结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

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

条款09:绝不在构造和析构过程中调用virtual函数

当基类的构造函数中调用了一个虚函数时,此时创建一个派生类对象,无疑基类构造函数将先于派生类构造函数执行,由于基类构造期间虚函数不会下降到派生类阶层,也就是说基类构造函数中的虚函数尚未被初始化,“要求使用对象内部尚未初始化的成分”是危险和不被允许的。
相同道理也适用于析构函数。
因此不要在构造和析构过程中调用virtual函数。
并且构造和析构函数调用的所有函数也须遵守这一点。

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

赋值可以写出连锁的形式:

x = y = z = 15;

因为赋值是采用右结合律的,所以赋值操作符必须返回一个引用指向操作符的左侧实参。这是为classes实现赋值操作符时应该遵循的协议:

class Widget {
public:
    ...
    Widget& operator=(const Widget* rhs) {  //返回类型是引用,指向当前对象
        ...
        return *this;  //返回左侧对象
    }
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,如+=,-=,*=等。

条款11:在operator= 中处理“自我赋值”

自我赋值是指对象赋值给自己。在有别名(即有一个以上的方法指向某对象)的情况下很容易出现自我赋值。
假设建立一个class用来保存一个指针指向一块动态分配的位图:

class Bitmap { ... };
class Widget {
    ...
private:
    Bitmap* pb;   //指针,指向一个从heap分配而得的对象
};

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

这里的自我赋值问题是如果*thisrhs是同一个对象,delete删除的就是rhsbitmapWidget将持有一个指针指向一个已被删除的对象。想要阻止这种错误,传统的做法是借由operator=最前面的一个“认同测试”达到“自我赋值”的检验目的:

Widget* Widget::operator=(const Widget& rhs) {
    if (this == &rhs) return *this;  //证同测试_如果是自我赋值就不做任何事

    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

这样做并不具备异常安全性。如果new Bitmap抛出异常(分配时内存不足或bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap。以下代码可以解决这个问题,只要注意在复制pb所指东西之前别删除pb

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;           //记住原先的pb
    pb = new Bitmap(*rhs.pb)     //令pb指向*pb的一个副本
    delete pOrig;                   //删除原先的pb
    return *this;
}

现在,如果new Bitmap抛出异常,pb及其栖身的Widget保持原状。同时也能处理自我赋值。
另一种替代方案是使用copy and swap技术。

class Widget {
    ...
    void swap(Widget& rhs); //交换*this和rhs的数据
    ...
};
Widget& Widget::operator=(const Widget& rhs) {
    Widget temp(rhs);   //为rhs数据制作一份副本
    swap(temp);        //将*this数据和上述复件的数据交换
    return *this;
}

这个方法基于的事实是:
(1)某class的copy assignment操作符可能被声明为“以by value方式接受实参”;
(2)以by value方式传递东西会造成一份副本。

Widget& Widget::operator=(Widget rhs) {
    swap(rhs):
    return *this;
}

这种做法为了伶俐巧妙的修补而牺牲了清晰性。然而将copying的动作从函数的本体移至函数参数构造阶段,可以令编译器生成更加高效的代码。

条款12:复制对象时勿忘其每一个成分(又名:编译器的复仇)

如果你手动地声明了copying函数,编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当实现代码几乎必然出错时却不告诉你。
例如有新的私有成员变量加入,就必须同时修改copying函数,如果忘记修改,编译器也不会提示。
当基类被继承,写copying函数时不能遗忘复制其基类成分。这些成分一般是private,无法直接访问,所以应该调用基类的构造函数。

当编写一个copying函数时请确保:
(1)复制所有的local成员变量;
(2)调用所有的base classes内的适当的copying函数。
如果发现copy构造函数和copy assignment操作符有相近的实现体,消除重复代码的做法是,建立一个新的成员函数给两者调用。这个函数通常是private且被命名为init

条款13-17——资源管理

条款13:以对象管理资源

把资源放进对象内,可以倚赖C++的“析构函数自动调用机制”确保资源被释放。
标准程序库提供了类指针对象auto_ptr(智能指针):

class Investment {…};
Investment* createInvestment(); //工厂函数返回指针,指向Investment继承体系内的动态分配对象
void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());
    ...
}

1.获得资源后立刻放进管理对象内。
2.管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁的时候会自动销毁所指的对象,所以要注意别让多个auto_ptr同时指向同一个对象。为了预防这个问题,auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们就会变成null,而复制所得的指针将获得资源的唯一拥有权。

std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); // 现在pInv2指向对象,pIn1被设为null
pInv1 = pInv2;  // 现在pInv1指向对象,pInv2被设为null

auto_ptr替代方案是“引用计数型智能指针”RCSP,记录共有多少对象指向某笔资源。

void f()
{
    ...
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());

    std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象
   pInv1 = pInv2;  // 同上,没有改变
      ...
}

auto_ptrtr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作,意味着在动态分配而得的array上使用auto_ptrtr1::shared_ptr是馊主意。

条款14:在资源管理中小心coping行为

coping函数包括copy构造函数与copy assignment操作符。
条款13中提出了RAII的概念:资源获得时机便是初始化时机。当一个RAII对象被复制时,应当选择以下两种行为:
1.禁止复制。具体做法见条款06:将copying操作声明为private
2.对底层资源采用“引用计数法”。
tr1::shared_ptr允许指定“删除器”,“删除器”是一个函数或函数对象,当引用次数为0时被调用。此机能可以用于封装希望对资源采取的操作,例如当处理互斥器对象mutex时,当引用计数为0时利用删除器函数解除对mutex的锁定,如果没有删除器,则会直接删掉mutex

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

在条款13中,使用智能指针保存工厂函数:

std::tr1::shared_ptr<Investment> pInv1(createInvestment());

假如用某个函数处理Investment 对象,如:

int daysHeld(const Investment* pi);
int days = daysHeld(pInv); //这样调用是错误的!

因为daysHeld需要的是Investment*指针,而不是类型为tr1::shared_ptr<Investment> 的对象。
这时候需要一个函数将RAII类对象(tr1::shared_ptr)转换为其所内含之原始资源(底部Investment*),有显式转换和隐式转换两种做法。

显式转换:get成员函数。

int days = daysHeld(pInv.get());

隐式转换:大多智能指针都重载了指针取值操作符(operator-> operator*)。

class Investment {
public:
    bool isTaxFree() const;
    …
};
Investment* createInvestment();

std::tr1::shared_ptr<Investment> pi1(createInvestment());
bool taxable = !(pi1->isTaxFree()); //用operator->访问资源

std::auto_ptr<Investment> pi2(createInvestment());
bool taxable2 = !((*pil).isTaxFree()); //用operator*访问资源

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

string* stringArray = new string[100];
…
delete stringArray; //错误 没有调用所有的析构函数
delete [] stringArray; //正确


string* stringArray2 = new string;
…
delete stringArray2; //正确
delete [] stringArray2; //错误 delete会读取若干内存解读为“数组大小”,然后开始多次调用析构函数

//注意对数组使用typedef时也要匹配使用delete []

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

int priority(); //函数
void processWidget(tr1::shared_ptr<Widget> pw, int priority); //在动态分配的Widget上进行带有优先权的处理
processWidget(new Widget, priority()); //错误无法进行将得自new Widget的原始指针转换为processWidget所要求的tr1::shared_ptr
processWidget(tr1::shared_ptr<Widget>(new Widget), priority()); //正确

上述代码中,调用processWidget之前,编译器要做三件事:执行new Widget、调用priority、调用tr1::shared_ptr构造函数。由于调用priority 发生的顺序是任意的,当发生在另外两个操作之间时(也就是按照上述顺序),调用一旦发生异常,则导致new Widget返回的指针未被置入tr1::shared_ptr而遗失,就引发了资源泄露。解决办法是将new操作分离出来,这样编译器对于跨越语句的操作没有重新排列的权利,不会导致上述问题:

tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值