《Effective C++》笔记(一)

《Effective C++》笔记(一)

一、基础C++习惯

1.视C++由四个次语言组成:

  • C
  • Object-Oriented C++ (C with Classes)
  • Template C++ (泛型编程部分)
  • STL (容器,迭代器,适配器,空间分配器,仿函数,算法)

2.宁可以编译器替换预处理器:

class GamePlayer{
private:
	enum { NumTurns = 5 };//"the enum hack" - 令NumTurns成为5的一个记号名称
	
	int scores[NumTurns];
};

**enum hack值得认识理由:**第一,enum hack的行为某方面说比较像#define而不像const。例如取一个const的地址是合法的,但取一个enum的地址就不合法。第二,实用主义。许多代码用了它,所以阿卡难道它时必须认识它。"enum hack"是模板元编程的基础技术。

请记住:

  • 对于单纯常量,最好以const对象或enums代替#define
  • 对于形似函数的宏,最好改用inline函数替换#define
  • enum hack - 行为更像#define而不是const,如果不希望别人得到你的常量成员的指针或引用可以用enum hack代之。没有#define可视范围难以控制,不利于调试的缺点。和#define一样不会导致非必要的内存分配。

3.尽可能使用const:

bitwise constness:成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。也就是说它不更改对象内的任何一个bit。

class CTextBlock{
public:
    char& operator[](std::size_t position) const //bitwise const声明,但其实不恰当
    { return pText[position]; }
private:
    char* pText;
};
//这个class不适当的将其operator[]声明为const成员函数,而该函数却返回一个ref指向对象内部值
//operator[]实现代码并不更改pText,编译器认为它是bitwise const可以顺利产出目标码

//但是可能发生下面这样事情:
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';		//最终还是改变了它的值

//涉及到另一个logical constness概念

mutable可以释放掉non-static成员变量的bitwise constness约束。

class CTextBlcok{
public:
    std::size_t length() const;
private:
    char* pText;
    mutable std::size_t textLength;//这些成员变量可能总是会被更改,即使在const成员函数内
    mutable bool lengthIsValid;
};

std::size_t CTextBlock::length() const
{
    if(!lengthIsValid){
        textLength = std::strlen(pText); //现在这样也是合法的
        lengthIsValid = true;
    }
    return textLength;
}
  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
  • 编译器强制实施bitwise constness
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
class TextBlock{
public:
    const char& operator[](std::size_t position) const
    {
        //...
        return text[position];
    }
    
    char& operator[](std::size_t position)
    {
        //若non-const operator[]内部只是单纯调用operator[],会递归调用自己.
        //先static_cast强制转为const对象,这样接下来operator[]时得以调用const版本.
        //最后则通过const_cast移除const.
        return
            const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    }
private:
    std::string text;
};

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

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static(即在函数里面声明static对象)
class FileSystem { //... };
    
FileSystem& tfs()//通过这个函数来拿tfs对象;它在FileSystem中可能是个static
{
    static FileSystem fs;
    return fs;
}

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

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

六大默认成员函数

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值运算符
  • 取地址运算符、const取地址运算符

当它们被调用时才会被编译器创建出来。

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

//继承Uncopyable阻止对象被拷贝
class Uncopyable{
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);//设置成私有防止copying
    Uncopyable& operator=(const Uncopyable&);
}
  • 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。
  • 继承Uncopyable这样的基类也是一种做法。(例如Boost的noncopyable类)

7.为多态基类声明virtual析构函数

  • 带多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • class的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数

8.别让异常逃离析构函数

  • C++并不禁止析构函数吐出异常,但并不鼓励这样做
  • 在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为
  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数允许期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

9.绝不在构造和析构过程中调用virtual函数

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

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

11.在operator=中处理“自我赋值”

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序(先用指针记住原来的data,新赋值后再删除原先的data)、copy-and-swap技术
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

  • Copying函数(复制构造函数、赋值操作符)应该确保复制“对象内的所有成员变量”及“所有base class成分”(1.复制所有local成员变量,2.调用所有base classes内的适当的copying函数)
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用

三、资源管理

13.以对象管理资源

//一个工厂函数
//返回指针,指向Investment继承体系内的动态分配对象,调用者有责任删除它。
Investment* createInvestment();

void f()
{
    Investment * pInv = createInvestment();//调用factory函数
    //...
    delete pInv; //释放pInv所指对象
}

有些情况下f() 可能无法删除它得自createInvestment得投资对象,例如“…”区域内的一个过早的return语句;对createInvestment的使用位于某循环内,而该循环由于某个continue或goto语句过早退出;最后一种可能是“…”区域内的语句抛出异常。

delete被略过去,我们泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。

为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f() ,该对象的析构函数会自动释放那些资源。

以对象管理资源的两个关键想法:

  • 获得资源后立刻放进管理对象内。createInvestment返回的资源当作只能指针的shared_ptr的初值。(RAII,资源取得时机便是初始化时机,在获得一笔资源后于同一语句内以它初始化某个管理对象)
  • 管理对象运用析构函数确保资源被释放。如果资源释放动作可能抛出异常,则比较棘手,参考条款8解决这个问题。

shared_ptr在其析构函数内做delete而不是delete[]操作,那意味在动态分配的数组身上使用shared_ptr是个馊主意。可通过一个可调用对象解决。

bool del(int *p)
{
    delete [] p;
}
//使用函数
share_ptr<int> ptr(new int[100], del);
//使用lambda表达式
shared_ptr<int> ptr(new int[100], [](int *p){delete [] p});

请记住:

  • 为防止资源泄露,请使用RAII对象。它们在构造函数中获得资源并在析构函数中释放资源。
  • 常用的RAII类比如shared_ptr。

14.在资源管理类中小心coping行为

假设使用C API函数处理类型为Mutex的互斥对象,共有lock和unlock两函数可用。

为了确保不会忘记将一个被锁住的Mutex解锁,建立一个class来进行管理。

class Lock{
    public:
    	explicit Lock(Mutex* pm)
            :mutexPtr(pm)
        { lock(mutexPtr);}
    
    	~Lock() { unlock(mutexPtr);}
    private:
    	Mutex *mutexPtr;
}

如果Lock对象被复制,将会发生什么?

Lock ml1(&m); //锁定m
Lock ml2(ml1); //将ml1复制到ml2身上

针对RAII对象可能被复制,大多数时候可能做的选择:

  • 禁止复制。许多时候允许RAII对象被复制并不合理。

  • 对底层资源祭出“引用计数法”

    //有时候想要做的释放动作是解除而非删除,可以指定“删除器”
    //那是一个函数或函数对象,当引用次数为0时便被调用
    class Lock{
    public:
    	explicit Lock(Mutex* pm) //以某个Mutex初始化shared_ptr
         :mutexPtr(pm, unlock)//并以unlock为删除器
        {
             lock(mutexPtr.get());//条款15提到get
        }
    private:
        std::shared_ptr<Mutex> mutexPtr;
    };
    
  • 复制底部资源。深拷贝。

  • 转移底部资源的拥有权。如unique_ptr。

请记住,

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII类 copying行为是:禁止copying,引用计数法。

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

  • 有时候API需要访问原始资源,所以每个RAII类应该提供一个“取得其所管理资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换(提供类型转换函数)。一般而言,显示转换比较安全,隐式转换对用户比较方便。

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

  • 在调用new时使用[ ],必须在对应调用delete时也是用[ ]。如果调用new时没有使用[ ],delete时也不该使用[ ]。

    //小心typedef类型对象时调用delete
    typedef std::string AddressLines[4];
    std::string* pa1 = new AddressLines; //返回一个string*,像“new string[4]”一样
    delete pa1;//错误,行为未定义
    delete [] pa1;//正确
    

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

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

//现在考虑调用processWidget
processWidget(new Widget, priority());//不能通过编译,shared_ptr构造函数是个explicit函数

//改写成这样可以通过编译了,现在有什么问题?
processWidget(std::shared_ptr<Widget>(new Widget), priority());

在调用processWidget之前,编译器必须创建代码,做以下三件事:

  • 调用priority()
  • 执行“new Widget”
  • 调用shared_ptr构造函数

C++编译器完成这些事情的顺序是不确定的,如果对priority的调用在第二顺位执行,万一对priority的调用导致异常,此情况下"new Widget"返回的指针将会遗失,因为它尚未被置入shared_ptr内。所以这样子对processWidget的调用过程中可能引发资源泄露。

std::shared_ptr<Widget> pw(new Widget);//在单独语句内用智能指针存储new所得对象
processWidget(pw, priority);//这个调用动作绝不至于造成泄露
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值