Effective C++读书笔记

1 尽量以const,enum,inline替换#define

​ #define不能够用来定义class专属常量,也不能够提供任何封装性,而const成员变量和enum都可以做到使用常量可能比使用#define导致较小量的目标码,宏只是简单的替换。

​ enum的行为某方面比较像#define而不像const。例如取一个const的地址是合法的,而取一个enum或#define的地址不合法;编译器可能为“整数型const对象”设定另外的存储空间,但绝不会为enum和#define做非必要的内存分配。

static const int iNum = 5;
OR static const int iNum; const int Cls::iNum = 5;
OR enum { eNum = 5 };

​ 对于形似函数的宏(macros),最好改用inline函数替换#define。

2 尽可能使用const

​ 将某些东西声明为const可帮助编译器侦测出错误用法,const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

​ const成员函数不可更改对象内的任何non-static成员变量,可以更改static成员变量(属于类而非对象)。编译器强制实施bitwise constness,const修饰的对象不可调用非const成员。

​ bitwise const:const成员函数不更改对象之任何成员变量;

​ logical const:const成员函数可以修改对象内的某些bits,但只有客户端侦测不出的情况下才可如此。如指针隶属于对象,const成员函数可更改“指针所指物”(测试结果:更改指针所指内容时编译不报错,运行时出现coredump)。

​ eg. char* pcMem为类成员变量,其所在对象用const修饰时表明该指针为常量,即指针所存地址即指向不可更改,但所指物可更改。

​ 若要使字符串为常量,最好使用std::string 。

​ 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

const_cast:移除const 
eg. const_cast<char&> fun(); 		//将fun()返回值的const移除
static_cast:添加const
eg. static_cast<const Text&>(*this) //为*this加上const

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

​ 为内置类型进行手动初始化,因为c++不保证初始化他们。

​ 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和他们在类中的声明次序相同。

​ 比如初始化一个string,使用赋值时要先调用default构造函数初始化自身,再调用copy assignment操作符对其赋予新值。而使用成员初值列,则直接调用copy构造函数进行初始化。

​ 为免除“跨编译单元(单一源文件加上其头文件)之初始化次序”问题,应用local static 对象(函数内的static对象)替换non-local static对象(global对象、定义于namespace作用域内的对象、 在classes内或在file作用域内声明为static的对象)。

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

​ 编译器可以暗自为class创建default构造函数、copy构造函数、析构函数以及copy assignment操作符operator=。

​ 当类中有reference成员或const成员或其基类copy assignment操作符为private,编译器将拒绝为该类生成一个copy assignment操作符(copy构造函数类似,不确定是否如此)。

5 若不想使用自动生成的函数就该明确拒绝

​ 若不想使用编译器自动生成的函数(default构造函数、copy构造函数、析构函数以及copy assignment操作符operator=),就该明确拒绝:将相应的成员函数如copy构造函数和copy assignment操作符声明为private并且不实现。另外对于member函数和friend函数,为将错误提前暴露在编译阶段,可将基类相应函数声明private。

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

​ polymorphic(带多态性质的) base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

​ 一个具备多态性的base class的析构函数若没有声明为virtual,delete该基类指针(指向其derived class对象)会使得derived class析构函数没有被调用,derived class成分没有被销毁,而其base class成分被销毁,从而导致资源泄露。

​ base classes不一定要声明一个virtual析构函数, 一般情况下需要声明一个virtual析构函数。

​ 声明一个pure virtual析构函数必须提供一份定义。

​ classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。若此处声明virtual析构函数,则会因为多了虚表指针(指向由函数指针构成的数组)而增加其对象大小。

7 别让异常逃离析构函数

​ 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序:

// 在析构函数内
try 
{
	fun(); //可能抛出异常的函数
}
catch (std::exception &e)
{
	std::cout << e.what() << std::endl; //打印异常信息
	std::abort(); 						//结束程序,删除此句也可使析构函数不传播异常
}

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

​ 相比上述解法,下面的策略更佳:

// 在类中
void close()
{
    fun(); 		//供客户使用的函数
    closed = true;
}
private:
	bool closed;

// 在析构函数内
if (!closed)
{
    try 
    {
    	fun(); //可能抛出异常的函数
    }
    catch (std::exception &e)
    {
        std::cout << e.what() << std::endl; //打印异常信息
        std::abort(); 						//结束程序,删除此句也可使析构函数不传播异常
    }
}

​ 如此,客户需要对某个操作函数运行期间抛出的异常做出反应时,可自行调用classname.close(),并用try…catch包起来。

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

​ 因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。

​ 场景:塑模股市交易如买进、卖出等,这样的交易一定要经过审计,所以每创建一个交易对象,在审计日志中也需要创建一笔适当记录,怎么设计比较好呢?

​ 日志记录会因交易不同而不同,因而将日志记录标记为virtual函数理所应当,为使创建一种交易的同时在审计日志中创建一笔记录,在构造函数中调用virtual函数则大错特错。比较好的做法是“去掉日志记录函数的virtual的属性,令derived class将必要的构造信息向上传递至base class构造函数”,构造信息经过处理如包装成一个static(必须为static)函数后进行传递往往比较方便,也比较可读。

9 令operator=返回一个reference to *this

​ 协议:为了实现“连锁赋值”,如x=y=z=15,赋值操作符必须返回一个reference指向操作符的左侧实参。如下所示。

classs Widget
{
	/* 这只是个协议,并无强制性,不这样写,代码一样可以编译成功,但这份协议被所有内置类型和标准程序库提供的类型(string/vector/complex)共同遵守 */
	Widget& operator=(const Widget& rhs) 
    {
		return *this;
    }
};

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

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

class Bitmap
{};
class Widget
{
public:
	Widget& operator=(const Widget& rhs);
private:
	Bitmap* pb; //指向一个从heap分配而得的对象,即malloc或new出的内存
};

/* 一份不安全的operator=实现版本,当*this和rhs是同一个对象时,pb将指向一个已被删除的对象 */
Widget& Widget::operator=(const Widget& rhs) 
{
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

/* 技术1:比较“来源对象”和“目标对象”的地址 */
Widget& Widget::operator=(const Widget& rhs) 
{
    if (this == &rhs) return *this; //证同测试(identity test),如果是自我赋值则不做任何事
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

/* 技术2:精心周到的语句顺序 */
Widget& Widget::operator=(const Widget& rhs) 
{		
    Bitmap* pOrig = pb;         //先保存原先的pb
    pb = new Bitmap(*rhs.pb);	//令pb指向*pb的一个副本
    delete pOrig;				//删除原先的pb
    return *this;
}

/* 技术3:copy-and-swap技术(为原件做出一份副本,然后交换当前对象和副本的数据,原件保持不变)*/
// 类Widget中加上 
void swap(Widget& rhs);     //交换*this和rhs的数据
/* 若入参为value形式,则入参即是副本,无需另外制作副本了
 * 但是其为了少写一行代码而牺牲了清晰性,并且将“copying动作”从函数本体内移至“函数参数构造阶段”可令编译器	* 有时生成更高效的代码
 */
Widget& Widget::operator=(const Widget& rhs) 
{		
    Widget temp(rhs);           //为rhs数据制作一份副本,copy构造函数会另外开辟一块内存
    swap(temp);					//将*this数据和上述副本的数据交换
    return *this;
}

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

11 复制对象时勿忘其任何一个成分

​ copying函数(copy构造函数和copy assignment操作符)应该确保

​ 1) 复制所有local成员变量;

​ 2) 调用所有base classes内的适当的copying函数。

Son::Son(const Son& rhs)
	:Father(rhs),       //Father:基类
	member(rhs.member)  //member:int型成员变量
{}
Son& Son::operator=(const Son& rhs)
{
    Father::operator=(rhs);
    member = rhs.member;
    return *this;
}

​ 不要尝试以某个coping函数实现另一个coping函数。应该将共同机能放进第三个函数中,这个函数往往是private而且常被命名为init,并有两个coping函数共同调用。

12 以对象管理资源

​ 智能指针(C++11版本引入unique_ptr、shared_ptr、weak_ptr,auto_ptr被废弃)就是一种范例,项目中版本为C++98(引入auto_ptr),一般不使用智能指针。

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

​ 1)获得资源后立刻放进管理对象(managing object)内;
​ 2)管理对象运用析构函数确保资源被释放。

​ tr1(Technical Report 1)是一份规范,描述加入C++标准程序库的诸多新机能,以template实现。

​ 为防止资源泄露,可使用RAII(Resource Acquisition Is Intialization;资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中使用delete而非delete[]释放资源。

​ 两个常用的RAII classes分别是tr1::shared_ptr(一种reference-counting smart pointer;RCSP,也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它是自动删除该资源,但无法打破环状引用,如两个已经没有被使用的对象彼此互指)和auto_ptr(智能指针)。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向NULL。

/* 永远只有一份指向该对象的auto_ptr */

/* Investment* createInvestment()创建对象初始化智能指针pInv1 */
std::auto_ptr<Investment> pInv1(createInvestment()); 
/* pInv2指向对象,而pInv1被设为NULL */
std::auto_ptr<Investment> pInv2(pInv1);  
/* 现在pInv1指向对象,而pInv2被设为NULL */
pInv1 = pInv2;                                       

/* pInv1指向createInvestment返回的对象 */
std::tr1::shared_ptr<Investment> pInv1(createInvestment());  
/* pInv1和pInv2指向同一个对象 */
std::tr1::shared_ptr<Investment> pInv2(pInv1);   
/* 无任何改变 */
pInv1 = pInv2;                                               

​ 如何精巧制作自己的资源管理类,且看下述13和14两条。

13 在资源管理类中小心copying行为

​ 复制RAII对象必须一并复制它所管理的资源,所以资源的copying的复制行为决定RAII对象的copying行为。一般的RAII class copying行为主要有下述前两种:

​ 1)禁止复制,如条款5所述;
​ 2)对底层资源祭出“引用计数法”(reference-count),如tr1::shared_ptr;
​ 3)复制底部资源,要进行“深度拷贝”,即复制资源管理对象同时也复制其所包覆的资源,如复制指针时,指针所指内存也要复制;
​ 4)转移底部资源的拥有权,即保持永远只有一个RAII对象指向一个原始资源(raw resource),如auto_ptr;

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

​ APIs往往要求访问原始资源(raw resource),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法,比如提供get方法。

​ 对原始资源的访问,即需要一个函数将RAII class对象转换为其所内含之原始资源。有两个做法可以达成目标:显示转换和隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

/* 一组C API */
FontHandle getFont();
void releaseFont(FontHandle fh);

/* 显示转换 */

/* RAII class */
class Font
{
public:
    explicit Font(FontHandle fh) // 获得资源,将构造函数声明为explicit意味着禁止隐式转换 
        : f(fh)					 // 采用pass-by-value,因为C API是这么做的
    {}
    ~Font()						 // 释放资源
    {
        releaseFont(f);
    }
public:
    FontHandle get() const       // 显示转换函数
    {
        return f;
    }
private:
    FontHandle f;                // 原始字体资源
};
void Fun(FontHandle fh); 

Font f(getFont());
Fun(f.get());  // 将Font显式转换为FontHandle

/* 隐式转换 */

/* RAII class */
class Font
{
public:
    Font(FontHandle fh)
    	: f(fh)
    {}
    ~Font()						 // 释放资源
    {
        releaseFont(f);
    }
public:
	operator FontHandle() const  // 隐式转换函数
    {
        return fh;
    }
private:
	FontHandle fh;
};
void Fun(FontHandle fh);

Font f(getFont());
Fun(f);  // 将Font隐式转换为FontHandle

FontHandle f0 = f; // 原意是拷贝f,却意外的将f隐式转换为FontHandle,然后才复制它,此为浅拷贝

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

std::string* strPtr1 = new std::string;      ------> delete strPtr1;
std::string  strPtr2 = new std::string[100]; ------> delete[] strPtr2;

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

processWidget(std::tr1::shared_ptr<Widget>(new Widget), fun());

​ 编译器有可能选择下述顺序执行上述函数参数:

​ 1)执行"new Widget";
​ 2)调用fun;
​ 3)调用tr1::shared_ptr构造函数。

​ 若调用fun()产生异常,则"new Widget"返回的指针会遗失,在对processWidget的调用过程中可能引发难以察觉的资源泄露。

​ 解决办法:使用独立语句,即

/* 可指定“删除器”。 
 * shared_ptr引用计数为0时,调用函数unlock()。若不指定则直接调用delete。
 */
std::tr1::shared_ptr<Widget> pw(new Widget); 
OR std::tr1::shared_ptr<Widget> pw(new Widget, unlock);
processWidget(pw, fun());

17 让接口容易被正确使用,不易被误用

​ 好的接口很容易被正确使用,不容易被误用。应在所有接口中努力达成这些性质。

​ “促进正确使用”的办法包括接口的一致性,比如每个STL容器都有一个名为size的成员函数,以及与内置类型的行为兼容,比如a和b都是int,那么对a*b赋值不合法。

​ “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

​ tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DDL问题(对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁),可被用来自动解除互斥锁(mutex)。

18 设计class犹如设计type

​ 新的对象应该如何被创建和销毁?单例或者其他?

​ 对象的初始化和对象的赋值该有什么样的差别?见条款3。

​ 新type的对象如果被pass by value,意味着什么?

​ 什么是新type的“合法值”?

​ 若新type继承自某些既有的class,那就要受到那些class设计的束缚;若新type允许被其他class继承,那就要将析构函数声明为virtual。

​ 新type需要什么样的转换?见条款14。

​ 什么样的操作符和函数对此type而言是合理的?哪些应该是member函数,哪些应该不是?

​ 什么样的标准函数应该被驳回?这些函数应声明为private。

​ 谁该取用新type的成员?

​ 什么是新type的“未声明接口”?如何保证效率、异常安全性、多任务锁定、动态内存管理等?

​ 新type有多么一般化?若要定义一整个type家族,就应该定义一个新的class template。

19 宁以pass-by-reference-to-const替换pass-by-value

​ 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。

fun(Son s); // pass by value
Father f;
/* fun函数中不管传进来的是什么类型,参数s就像是一个Father对象 
 * 函数内调用的成员均隶属Father,这就是切割问题
 */
fun(f); 

​ 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

20 必须返回对象时别妄想返回其reference

​ 绝不要返回point或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象(要使用只能使用这种,但要求内部实现资源管理),或返回point或reference指向一个local static对象而有可能同时需要多个这样的对象。

21 将成员变量声明为private

​ 切记将成员变量声明为private。这可赋予客户访问数据的一致性(唯一能够访问对象的办法是通过成员函数,不用考虑怎么访问)、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

​ 从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。protected并不比public更具封装性。

22 宁以non-member、non-friend替换member函数

​ 这种做法可增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

​ 面向对象守则要求数据尽可能被封装。愈多东西被封装,愈少人可以看到它,我们就有愈大的弹性去变化它。

​ 让函数“成为class的non-member"并不意味这它“不可以是另一个class的member”,比较自然的做法是让该函数成为一个non-member函数并位于class所在的同一个namespace内。这正是C++标准程序库的组织方式。

23 若所有参数皆需类型转换则为此采用non-member函数

​ 只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。

class Rational
{
public:
    Rational(int numerator = 0, int denomiator = 1);       // 刻意不为explicit,允许隐式转换
    int numerator() const;
    int denominator() const;
public:
	const Rational operator* (const Rational& rhs) const;  // 这种写法必须为member函数
}

/* 等价于 result = oneHalf.operator*(2)
 * 在编译器看来,这相当于const Rational temp(2); result = oneHalf * temp,   
 * 2可以隐式转换为Rational
 */
result = oneHalf * 2;

/* 等价于 result = 2.operator*(oneHalf)
 * 2不在参数列内,错误!
 */
result = 2 * oneHalf 

/* 为使上述两式等价,应改为 */
const Rational operator* (const Rational& lhs, const Rational& rhs) // non-member函数
{
    return Rational(lhs.numerator() * rhs.numerator(), 
    				lhs.denominator() * rhs.denominator());
}

​ member函数的反面是non-member函数,不是friend函数。friend函数能避免就避免,因为朋友带来的麻烦往往多过其价值。

24 考虑写出一个不抛异常的swap函数

/* 缺省的swap实现 */
namespace std
{
	template<typename T>
	void swap(T& a, T&b) // 置换a和b的值,只要T类型支持copying
    {
        T temp(a);
        a = b;
        b = temp;
    }
}	

​ 对于内含指针的类或只是想交换对象中一部分数据,使用缺省的swap缺乏效率,此时应提供一个swap成员函数,并确定这个函数不抛出异常。

​ 对于classes(而非templates),应特化std::swap,也可以提供一个non-member swap用来调用swap成员函数,或两者都提供,但编译器会首选non-member swap。

​ 不管什么情况下,特化std::swap最好提供。因为当使用std::swap()调用swap函数时,编译器只认std内的swap,包括其特化版本。

/* 特化std::swap */
namespace std
{
	/* 表示这是std::swap的一个全特化(total template specialization)版本 */
	template<>                  
	void swap<B>(B& b1, B& b2)  // 函数名之后的“<B>”表示这一特化版本系针对“T是B”而设计
    {
    	b1.swap(b2);
    }
}

​ 这种做法与STL容器有一致性。因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。

​ 对于class templates,我们只能在这个class template所在的命名空间内提供一个non-member swap,并令它调用swap成员函数。

​ C++只允许对class templates偏特化(partially specialize),偏特化一个function template的一般做法是为它添加一个重载版本,但要注意不可在std内加入某些对std而言全新的东西。

/* 偏特化std::swap */

/* 错误的做法 */
namespace std
{
	template<typename T>                  
	void swap<B<T>>(B<T>& b1, B<T>& b2)   //错误!不合法!
    {
    	b1.swap(b2);
    }
}

/* 重载swap函数(参数不一样)*/
template<typename T>
void swap(B<T>& b1, B<T>& b2)   //正确
{
	b1.swap(b2);
}

​ 调用swap时应针对std::swap使用using申明式,然后调用swap并且不带任何“命名空间资格修饰”,这样编译器可选择调用swap的最佳版本。

25 尽可能延后变量定义式的出现时间

​ 如此可增加程序的清晰度并改善程序效率。一般情况下,延后到这份定义能够给它初值实参为止。对于循环,具体问题具体分析。

26 尽量少做转型动作

/* 将expression转型为T,C风格的转型,是为“旧式转型” */
(T)expression <=> T(expression) 

/* C++的新式转型 */
const_cast<T>(expression)       /* 去除常良性,仅此一种方法 */
dynamic_cast<T>(expression)     /* 执行“安全向下转型”,即用来决定某对象是否归属继承体系中的某个类  								  *	型,是唯一无法由旧式语法执行的动作
								 */
reinterpret_cast<T>(expression) /* 执行低级转型,动作结果取决于编译器,不可移植,
								 * 如将一个point to int转型为一个int
								 */
static_cast<T>(expression)      /* 用来强迫隐式转换(implicit conversion),
								 * 如non-const->const,int->double,void*->typed pointer,								   * pointer-to-base->pointer-to-derived等
								 * 不做类型检查
								 */

​ 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

​ 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。

​ 宁可使用C++ style转型,也不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

27 避免返回handles指向对象内部成分

​ 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

​ “虚吊号码牌”:handles所指东西不复存在,这种“不复存在的对象”最常见的来源就是函数返回值。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值