Effective C++ 读书笔记(1)

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


条款2:尽量以const、enum、inline替换#define

原因1:

#define并不会被编译器发现,而是提前被 预处理器发现并处理,原因是有可能define定义的语句不会进入 记号表(symbol table)中,最后出现编译错误。
PSsymbol table (from stackoverflow)

Briefly, it is the mapping of the name you assign a variable to its address in memory, including metadata like type, scope, and size. It is used by the compiler.

That's in general, not just C[++]*. Technically, it doesn't always include direct memory address. It depends on what language, platform, etc. the compiler is targeting.

原因2:

对float这类浮点数,如果使用const来替换define,可以减少代码替换量,不至于出现多份浮点数。

class专属常量

class A
{
public:
	//class专属常量
	static const int a;
	A(){};
};

上面代码中static int a为声明式,而非定义式,只要不取它们的地址,可以只用声明它,但如果要取地址,则必须定义它

const int A::a = 10;

条款3:尽可能使用const

区分常量指针与指针常量

const int *p1 = &a; //常量指针,const data
int *const p2 = &b; //指针常量,const pointer

*p1 = 10; //ERROR
p1 = &b; //OK

*p2 = 10; //OK
p2 = a; //ERROR

const在iterator中的应用

vector<int> v;
//it1相当于一个T* const
const vector<int>::iterator it1 = v.begin(); 
*it1 = 10; //OK
++it; //ERROR
//it2相当于一个const T*
vector<int>::const_iterator it2 = v.begin();
*it2 = 10; //ERROR
it2++; //OK

const成员函数

两个成员函数如果只是常量性不同,它们可以被重载

bitwise constness 和 logical constness

bitwise constness:成员函数只有不改变对象任何成员变量(除static)时才称为const
logical constness:可以修改内部某些变量,但只有在客户端发现不了的情况下(在const成员函数内若想进行变量内容的修改,可以通过将变量定义为 mutable来实现)

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

类构造函数初始化的较佳做法是  列表初始化,特别是成员变量是 const或reference时, 一定要初始化,不可以赋值.

列表初始化是发生于类成员调用它们的default构造函数时(比进入该类的构造函数本体的时间更早)

class的成员变量初始化总是以声明次序来进行初始化,即使在初始化列表中以不同的次序出现,也不会有影响。

不同编译单元内定义之non-local static 对象的初始化次序

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

一个空类(empty class),编译器会为它声明一个 copy构造函数、一个 copy assignment操作符、一个 析构函数以及一个 default构造函数
如果打算在一个内含reference或const成员的class内支持赋值操作,必须自己定义copy assignment操作符


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

不想类中支持复制构造、赋值构造函数,方法有:
1.将copy构造函数、copy assignment定义为 private(虽然 成员函数和friend还是可以调用)且 只声明不定义
class A
{
public:
	A(){}
private:
	A(const A&);
	A &operator= (const A&);
};
2.专门设计一个Uncopyable类并继承它
class Uncopyable
{
public:
	Uncopyable() {}
	~Uncopyable() {}
private:
	Uncopyable(const Uncopyable&);
	Uncopyable& operator=(const Uncopyable&);
}
class A:private Uncopyable
{.....}


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

原因

局部销毁,即当一个derived class对象经由一个base class指针删除,而base class带着一个non-virtual析构函数,其结果未有定义---实际执行时通常发生的是对象的derived成分没有被销毁

解决办法:

给base class一个virtual析构函数
PS:并非所有的base class都需要virtual析构函数,盲目的加入虚析构函会增加class的内存(虚析构函会带有vtbl),所以该规则只适用于带多态性质的base class,这种class 的设计目的是为了用来通过base class接口处理derived class对象

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

析构函数不要吐出异常
1.如果程序遭遇一个“于 析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项,即使用abort(),或吞下该异常

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

class A
{
public:
    A();
    virtual void getA() const = 0;
};
A::A()
{
    ...
    getA();
}
class B:public A
{
public:
    virtual void getA() const;
};
class C:public A
{
public:
    virtual void getA() const;
};

B b;
当创建b时,B的构造函数会被调用,但首先A的构造函数会被先调用,在A的构造函数运行时最后会调用getA()函数,这时getA()是base class A的版本,并不是derived class B的版本,即 在base class构造期间,virtual函数不是virtual函数

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


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

原因:由“别名(aliasing)”所引起,即有一个以上的方法指涉某对象,一般而言如果某段代码操作指针或引用而它们被用来“指向多个相同类型的对象”,那么就需要考虑这些对象是否为同一个
a[i] = a[j]; //若i j相同,便为自我赋值
*p1 = *p2; //p1,p2可能指向同一个东西
class A {...};
class B:public A{...};
void func(const A &a,B *b); //a,*b可能是同一个对象

证同测试

A &A::operator= (const A &a)
{
    if(this == &a)
        return *this;
    delete p;
    p = new A(*a.p);
    return *this;
}
注意:进行证同测试,可能会引发异常(在new新成员时),最终会导致持有一个指针指向一块被删除的A。但是operator=如果具备异常安全性,往往会自动获得“自我赋值安全”,因此多数情况下我们优先处理异常:
A &A::operator= (const A &a)
{
    A* pa = p; //记住原先的指针p
    p = new A(a.p);
    delete pa;
    return *this;   
}
另一种可以处理异常安全、自我赋值安全的解决方法是copy and swap:
class A{
    ...
    void swap(A &a);
    ...
};
A &A::operator= (const A &a)
{
    A temp(a);
    swap(temp);
    return *this;   
}

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

class B{...};
class A
{
public:
    A(const A& a);
    A& operator =(const A& a);
private:
    string s;
    B b;
};
A此时若进行复制构造,那么将出现 局部拷贝的情况,因为未定义B的复制构造函数。

当调用 derived class的copy 构造函数时,应该确保复制 对象内所有成员变量 以及  base class的所有成分
class C:public A
{
public:
    C(const C& c);
    C& operator =(const C& c);
private:
    int x;
};
C::C(const C& c)
    :A(c),x(c.x) {}
C& C::operator =(const C& c)
{
    A::operator =(c);
    x = c.x;
    return *this;
}


条款13:以对象资源管理

使用auto_ptr智能指针来管理对象资源
特点
1.获得资源后立刻放进管理对象(RAII--Resource Acquisition is Initialization)
2.管理对象运用析构函数确保资源被释放

PS
1.若通过copy构造函数、copy assignment操作符复制,它们会变成null,而复制所得的指针将取得资源的唯一拥有权
A* createFactory(); //工厂函数
auto_ptr<A> p1(createFactory());
auto_ptr<A> p2(p1); //p2指向对象,p1为null
因此auto_ptr无法发挥复制等功能,所以当这种情况出现时,替代者为 shared_ptr
A* createFactory(); //工厂函数
shared_ptr<A> p1(createFactory());
shared_ptr<A> p2(p1); //p1,p2指向同一个对象
2.auto_ptr、shared_ptr都是在析构函数内做delete,而不是delere[ ],因此在智能指针处使用array是错误的


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

什么是RAII??

RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

什么时候需要使用RAII?

new一个变量的时候可能会忘记delete,因此如果我们将构造、析构这一过程封装起来,即委托给一个对象来处理,那么就可以很好的解决这一问题。

当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

示例代码:

class ArrayOperation 
{ 
public : 
    ArrayOperation() 
    { 
        m_Array = new int [10]; 
    } 
 
    void InitArray() 
    { 
        for (int i = 0; i < 10; ++i) 
        { 
            *(m_Array + i) = i; 
        } 
    } 
 
    void ShowArray() 
    { 
        for (int i = 0; i <10; ++i) 
        { 
            cout<<m_Array[i]<<endl; 
        } 
    } 
 
    ~ArrayOperation() 
    { 
        cout<< "~ArrayOperation is called" <<endl; 
        if (m_Array != NULL ) 
        { 
            delete[] m_Array;  
            m_Array = NULL ; 
        } 
    } 
 
private : 
    int *m_Array; 
}; 
 
bool OperationA(); 
bool OperationB(); 
 
int main() 
{ 
    ArrayOperation arrayOp; 
    arrayOp.InitArray(); 
    arrayOp.ShowArray(); 
    return 0;
}
RAII对象发生复制时,出现的可能:

1.禁止复制

见条款6

2.对底层资源使用“引用计数法”

使用shared_ptr来计数

3.复制底部资源


4.转移底部资源的拥有权


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

Investment* createInvestment();
shared_ptr<Investment> pInv(createInvestment());
int func(const Investment* pi);

int days = func(pInv); //报错,func需要的是Investment*指针,但传入的却是shared_ptr<Investment>。
将RAII class对象转换为其所包含的原始资源,方法:
(1)显示转换(get函数)
auto_ptr、shared_ptr提供get函数, 返回智能指针内部的原始指针(的复件)
(2)隐式转换(operator())
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
	explicit Font(FontHandle fh) :f(fh) {}
	~Font() {releaseFont(f);}
	//隐式转换函数
	operator FontHandle() const {return f;}
private:
	FontHandle f;
}
//进行隐式转换
Font f(getFont());


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

new --------- delete
new [] ----------delete [] 

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

int num();
void processA(shared_ptr<A> pa,int num);
//现在调用processA函数
processA(new A, num());
由于shared_ptr构造函数是explicit的,无法进行隐式转换,因此需要写成
processA(shared_ptr<A>(new A), num());
但此时遇到的新问题就是执行顺序,该过程要做三件事:
1.调用num()
2.执行new A
3.调用shared_ptr构造函数
可以确定的时,2必在3之前完成,但1的执行顺序不确定,可能出现2 1 3的情况,那么如果num()调用出现异常,则new出的指针无法进入到智能指针中,会出现资源泄漏。
解决方法就是分离语句:
//语句单独出来,不会造成泄漏
shared_ptr<A> pa(new A);
processA(pa, num());

总结:以独立语句将newed对象置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏

猜你喜欢

转载自blog.csdn.net/sinat_25394043/article/details/80095888