《Effective C++》改善程序与设计的55个具体做法

前言

学习"《Effective C++》改善程序与设计的55个具体做法"这本书以及简单笔记记录。

内容

1.让自己习惯C++

  • 条款01:视C++为一个语言联邦
    C++是一个多重泛型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程的语言。

  • 条款02:尽量以const,enum,inline替换 # define
    宁可以编译器替换预处理器。
    "enum hack"的实用性。
    建议:
    对于单纯常量,最好以const对象或enums替换#define。
    对于形似函数的宏,最好改用inline函数替换#define。

  • 条款03:尽可能使用const
    对于const关键字的详细描述进行单独说明,见传送门。

  • 条款04:确定对象被使用前已被初始化(其中某些细节没有看懂)

2.构造/析构/赋值运算

  • 条款05:了解C++默认编写并调用哪些函数。
    编译器可以暗自为class创建default构造函数,copy构造函数、copy assignment操作符,以及析构函数。

  • 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
    某些对象独一无二时,就应该禁止对象拷贝动作(拷贝赋值、拷贝构造),我们可以设定默认拷贝函数为private(所有编译器产生的函数都是public,这种做法对于member函数和friend函数还是可以调用的)。为了防止这点我们可以专门设计一个阻止coping动作而设计的base class内。

class Uncopyable {
protected:					//允许derived(派生的)对象构造和析构
	Uncopyable () {  }
	~Uncopyable () {  }
private:
	Uncopyable(const Uncopyable&)//但阻止copying
	Uncopyable& operator=(const Uncopyable);
}
class HomeForSale::private Uncopyable {                                                                                                                                                                                                                                                                                                                                                                                                                                
	...
};

这样即使是HomeForSale类的member函数和friend函数都无法拷贝了。

  • 条款07:为多态基类声明virtual析构函数
    当使用基类指针指向子类对象,则必须将基类的析构函数定义为virtual声明(删除保持一致)
SpecialString* ps = new SpecialString("Impending Doom");
std::string* ps;
...
ps = pss;
...
delete ps;	//未有定义!现实中*ps的SpecialString资源会泄露,因为SpecialString析构函数没被调用

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

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

    因为析构函数吐出异常就是危险,总会带来"过早结束程序"或"发生不明确行为"的风险。

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

  • 条款10:令operator=返回一个reference to this
    该协议适应于+=、-=、
    =等等运算符。

Widget& oeprator=(const Widhet& rhs)//返回类型是reference,指向当前对象
{
	...
	return *this;		//返回左侧类型,可以连环操作
}
  • 条款11:在operator=中处理"自我处理"
  1. 证同测试:
	Widget& Widget::operator=(const Widget& rhs)
	{
		if (this == &rhs) return *rhs;		//证同测试
		
		delete db;
		pb = new Bitmap(*rhs.pb);
		return *this;
	}
  1. 复制副本,在复制之前不进行删除
	Widget& Widget::operator=(const Widget& rhs)
	{
		Bitmap* pOrig = pb;	//记住原先的pb
		pb = new Bitmap(*rhs.pb);//今pb指向*pb的一个复件(副本)
		delete pObrig;				//删除原先的pb
		return *this;
	}
  1. 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;
	}

请记住:
确保当对象自我赋值时operator=有良好行为,其中技术包括比较"来源对象"和"目标对象"的地址、精心周到的语句顺序、以及copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一对象时,其行为仍然正确。

  • 条款12:复制对象时勿忘其·每一个成分(Copy all parts of an object)

3.资源管理

C++程序中最常见的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏),但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(files descriptors)、互斥锁(mutex locks)、图像界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当你不再使用它时,必须将它归还给系统。

  • 条款13:以对象管理资源

    • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
    • 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
  • 条款14:在资源管理类中小心copying行为

    • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
    • 普通而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能实现。
  • 条款15:在资源管理类中提供对原始资源的访问

    • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的办法。
    • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
  • 条款16:成对使用new和delete时要采取相同形式
    如果你在new表达式中使用[ ],必须在相应的delete表达式中也使用[ ]。如果你在new表达式中不使用[ ],一定不要在相应的delete表达式中使用[ ]。

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;		//删除一个对象
delete [] stringPtr2;	//删除一个由对象组成的数组
  • 条款17:以独立语句将newed对象置入智能指针
    原始版:
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority())
//在"资源被创建(经由'new Widget')"和"资源被转换为资源管理对象"两个时间点之间有可能发生异常干扰。

解决办法:

std::tr1::shared_ptr<Widget> pw(new Widget); 
processWidget(pw,priority())//这个调用动作绝不至于造成泄漏

4.设计与声明

  • 条款18:让接口容易被正确使用,不易被误用
    好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
    "促进正确使用"的办法包括接口的一致性,以及于内置类型的行为兼容。
    "阻止误用"的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
    trl::shared_ptr支持定制型删除器(custom deleter)。这颗防范DLL问题,可被用来自动解除互斥锁等等。

  • 条款19:设计class犹如设计type

    • 新type的对象应该如何被创建和销毁?
    • 对象的初始化和对象的赋值该有什么样的差别?
    • 新type的对象如果被passed by value(以值传递),意味着什么?
    • 什么是新type的"合法值"?
    • 你的新type需要配合某些继承图系吗?
    • 你的新type需要什么样的转换?
    • 什么样的操作符和函数对比新type而言是合理的?
    • 什么样的标准函数应该驳回?
    • 谁该区的新type的成员?
    • 什么是新type的"未声明接口"?
    • 你的新type有那么一般化?
    • 你真的需要一个新的type吗?
  • 条款20:宁以pass-by-reference-to-const替换pass-by-value

    • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
    • 以上规则并不 适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
  • 条款21:必须返回对象时,别妄想返回其reference

  • 条款22:将成员变量声明为private

    • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
    • protected并不比public更具有封装性。
  • 条款23:宁以non-member、non-friend替换member函数
    这样做可以增加封装性、包裹弹性和机能扩充性

  • 条款24:若所有参数皆需类型转换,请为此采用non-member函数

  • 条款25:考虑写出一个不抛异常的swap函数

5.实现

  • 条款26:尽可能延后变量定义式的出现时间
  • 条款27:尽可能少做转型动作
class Widget{
public:
	explicit Widget(int size);
	...
};

void doSomeWork(const Widget& w);
doSomeWork(Widget(15));		//以一个int加上"函数风格"的转型动作创建一个Widget
doSomeWork(static_cast<Widget>(15));	//以一个int加上"C++风格"的转型动作创建一个Widget
  • 条款28:避免返回handles指向对象内部成分

  • 条款29:为"异常安全"而努力是值得的

    • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分三种可能的保证:基本型、强烈型、不抛异常型。
    • "强烈保证"往往能够以copy-and-swap实现出来,但"强烈保证"并非对所有函数都可实现或具备现实意义。
    • 函数提供的"异常安全保证"通常最高只等于其所调用之各个函数的"异常安全保证"中的最弱者。
  • 条款30:透彻了解inlining的里里外外

    • 将大多数inlining限制在小型、被频繁调用的函数身上。
    • 不要只因为function templates出现在头文件,就将它们声明为inline。
  • 条款31:将文件间的编译依存关系将至最低(不太理解)

    • 支持"编译依存性最小化"的一般构想是:想依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
    • 程序库头文件应该从"完全且仅有声明式"的形式存在。这种做法不论是否涉及templates都适用。

6.继承与面向对象设计

  • 条款32:确定你的public继承塑模出is-a关系(public继承)

  • 条款33:避免遮掩继承而来的名称

    • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
    • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。
  • 条款34:区分接口继承和实现继承

    • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
    • pure virtual函数只具体指定接口继承。
    • 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
    • non-virtual函数具体指定接口继承以及强制性实现继承。
  • 条款35:考虑virtual函数以外的其他选择

7.模板与泛型编程

8.定制new和delete

9.杂项讨论

后记

持续更新!

你可能感兴趣的:(技术书籍,c++,开发语言,后端)