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

C/C++/C++11 专栏收录该内容
207 篇文章 76 订阅

Scott Meyers大师Effective三部曲:Effective C++、More Effective C++、Effective STL,这三本书出版已很多年,后来又出版了Effective Modern C++。

More Effective C++的笔记见:https://blog.csdn.net/fengbingchun/article/details/102990753

Effective STL的笔记见:https://blog.csdn.net/fengbingchun/article/details/103223914

Effective Modern C++的笔记见:https://blog.csdn.net/fengbingchun/article/details/104136592

这里是Effective C++的笔记:

1. 视C++为一个语言联邦(View C++ as a federation of languages)

最简单的方法将C++视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通例都倾向于简单、直观易懂、并且容易记住。总共只有4个次语言:

(1). C:说到底C++仍是以C为基础。区块(blocks)、语句(statements)、预处理(preprocessor)、内置数据类型(build-in data types)、数组(arrays)、指针(pointers)等统统来自C。许多时候C++对问题的解法不过就是较高级的C解法。

(2). Object-Oriented C++:classes(包括构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)…等等。

(3). Tesplate C++:这是C++的泛型编程(generic programming)部分。实际上由于templates威力强大,它们带来崭新的编程范型(programming paradigm),也就是所谓的template metaprogramming(TMP,模板元编程)。

(4). STL:是个template程序库。它对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合与协调。

对内置(也就是C-like)类型而言pass-by-value通常比pass-by-reference高效,但当你从C part of C++移往Object-Oriented C++,由于用户自定义(user-defined)构造函数和析构函数的存在,pass-by-reference-to-const往往更好。运用Template C++时尤其如此,因此此时你甚至不知道所处理的对象的类型。

请记住:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分

2. 尽量以const, enum, inline替换#define(Prefer consts, enums, and inlines to #defines)

class GamePlayer {
private:
	static const int NumTurns = 5; // 常量声明式
	int scores[NumTurns]; // 使用该常量
};

// 注意:由于class常量已在声明时获得初值(设初值为5),因此定义时不可以再设初值
const int GamePlayer::NumTurns; // NumTurns的定义

void f(int) {}
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) // 不推荐
template<typename T>
inline void callWithMax(const T& a, const T& b) // 推荐
{
	f(a > b ? a : b);
}

int test_item_2()
{
	#define ASPECT_RATIO 1.653 // 不推荐
	const double AspectRatio = 1.653; // 推荐

	{const char* const authorName = "Scott Meyers";} // 推荐
	const std::string autorName("Scott Meyers"); // 更推荐

	return 0;
}

这个条款或许改为”宁可以编译器替换预处理器”比较好,因为或许#define不被视为语言的一部分。

对浮点常量(floating point constant)而言,使用常量可能比使用#define导致较小量的码,因为预处理器”盲目地将名称ASPECT_RATIO替换为1.653”可能导致目标码(object code)出现多份1.653,若改用常量AspectRatio绝不会出现相同情况。

以常量替换#define,有两种特殊情况:

第一种是定义常量指针(constant pointers),由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。例如若要在头文件内定义一个常量的(不变的)char*-based字符串,你必须写const两次。string对象通常比其前辈char*-base合宜。

第二种是class专属长量。为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member);而为确保此常量至多只有一份实体,你必须让它成为一个static成员。

我们无法利用#define创建一个class专属常量,因为#define并不重视作用域(scope)。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味#define不仅不能够用来定义class专属常量,也不能够提供任何封装性,也就是说没有所谓private #define这样的东西。而当然const成员变量是可以被封装的,NumTurns就是。

另一个常见的#define误用情况是以它实现宏(macro)。宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。无论何时你写成这种宏,你必须记住为宏中的所有实参加上小括号。

请记住:(1). 对于单纯常量,最好以const对象或enum替换#define;(2). 对于形式函数的宏(macro),最好改用inline函数替换#define

3. 尽可能使用const(Use const whenever possible)

class TextBlock {
public:
	TextBlock(const std::string& str) { text = str; }
	// 两个成员函数如果只是常量性(constness)不同,可以被重载
	const char& operator[] (std::size_t position) const // operator[] for const对象
	{ return text[position]; }
	//char& operator[] (std::size_t position) // operator[] for non-const对象
	//{ return text[position]; }
	// 在const和non-const成员函数中避免重复
	char& operator[] (std::size_t position) // 现在只调用const op[]
	{ return const_cast<char&>( // 将const op[]返回值的const移除
		static_cast<const TextBlock&>(*this) // 为*this加上const,调用const op[]
		[position]); }

private:
	std::string text;
};

void print(const TextBlock& ctb) // ctb是const
{
	std::cout<< ctb[0]; // 调用const TextBlock::operator []
}

int test_item_3()
{
	char greeting[] = "Hello";
	{char* p = greeting;} // non-const pointer, non-const data
	{const char* p = greeting;} // non-const pointer, const data
	{char* const p = greeting;} // const pointer, non-const data
	{const char* const p = greeting;} //const pointer, const data

	std::vector<int> vec(1);
	const std::vector<int>::iterator iter = vec.begin(); // iter的作用像个T* const
	*iter = 10; // 没问题,改变iter所指物
	//++iter; // 错误,iter是const

	std::vector<int>::const_iterator cIter = vec.begin(); // cIter的作用像个const T*
	//*cIter = 10; // 错误,*cIter是const
	++cIter; // 没问题,改变cIter

	TextBlock tb("Hello");
	std::cout<< tb[0]; // 调用non-const TextBlock::operator []
	const TextBlock ctb("World");
	std::cout<< ctb[0]; // 调用const TextBlock::operator []
	print(ctb);
	print(tb);

	return 0;
}

你可以用它在classes外边修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。你也可以用它修饰classes内部的static和non-static成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是const。

如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量

STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希望迭代器所指的东西不可被改动(即希望STL模拟一个const T*指针),你需要的是const_iterator

两个成员函数如果只是常量性(constness)不同,可以被重载。

真实程序中const对象大多用于passed by pointer-to-const或passed by reference-to-const的传递结果。

const成员函数不可以更改对象内任何non-static成员变量,称为bitwise const。

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。可以预防那个”没意思的赋值动作”。

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

在const和non-const成员函数中避免重复:转型(casting)。只能non-const成员函数调用const成员函数,不能const成员函数调用non-const成员函数。

请记住:(1).将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。(2).编译器强制实施bitwise constness。(3).当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

4. 确定对象被使用前已先被初始化(Make sure that objects are initialized before they’re used.)

class ABEntry {
public:
	ABEntry(const std::string& name, const std::string& address);
	ABEntry(const std::string& name, const std::string& address, int phones);
	ABEntry();
private:
	std::string theName;
	std::string theAddress;
	int thePhones;
};

ABEntry::ABEntry(const std::string& name, const std::string& address)
{
	// 这些都是赋值(assignments)而非初始化(initializations)
	// 首先调用default构造函数为这些变量设初值,然后立刻再对它们赋予新值
	theName = name;
	theAddress = address;
	thePhones = 2;
}

ABEntry::ABEntry(const std::string& name, const std::string& address, int phones)
	: theName(name), theAddress(address), thePhones(phones) // 这些都是初始化(initializations)
{} // 构造函数本体不必有任何动作

ABEntry::ABEntry() : theName(), theAddress(), thePhones(0) // 调用theName等的default构造函数
{}

读取未初始化的值会导致不明确的行为。

永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。

至于内置类型以外的任何其它东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。别混淆了赋值(assignment)和初始化(initialization)。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。使用所谓的member initialization list(成员初值列)替换赋值动作。

编译器会为用户自定义类型(user-defined types)之成员变量自动调用default构造函数。规定总是在初值列中列出所有成员变量。

如果成员变量是const或reference,它们就一定需要初值,不能被赋值。

许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种classes存在许多成员变量和/或base classes。这种情况下可以合理地在初值列中遗漏那些”赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在”成员变量的初值系由文件或数据库读入”时特别有用。

C++有着十分固定的”成员初始化次序”。次序总是相同:base classes更早于其derived classes 被初始化,而class的成员变量总是以其声明次序被初始化。

所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其它static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用

所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。C++对”定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。消除这个问题,唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是Singleton模式的一个常见实现手法。C++保证,函数内的local static对象会在”该函数被调用期间””首次遇上该对象之定义式”时被初始化。

Singleton模式的介绍参考:https://blog.csdn.net/fengbingchun/article/details/22584107

任何一种non-const static对象,不论它是local或non-local,在多线程环境下”等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段(single-threaded startup portion)手工调用所有reference-returning函数,这个消除与初始化有关的”竞速形势(race conditions)”。

请记住:(1).为内置型对象进行手工初始化,因为C++不保证初始化它们。(2).构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。(3).为免除”跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

5. 了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls)

class Empty {};

int test_item_5()
{
	Empty e1; // default构造函数
	Empty e2(e1); // copy构造函数
	e2 = e1; // copy assignment操作符

	return 0;
}

什么时候empty class(空类)不再是个empty class呢?当C++处理过它之后。是的,如果自己没声明,编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline。唯有当这些函数被需要(被调用),它们才会被编译器创建出来。default构造函数和析构函数主要是给编译器一个地方用来放置”藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。注意,编译器产出的析构函数是个non-virtual,除非这个class的bass class自身声明有virtual析构函数(这种情况下这个函数的虚属性virtualness,主要来自base class)。至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。

如果你打算在一个”内含reference成员”的class内支持赋值操作(assignment),你必须自己定义copy assignment操作符。面对”内含const成员”的classes,编译器的反应也一样。更改const成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况:如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived class生成一个copy assignment操作符。

请记住:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数

6. 若不想使用编译器自动生成的函数,就该明确拒绝(Explicitly disallow the use of compiler-generated functions you do not want)

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

class HomeForSale: private Uncopyable {
// class不再声明copy构造函数或copy assign操作符
};

int test_item_6()
{
	HomeForSale h1;
	HomeForSale h2;
	//HomeForSale h3(h1); // 不会通过编译
	//h1 = h2; // 也不该通过编译

	return 0;
}

请记住:为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法

7. 为多态基类声明virtual析构函数(Declare destructors virtual in polymorphic base classes)

class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
	virtual ~AWOV() = 0; // 声明pure virtual析构函数
};

AWOV::~AWOV() {} // 必须为这个pure virtual析构函数提供一份定义

任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。无端地将所有classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。

vptr(virtual table pointer)指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl----编译器在其中寻找适当的函数指针。

析构函数的运作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。

“给base classes一个virtual析构函数”,这个规则只适用于polymorphic(带多态性质的)base classes身上。这种base classes的设计目的是为了用来”通过base class接口处理derived class对象”。并非所有base classes的设计目的都是为了多态用途。例如标准string和STL容器都不被设计作为base classes使用,更别提多态了。

请记住:(1).polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。(2).Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数

8. 别让异常逃离析构函数(Prevent exceptions from leaving destructors)

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

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

9. 绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)

derived class对象内的base class成分会在derived class自身成分被构造之前先构造妥当。由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束。

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

10. 令operator=返回一个reference to *this(Have assignment operators return a reference to *this)

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

	Widget& operator+= (const Widget& rhs) // 这个协议适用于+=、-=、*=等等
	{
		return *this;
	}

	Widget& operator= (int rhs) // 此函数也适用,即使此一操作符的参数类型不符协定
	{
		return *this;
	}
};

请记住:令赋值(assignment)操作符返回一个reference to *this

11. 在operator=中处理”自我赋值”(Handle assignment to self in operator=)

class Widget11 {
public:
	void swap(Widget11& rhs) {} // 交换*this和rhs的数据

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

int test_item_11()
{
	Widget11 w;
	w = w; // 赋值给自己

	return 0;
}

在operator=函数内手工排列语句(确保代码不但”异常安全”而且”自我赋值安全”)的一个替代方案是,使用所谓的copy and swap技术。

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

12. 复制对象时勿忘其每一个成分(Copy all parts of an object)

void logCall(const std::string& funcName) {} // 制造一个log entry

class Customer {
public:
	Customer(const Customer& rhs)
		: name(rhs.name) // 复制rhs的数据
	{
		logCall("Customer copy constructor");
	}

	Customer& operator= (const Customer& rhs)
	{
		logCall("Customer copy assignment operator");
		name = rhs.name; // 复制rhs的数据
		return *this;
	}

private:
	std::string name;
};

class PriorityCustomer : public Customer {
public:
	PriorityCustomer(const PriorityCustomer& rhs)
		: Customer(rhs), // 调用base class的copy构造函数
		  priority(rhs.priority)
	{
		logCall("PriorityCustormer copy constructor");
	}

	PriorityCustomer& operator= (const PriorityCustomer& rhs)
	{
		logCall("PriorityCustomer copy assignment operator");
		Customer::operator=(rhs); // 对base class成分进行赋值操作
		priority =  rhs.priority;
		return*this;
	}

private:
	int priority;
};

如果你为class添加一个成员变量,你必须同时修改copying函数(包括copy构造函数和copy assignment操作符)。(你也需要修改class的所有构造函数以及任何非标准形式的operator=。如果你忘记,编译器不太可能提醒你。)

任何时候只要你承担起”为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。

令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。令copy构造函数调用copy assignment操作符同样无意义。构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。

如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复。

请记住:(1).Copying函数应该确保复制”对象内的所有成员变量”及”所有base class成分”。(2).不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用

13. 以对象管理资源(Use objects to manage resources)

把资源放进对象内,我们便可依赖C++的”析构函数自动调用机制”确保资源被释放。

auto_ptr是个”类指针(pointer-like)对象”,也就是所谓的”智能指针”,其析构函数自动对其所指对象调用delete。一定要注意别让多个auto_ptr同时指向同一对象,如果真是那样,对象会被删除一次以上。注:在C++11中auto_ptr已经被废弃,用unique_ptr替代。

std::unique_ptr用法参考:https://blog.csdn.net/fengbingchun/article/details/52203664

auto_ptr的替代方案是”引用计数型智慧指针”(reference-counting smart pointer, RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。类似于C++11中的shared_ptr。

std::shared_ptr用法参考:https://blog.csdn.net/fengbingchun/article/details/52202007

请记住:(1).为防止资源泄漏,请使用RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。(2).两个常被使用的RAII classes分别是stdn::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null

14. 在资源管理类中小心copying行为(Think carefully about copying behavior in resource-managing classes)

Copying函数(包括copy构造函数和copy assignment操作符)有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事,否则你得自己编写它们。

请记住:(1).复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。(2).普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其它行为也都可能被实现

15. 在资源管理类中提供对原始资源的访问(Provide access to raw resources in resource-managing classes)

请记住:(1).APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个”取得其所管理之资源”的办法。(2).对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便

16. 成对使用new和delete时要采用相同形式(Use the same form in corresponding uses of new and delete)

int test_item_16()
{
	std::string* stringPtr1 = new std::string;
	std::string* stringPtr2 = new std::string[100];

	delete stringPtr1;    // 删除一个对象
	delete [] stringPtr2; // 删除一个由对象组成的数组

	typedef std::string AddressLines[4]; // 每个人的地址有4行,每行是一个string
	std::string* pal = new AddressLines; // 注意:"new AddressLines"返回一个string*,就像"new string[4]"一样

	//delete pal;    // 行为未有定义
	delete [] pal; // 很好

	return 0;
}

当你使用new(也就是通过new动态生成一个对象),有两件事发生:第一,内存被分配出来(通过名为operator new的函数);第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。

当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个”数组大小记录”的办法就是:由你来告诉它。如果你使用delete时加上中括号(方括号),delete便认定指针指向一个数组,否则它便认定指针指向单一对象。

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

最好尽量不要对数组形式做typedef动作。因为C++标准程序库含有string、vector等template,可将数组的需求降至几乎为零。

请记住:如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]

17. 以独立语句将newed对象置入智能指针(Store newed objects in smart pointers in standalone statements)

class Widget17 {};
int priority() { return 0; }
void processWidget(std::shared_ptr<Widget17> pw, int priority) {}

int test_item_17()
{
	// 执行new Widget17; 调用priority; 调用std::shared_ptr构造函数,它们的执行顺序不确定
	processWidget(std::shared_ptr<Widget17>(new Widget17), priority()); // 可能泄露资源
	
	std::shared_ptr<Widget17> pw(new Widget17); // 在单独语句内以智能指针存储newed所得对象
	processWidget(pw, priority()); // 这个调用动作绝不至于造成泄露

	return 0;
}

请记住:以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏

18. 让接口容易被正确使用,不易被误用(Make interface easy to use correctly and hard to use incorrectly)

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

19. 设计class犹如设计type(Treat class design as type design)

20. 宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)

如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。

请记住:(1).尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。(2).以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当

21. 必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1) : n(numerator), d(denominator)
	{}

private:
	int n, d; // 分子(numerator)和分母(denominator)

// 返回const Rational可以预防"没意思的赋值动作": Rational a, b, c; (a * b) = c;
friend const Rational operator* (const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
};

所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。任何函数如果返回一个reference指向某个local对象,都将一败涂地。(如果函数返回指针指向一个local对象,也是一样。)

请记住:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象

22. 将成员变量声明为private(Declare data members private)

请记住:(1).切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。(2).protected并不比public更具封装性

23. 宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions)

请记住:宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性

24. 若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)

class Rational24 {
public:
	Rational24(int numerator = 0, int denominator = 1) {} // 构造函数刻意不为explicit,允许int-to-Rational24隐式转换
	int numerator() const { return 1; } // 分子(numerator)的访问函数
	int denominator() const { return 2; } // 分母(denominator)的访问函数

	/*const Rational24 operator* (const Rational24& rhs) const
	{
		return Rational24(this->n * rhs.numerator(), this->d * rhs.denominator());
	}*/

private:
	int n, d;
};

const Rational24 operator* (const Rational24& lhs, const Rational24& rhs) // non-member函数
{
	return Rational24(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int test_item_24()
{
	Rational24 oneEighth(1, 8);
	Rational24 oneHalf(1, 2);
	Rational24 result = oneHalf * oneEighth; // 很好
	result = result * oneEighth; // 很好

	result = oneHalf * 2; // 很好,隐式类型转换(implicit type conversion)
	result = 2 * oneHalf; // 错误, only non-member function success

	// 以对应的函数形式重写上述两个式子
	//result = oneHalf.operator*(2); // 很好, only member function success
	//result = 2.operator*(oneHalf); // 错误, only non-member function success

	result = operator*(2, oneHalf); // 错误, only non-member function success

	return 0;
}

无论何时如果你可以避免friend函数就该避免。

请记住:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

25. 考虑写出一个不抛异常的swap函数(Consider support for a non-throwing swap)

class WidgetImpl { // 针对Widget25数据而设计的class
public:
private:
	int a, b, c; // 可能有许多数据,意味复制时间很长
	std::vector<double> v;
};

class Widget25 { // 这个class使用pimpl(pointer to implementation)手法
public:
	Widget25(const Widget25& rhs) {}
	Widget25& operator= (const Widget25& rhs) // 复制Widget25时,令它复制其WidgetImpl对象
	{
		*pImpl = *(rhs.pImpl);

		return *this;
	}

	void swap(Widget25& other)
	{
		using std::swap;
		swap(pImpl, other.pImpl); // 若要置换Widget25就置换其pImpl指针
	}

private:
	WidgetImpl* pImpl; // 指针,所指对象内含Widget25数据
};

// std::swap针对Widget25特化版本
namespace std {
template<>
void swap<effective_cplusplus_::Widget25>(effective_cplusplus_::Widget25& a, effective_cplusplus_::Widget25& b)
{
	a.swap(b); // 若要置换Widget25,调用其swap成员函数
}

}

所谓swap(置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下swap动作可由标准程序库提供的swap算法完成。

一般而言,重载function template没有问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的template,但不可以添加新的template(或class或function或其它任何东西)到std里头。

首先,如果swap的缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。其次,如果swap缺省实现版的效率不足,试着做以下事情:(1). 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值。这个函数不该抛出异常。(2). 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。(3). 如果你正在编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。

请记住:(1).当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。(2).如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap。(3).调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。(4).为”用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西

26. 尽可能延后变量定义式的出现时间(Postpone variable definitions as long as possible)

你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。

请记住:尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率

27. 尽量少做转型动作(Minimize casting)

const_cast通常被用来将对象的常量性移除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。

dynamic_cast主要用来执行”安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。

static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象,或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const_cast才办得到。

请记住:(1).如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无须转型的替代设计。(2).如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。(3). 宁可使用C++-style(新式)转型,不要使用旧式转型(C风格转型)。前者很容易辨识出来,而且也比较有着分门别类的职掌

28. 避免返回handles指向对象内部成分(Avoid returning “handles” to object internals)

class Point { // 这个class用来表述"点"
public:
	Point(int x, int y) {}
	void setX(int newVal) {}
	void setY(int newVal) {}
};

struct RectData { // 这些"点"数据用来表现一个矩形
	Point ulhc; // ulhc = "upper left-hand corner"(左上角)
	Point lrhc; // lrhc = "lower right-hand corner"(右上角)
};

class Rectangle {
public:
	Rectangle(const Point&, const Point&) {}
	Point& upperLeft() const { return pData->ulhc; }
	Point& lowerRight() const { return pData->lrhc; }

	// 有了这样的改变,客户可以读取矩形的Point,但不能涂写它们
	// 但即使如此,也可能导致dangling handles(空悬的号码牌):这种handles所指东西(的所属对象)不复存在
	//const Point& upperLeft() const { return pData->ulhc; }
	//const Point& lowerRight() const { return pData->lrhc; }

private:
	std::shared_ptr<RectData> pData;
};

int test_item_28()
{
	Point coord1(0, 0);
	Point coord2(100, 100);
	const Rectangle rec(coord1, coord2); // rec是个const矩形,从(0,0)到(100,100)

	// upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员,但rec其实应该是不可变的(const)
	rec.upperLeft().setX(50); // 现在rec却变成从(50,0)到(100,100)

	return 0;
}

reference、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个”代表对象内部数据”的handle,随之而来的便是”降低对象封装性”的风险。

通常我们认为,对象的”内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private者)也是对象”内部”的一部分。因此也应该留心不要返回它们的handles。这意味你绝对不该令成员函数返回一个指针指向”访问级别较低”的成员函数。

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

29. 为”异常安全”而努力是值得的(Strive for exception-safe code)

“异常安全”有两个条件:(1).不泄漏任何资源。(2).不允许数据败坏。

异常安全函数(Exception-safe functions)提供以下三个保证之一:

(1).基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。

(2).强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会回复到”调用函数之前”的状态。

(3).不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如int,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

有个一般化的设计策略很典型地会导致强烈保证,这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。但一般而言它并不保证整个函数有强烈的异常安全性。

请记住:(1).异常安全函数(Exception-safe function)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。(2).”强烈保证”往往能够以copy-and-swap实现出来,但”强烈保证”并非对所有函数都可实现或具备现实意义。(3).函数提供的”异常安全保证”通常最高只等于其所调用之各个函数的”异常安全保证”中的最弱者

30. 透彻了解inlining的里里外外(Understand the ins and outs of inlining)

inline void f() {} // 假设编译器有意愿inline“对f的调用”

int test_item_30()
{
	void (*pf)() = f; // pf指向f

	f(); // 这个调用将被inlined,因为它是一个正常调用
	pf(); // 这个调用或许不被inlined,因为它通过函数指针达成

	return 0;
}

inline函数背后的整体观念是,将”对此函数的每一个调用”都以函数本体替换之。

过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内。这样的函数通常是成员函数。friend函数如果被定义于class内,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式前加上关键字inline。

inline函数通常一定被置于头文件内,因为大多数建置环境(build environments)在编译过程中进行inlining,而为了将一个”函数调用”替换为”被调用函数的本体”,编译器必须知道那个函数长什么样子。

inlining在大多数C++程序中是编译期行为。

templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。(这其实也不是世界一统的准则。某些建置环境可以在链接期才执行template具现化。只不过编译期完成具现化动作比较常见。)template的具现化与inlining无关。

大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。

一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。大多数编译器提供了一个诊断级别:如果它们无法将你要求的函数inline化,会给你一个警告信息。为数ining个函数长声明entshit rate

编译器通常不对”通过函数指针而进行的调用”实施inlining,这意味对inline函数的调用有可能被inlined,也可能不被inlined,取决于该调用的实施方式。

实际上构造函数和析构函数往往是inlining的糟糕候选人。

inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个inline函数,客户将”f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。

一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些”一定成为inline”或”十分平淡无奇”的函数身上。慎重使用inline便是对日后使用调试器带来帮助。

请记住:(1).将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。(2).不要只因为function templates出现在头文件,就将它们声明为inline

31. 将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)

标准程序库组件不该被前置声明。

设计策略:

(1). 如果使用object references或object pointers可以完成任务,就不要使用objects。你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用到该类型的定义式。

(2). 如果能够,尽量以class声明式替换class定义式。注意,当你声明一个函数而它用到某个class时,你并不需要该class的定义;纵使函数以by value方式传递该类型的参数(或返回值)亦然。

(3). 为声明式和定义式提供不同的头文件。为了促进严守上述准则,需要两个头文件,一个用于声明式,一个用于定义式。当然,这些文件必须保持一致性,如果有个声明式被改变了,两个文件都得改变。

Handle classes(使用pimpl idiom(pimpl是”pointer to implementation”的缩写))和Interface classes(特殊的abstract base class(抽象基类))解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性(compilation dependencies)。

在Handle classes身上,成员函数必须通过implementation pointer取得对象数据。那会为每一次访问增加一层间接性。而每一个对象消耗的内存数量必须增加implementation pointer的大小。最后,implementation pointer必须初始化(在Handle class构造函数内),指向一个动态分配得来的implementation object,所以你将蒙受因动态内存分配(及其后的释放动作)而来的额外开销,以及遭遇bad_alloc异常(内存不足)的可能性。

至于interface classes,由于每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃(indirect jump)成本。此外interface class派生的对象必须内含一个vptr(virtual table pointer),这个指针可能会增加存放对象所需的内存数量----实际取决于这个对象除了interface class之外是否还有其它virtual函数来源。

最后,不论handle classes或interface classes,一旦脱离inline函数都无法有太大作为。函数本体为了被inlined必须(很典型地)置于头文件内,但handle classes和interface classes正是被设计用来隐藏实现细节如函数本体。

然而,如果只因为若干额外成本便不考虑handle classes和interface classes,将是严重的错误。

请记住:(1). 支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。(2). 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用

32. 确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”)

public inheritance(公开继承)意味”is-a”(是一种)的关系。

如果你令class D(“Derived”)以public形式继承class B(“Base”),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。

请记住:”public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象

33. 避免遮挡继承而来的名称(Avoid hiding inherited names)

class Base {
private:
	int x;

public:
	virtual void mf1() = 0;
	virtual void mf1(int) {}
	virtual void mf2() {}
	void mf3() {}
	void mf3(double) {}
};

class Derived : public Base {
public:
	virtual void mf1() {}
	void mf3() {}
	void mf4() {}
};

class Derived33 : public Base {
public:
	// 必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩
	using Base::mf1; // 让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
	using Base::mf3;
	virtual void mf1() {}
	void mf3() {}
	void mf4() {}
};

int test_item_33()
{
	Derived d;
	int x = 0;

	d.mf1(); // 没问题,调用Derived::mf1
	//d.mf1(x); // 错误,因为Derived::mf1遮掩了Base::mf1
	d.mf2(); // 没问题,调用Base::mf2
	d.mf3(); // 没问题,调用Derived::mf3
	// Derived内的函数mf3遮掩了一个名为mf3但类型不同的Base函数
	//d.mf3(x); // 错误,因为Derived::mf3遮掩了Base::mf3

	Derived33 d2;

	d2.mf1(); // 仍然没问题,仍然调用Derived::mf1
	d2.mf1(x); // 现在没问题了,调用Base::mf1
	d2.mf2(); // 仍然没问题,调用Base::mf2
	d2.mf3(); // 没问题,调用Derived::mf3
	d2.mf3(x); // 现在没问题了,调用Base::mf3

	return 0;
}

请记住:(1). derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。(2).为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)

34. 区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation)

成员函数的接口总是会被继承。

声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。

声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

请记住:(1).接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。(2). pure virtual函数只具体指定接口继承。(3). 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。(4). non-virtual函数具体指定接口继承以及强制性实现继承

35. 考虑virtual函数以外的其它选择(Consider alternatives to virtual functions)

class GameCharacter {
public:
	int healthValue() const // derived classes不重新定义它
	{
		// ... // 做一些事前工作
		int retVal = doHealthValue(); // 做真正的工作
		// ... // 做一些事后工作

		return retVal;
	}

private:
	virtual int doHealthValue() const // derived classes可重新定义它
	{
		return 10;
	}
};

// Strategy设计模式的简单应用
class GameCharacter35; // 前置声明(forward declaration)
int defaultHealthCalc(const GameCharacter35& gc) { return 1; }
class GameCharacter35 {
public:
	typedef int (*HealthCalcFunc)(const GameCharacter35&);
	explicit GameCharacter35(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
	int healthValue() const
	{
		return healthFunc(*this); 
	}

private:
	HealthCalcFunc healthFunc;
};

class EvilBadGuy : public GameCharacter35 {
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter35(hcf) {}
};

int loseHealthQuickly(const GameCharacter35&) { return 2; } // 健康指数计算函数1
int loseHealthSlowly(const GameCharacter35&) { return 3; } // 健康指数计算函数2

// 藉由std::function完成Strategy模式
class GameCharacter35_1;
int defaultHealthCalc_1(const GameCharacter35_1& gc) { return 11; }
class GameCharacter35_1 {
public:
	// HealthCalcFunc可以是任何"可调用物"(callae entity),可被调用并接受
	// 任何兼容于GameCharacter35_1之物,返回任何兼容于int的东西
	typedef std::function<int (const GameCharacter35_1&)> HealthCalcFunc;
	explicit GameCharacter35_1(HealthCalcFunc hcf = defaultHealthCalc_1) : healthFunc(hcf) {}
	int healthValue() const
	{
		return healthFunc(*this);
	}

private:
	HealthCalcFunc healthFunc;
};

int test_item_35()
{
	EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的人物搭配不同的健康计算方式
	EvilBadGuy ebg2(loseHealthSlowly);

	return 0;
}

藉由Non-Virtual Interface手法实现Template Method模式:这一基本设计,也就是”令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。把这个non-virtual函数称为virtual函数的外覆器(wrapper)。在NVI手法下其实没有必要让virtual函数一定得是private。

Template Method模式介绍参考:https://blog.csdn.net/fengbingchun/article/details/30990475

藉由Function Pointers实现Strategy模式。

Strategy模式介绍参考:https://blog.csdn.net/fengbingchun/article/details/32179281

藉由std::function完成Strategy模式。

std::function介绍参考:https://blog.csdn.net/fengbingchun/article/details/52562918

当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案:它们各自有其相对的优点和缺点

(1). 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

(2). 将virtual函数替换为”函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

(3). 以std::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

(4). 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现方法。

请记住:(1). virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。(2). 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。(3). std::function对象的行为就像一般函数指针。这样的对象可接纳”与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)

36. 绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function)

class B {
public:
	void mf() { fprintf(stdout, "B::mf\n"); }
};

class D : public B {
public:
	void mf() { fprintf(stdout, "D::mf\n"); } // 遮掩(hides)了B::mf
};

int test_item_36()
{
	D x;
	B* pB = &x;
	// 由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为"B派生之class"的对象
	pB->mf(); // 调用B::mf

	D* pD = &x;
	pD->mf(); // 调用D::mf

	return 0;
}

non-virtual函数都是静态绑定(statically bound),virtual函数确是动态绑定(dynamically bound)。

请记住:绝对不要重新定义继承而来的non-virtual函数

37. 绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value)

class Shape37 {
public:
	enum ShapeColor { Red, Green, Blud };
	// 所有形状都必须提供一个函数,用来绘出自己
	virtual void draw(ShapeColor color = Red) const = 0;
};

class Rectangle37 : public Shape37 {
public:
	// 注意,赋予不同的缺省参数值。这真糟糕!
	virtual void draw(ShapeColor color = Green) const
	{
		fprintf(stdout, "rectangle shape color: %d\n", color);
	}
};

class Circle37 : public Shape37 {
public:
	virtual void draw(ShapeColor color) const
	// 请注意,以上这么写则当客户以对象调用此函数,一定要指定参数值。因为静态绑定下这个函数并
	// 不从其base继承缺省参数值。但若以指针(或reference)调用此函数,可以不指定参数值,因为动态
	// 绑定下这个函数会从其base继承缺省参数值
	{
		fprintf(stdout, "circle shape color: %d\n", color);
	}
};

int test_item_37()
{
	Shape37* ps; // 静态类型为Shape37*;没有动态类型,因为它尚未指向任何对象
	Shape37* pc = new Circle37; // 静态类型为Shape37*,动态类型为Circle37*
	Shape37* pr = new Rectangle37; // 静态类型为Shape37*,动态类型为Rectangle37*

	pc->draw(Shape37::Red); // 调用Circle37::draw(Shape37::Red)
	pr->draw(Shape37::Red); // 调用Rectangle::draw(Shape37::Red)

	// 即使把指针换成references问题仍然存在
	pr->draw(); // 调用Rectangle37::draw(Shape37::Red)!

	return 0;
}

virtual函数系动态绑定(dynamically bound),而缺省参数值确是静态绑定(statically bound)。静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。

对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。对象的所谓动态类型(dynamic type)则是指”目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。

请记住:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数----你唯一应该覆写的东西----确是动态绑定

38. 通过复合塑模出has-a或”根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition)

class Address {};
class PhoneNumber {};
class Person {
private:
	std::string name;        // 合成成分物(composed object)
	Address address;         // 同上
	PhoneNumber voiceNumber; // 同上
	PhoneNumber faxNumber;   // 同上
};

复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。

public继承带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。

请记住:(1). 复合(composition)的意义和public继承完全不同。(2). 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)

39. 明智而审慎地使用private继承(Use private inheritance judiciously)

class Empty39 {};
class HoldsAnInt {
private:
	int x;
	Empty39 e;
};

class HoldsAnInt39 : private Empty39 { // EBO(empty base optimization)
private:
	int x;
};

int test_item_39()
{
	fprintf(stdout, "sizeof(Empty39): %d\n", sizeof(Empty39)); // 1
	fprintf(stdout, "sizeof(HoldsAnInt): %d\n", sizeof(HoldsAnInt)); // 8 
	fprintf(stdout, "sizeof(HoldsAnInt39): %d\n", sizeof(HoldsAnInt39)); // 4

	return 0;
}

如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。这和public继承的情况不同。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。private继承意味implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其它意涵了。

尽可能使用复合,必要时才使用private继承。

EBO(empty base optimization, 空白基类最优化),EBO一般只在单一继承(而非多重继承)下才可行。

请记住:(1). private继承意味is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。(2).和复合(composition)不同,private继承可以造成empty base最优化。这对致力于”对象尺寸最小化”的程序库开发者而言,可能很重要

40. 明智而审慎地使用多重继承(Use multiple inheritance judiciously)

请记住:(1).多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。(2).virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。(3).多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两相组合

41. 了解隐式接口和编译期多态(Understand implicit interfaces and compile-time polymorphism)

// 隐式接口
template<typename T>
void doProcessing(T& w)
{
	T someNastyWidget;
	if (w.size() > 10 && w != someNastyWidget) {
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

// 显示接口
class Widget41 {
public:
	Widget41();
	virtual std::size_t size() const;
	virtual void normalize();
	void swap(Widget41& other);
};

“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态(compile-time polymorphism)。”哪一个重载函数该被调用”(发生在编译期)和”哪一个virtual函数该被绑定”(发生在运行期)。

通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。隐式接口就完全不同了,它并不基于函数签名式,而是由有效表达式(valid expressions)组成。

请记住:(1).classes和templates都支持接口(interface)和多态(polymorphism)。(2).对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。(3).对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期

42. 了解typename的双重意义(Understand the two meanings of typename)

template<typename C>
void print2nd(const C& container)
{
	if (container.size() >= 2) {
		// 一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须
		// 在紧临它的前一个位置放上关键字typename
		typename C::const_iterator iter(container.begin());
	}
}

template<typename C> //允许使用"typename"(或"class")
void f42(const C& container, // 不允许使用"typename",C并不是嵌套从属类型名称
	typename C::iterator iter); // 一定要使用"typename",C::iterator是个嵌套从属类型名称

template<typename T>
class Base42 {
	class Nested {
		Nested(int x) {}
	};
};

template<typename T>
class Derived42 : public Base42<T>::Nested { // base class list中不允许"typename"
public:
	explicit Derived42(int x) 
	: Base42<T>::Nested(x) // mem.init.list中不允许"typename"
	{
		typename Base42<T>::Nested temp; // 嵌套从属类型名称,既不在base class list中也不在
			// mem.init.list中,作为一个base class修饰符需加上typename
	}
};

template<typename IterT>
void workWithIterator(IterT iter)
{
	// 使用IterT对象所指物的相同类型将temp初始化为iter所指物
	// 如果IterT是vector<int>::iterator,temp的类型就是int
	// value_type被嵌套于iterator_traits<IterT>之内而IterT是个template参数,所以必须在它之前放置typename
	typename std::iterator_traits<IterT>::value_type temp(*iter);
	
	typedef typename std::iterator_traits<IterT>::value_type value_type;
	value_type temp2(*iter);
}

当我们声明template类型参数,class和typename的意义完全相同。

template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name)。嵌套从属类型名称(nested dependent type name),也就是个嵌套从属名称并且指涉某类型。

tepename只被用来验明嵌套从属类型名称,其它名称不该有它存在。

“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。

请记住:(1).声明template参数时,前缀关键字class和typename可互换。(2).请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符

43. 学习处理模板化基类内的名称(Know how to access names in templatized base classes)

class CompanyA {
public:
	void sendCleartext(const std::string& msg) {}
	void sendEncrypted(const std::string& msg) {}
};

class CompanyB {
public:
	void sendCleartext(const std::string& msg) {}
	void sendEncrypted(const std::string& msg) {}
};

class CompanyZ {
public:
	void sendEncrypted(const std::string& msg) {}
};

class MsgInfo {};

template<typename Company>
class MsgSender {
public:
	void sendClear(const MsgInfo& info)
	{
		std::string msg;
		Company c;
		c.sendCleartext(msg);
	}

	void sendSecret(const MsgInfo& info)
	{
		std::string msg;
		Company c;
		c.sendEncrypted(msg);
	}
};

// 一个全特化的MsgSender,它和一般template相同,差别只在于它删掉了sendClear
// class定义式最前面的"template<>"语法象征这既不是template也不是标准class,而是个特化版的MsgSender template
template<>
class MsgSender<CompanyZ> {
public:
	void sendSecret(const MsgInfo& info)
	{
		std::string msg;
		CompanyZ c;
		c.sendEncrypted(msg);
	}
};

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
	//using MsgSender<Company>::sendClear;

	void sendClearMsg(const MsgInfo& info)
	{
		//sendClear(info); // 调用base class函数,无法通过编译

		//this->sendClear(info); // 成立,假设sendClear将被继承

		//sendClear(info); // using MsgSender<Company>::sendClear, ok,假设sendClear将被继承下来

		MsgSender<Company>::sendClear(info); // 明白指出被调用的函数位于base class内,ok,假设sendClear将被继承下来
	}
};

从Object Oriented C++跨进Template C++,继承就不像以前那般畅行无阻了。令C++”不进入templatized base classes观察”的行为失效。有三个办法,第一是在base class函数调用动作之前加上”this->”;第二是使用using声明式;第三是明白指出被调用的函数位于base class内。

面对”指涉base class members”之无效references,编译器的诊断时间可能发生在早期(当解析derived class template的定义式时),也可能发生在晚期(当那些templates被特定之template实参具现化时)。C++的政策是宁愿较早诊断。

请记住:可在derived class templates内通过”this->”指涉base class templates内的成员名称,或藉由一个明白写出的”base class资格修饰符”完成

44. 将与参数无关的代码抽离templates(Factor parameter-independent code out of templates)

class templates的成员函数只有在被使用时才被暗中具现化。

如果你不小心,使用templates可能会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据或两者。

请记住:(1).Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。(2).因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。(3).因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码

45. 运用成员函数模板接受所有兼容类型(Use member function templates to accept “all compatible types”)

template<typename T>
class SmartPtr {
public:
	// 泛化copy构造函数并未被声明为explicit,那是蓄意的,因为原始指针类型之间的转换(例如从
	// derived class指针转为base class指针)是隐式转换,无需明白写出转型动作(cast)
	template<typename U>
	SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {}// 以other的heldPtr初始化this的heldPtr

	T* get() const { return heldPtr; }

private:
	T* heldPtr; // 这个SmartPtr持有的内置(原始)指针
};

在class内声明泛化copy构造函数(是个member template)并不会阻止编译器生成它们自己的copy构造函数(一个non-template),所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和”正常的”copy构造函数。相同规则也适用于赋值(assignment)操作。

请记住:(1).请使用member function template(成员函数模板)生成”可接受所有兼容类型”的函数。(2).如果你声明member templates用于”泛化copy构造”或”泛化assignment操作”,你还需要声明正常的copy构造函数和copy assignment操作符

 46. 需要类型转换时请为模板定义非成员函数(Define non-member functions inside templates when type conversions are desired)

template<typename T> class Rational46;
template<typename T>
const Rational46<T> doMutiply(const Rational46<T>& lhs, const Rational46<T>& rhs);

template<typename T>
class Rational46 {
public:
	Rational46(const T& numerator = 0, const T& denominator = 1) {}
	const T numerator() const { return (T)0; }
	const T denominator() const { return (T)0; }

	friend const Rational46 operator* (const Rational46& lhs, const Rational46& rhs)
	{
		//return Rational46(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
		
		// 令friend函数调用辅助函数
		return doMultiply(lhs, rhs);
	}
};

template<typename T>
const Rational46<T> doMultiply(const Rational46<T>& lhs, const Rational46<T>& rhs)
{
	return Rational46<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int test_item_46()
{
	Rational46<int> oneHalf(1, 2);
	Rational46<int> result = oneHalf * 2;

	return 0;
}

template实参推导过程中从不将隐式类型转换函数纳入考虑。

在一个class template内,template名称可被用来作为”template和其参数”的简略表达方式,所以在Rational<T>内我们可以只写Rational而不必写Rational<T>。

请记住:当我们编写一个class template,而它所提供之”与此template相关的”函数支持”所有参数之隐式类型转换”时,请将那些函数定义为”class template内部的friend函数”

47. 请使用traits classes表现类型信息(Use traits classes for information about types)

// 类型IterT的iterator_category
template<typename IterT>
struct iterator_traits {
	typedef typename IterT::iterator_category iterator_category;
};

// template偏特化,针对内置指针
template<typename IterT>
struct iterator_traits<IterT*> {
	typedef std::random_access_iterator_tag iterator_category;
};

// 实现用于random access迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
	iter += d;
}

// 实现用于bidirectional迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
	if (d >= 0) { while (d--) ++iter; }
	else { while (d++) --iter; }
}

// 实现用于input迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
	if (d < 0) {
		throw std::out_of_range("Negative distance");
	}
	
	while (d--) ++iter;
}

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
	doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}

std::advance用来将某个迭代器移动某个给定距离。STL共有5种迭代器分类,可参考:https://blog.csdn.net/fengbingchun/article/details/77985191

Traits并不是C++关键字或一个预先定义好的构件;它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术的要求之一是,它对内置(built-in)类型和用户自定义(user-defined)类型的表现必须一样好。类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits。

iterator_traits是个struct,习惯上traits总是被实现为structs,但它们却又往往被称为traits classes。iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_tratis<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。

设计并实现一个traits class:(1).确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,希望将来可取得其分类(category)。(2).为该信息选择一个名称(例如iterator_category)。(3).提供一个template和一组特化版本(例如iterator_traits),内含你希望支持的类型相关信息。

如何使用一个traits class:(1).建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。(2).建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述那些”劳工函数”并传递traits class所提供的信息。

Traits广泛用于标准程序库。如iterator_traits,除了供应iterator_category还供应另四份迭代器相关信息(其中最有用的是value_type)。此外还有char_traits用来保存字符类型的相关信息,以及numeric_limits用来保存数值类型的相关信息。

请记住:(1).Traits classes使得”类型相关信息”在编译期可用。它们以templates和”templates特化”完成实现。(2).整合重载技术(overloading)后,traits classes有可能在编译期对类型进行if…else测试

48. 认识template元编程(Be aware of template metaprogramming)

// TMP的阶乘运算, 一般情况:Factorial<n>的值是n乘以Factorial<n-1>的值
template<unsigned n>
struct Factorial {
	enum { value = n * Factorial<n-1>::value };
};

// 特殊情况:Factorial<0>的值是1
template<>
struct Factorial<0> {
	enum { value = 1 };
};

int test_item_48()
{
	fprintf(stdout, "Factorial<5>::value: %d\n", Factorial<5>::value);
	fprintf(stdout, "Factorial<10>::value: %d\n", Factorial<10>::value);

	return 0;
}

Template metaprogramming(TMP, 模板元编程)是编写template-based C++程序并执行于编译期的过程。所谓template metaprogram(模板元程序)是以C++写成、执行于C++编译期内的程序。

请记住:(1).Template metaprogramming(TMP, 模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。(2).TMP可被用来生成”基于政策选择组合”(base on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码

49. 了解new-handler的行为(Understand the behavior of the new-handler)

// 当operator new无法分配足够内存时,该被调用的函数
void outOfMem()
{
	std::cerr<<"Unable to satisfy request for memory\n";
	std::abort();
}

class NewHandlerHolder {
public:
	explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {} // 取得目前的new-handler
	~NewHandlerHolder() // 释放它
	{ std::set_new_handler(handler); }

private:
	std::new_handler handler; // 记录下来
	NewHandlerHolder(const NewHandlerHolder&); // 阻止copying
	NewHandlerHolder& operator= (const NewHandlerHolder&);
};

// "mixin"风格的base class,用以支持class专属的set_new_handler
template<typename T>
class NewHandlerSupport {
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);

private:
	static std::new_handler currentHandler;
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
	std::new_handler oldHandler = currentHandler;
	currentHandler = p;
	return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
	NewHandlerHolder h(std::set_new_handler(currentHandler));
	return ::operator new(size);
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = NULL;

class Widget49 : public NewHandlerSupport<Widget> {};

int test_item_49()
{
	std::set_new_handler(outOfMem);
	int* pBigDataArray = new int[100000000L];

	Widget49::set_new_handler(outOfMem); // 设定outOfMem为Widget49的new-handling函数
	Widget49* pw1 = new Widget49; // 如果内存分配失败,调用outOfMem

	std::string* ps = new std::string; // 如果内存分配失败,调用global new-handling函数

	Widget49::set_new_handler(NULL); // 设定Widget49专属的new-handling函数为NULL
	Widget49* pw2 = new Widget49; // 如果内存分配失败,立刻抛出异常(clalss Widget49并没有专属的new-handling函数)

	return 0;
}

 当operator new无法满足某一内存分配需求时,它会抛出异常。以前它会返回一个null指针。当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指定这个”用于处理内存不足”的函数,客户必须调用set_new_handler,那是声明于<new>的标准程序库函数。new-handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler则是”获得一个new-handler并返回一个new-handler”的函数。set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行的那个new-handler函数。当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。

设计良好的new-handler函数必须做以下事情:(1).让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。(2).安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。(3).卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler, operator new会在内存分配不成功时抛出异常。(4).抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。(5).不返回,通常调用abort或exit。

请记住:(1).set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。(2).nothow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常

std::nothrow介绍参考:https://blog.csdn.net/fengbingchun/article/details/63686673

50. 了解new和delete的合理替换时机(Understand when it makes sense to replace new and delete)

内存对齐介绍参考:https://blog.csdn.net/fengbingchun/article/details/81270326

重载new和delete介绍参考:https://blog.csdn.net/fengbingchun/article/details/78991749

请记住:有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息

51. 编写new和delete时需固守常规(Adhere to convention when writing new and delete)

C++保证”删除null指针永远安全”。

请记住:(1).operator new应该包含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 btyes申请。class专属版本则还应该处理”比正确大小更大的(错误)申请”。(2).operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理”比正确大小更大的(错误)申请”

52. 写了placement new也要写placement delete(Write placement delete if you write placement new)

如果operator new接受的参数除了一定会有的那个size_t之外还有其它,这便是所谓的placement new。”placement new”意味带任意额外参数的new。类似于new的placement版本,operator delete如果接受额外参数,便称为placement delete。

如果一个带额外参数的operator new没有”带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。

请记住:(1).当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。(2).当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本

53. 不要轻忽编译器的警告(Pay attention to compiler warnings)

请记住:(1).严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取”无任何警告”的荣誉。(2).不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失

54. 让自己熟悉包括RT1在内的标准程序库(Familiarize yourself with the standard library, including TR1)

TR1代表”Technical Report 1”。TR1自身只是一份规范,为获得TR1提供的好处,你需要一份实物,一个好的实物来源是Boost。

std::shared_ptr介绍参考:https://blog.csdn.net/fengbingchun/article/details/52202007

std::weak_ptr介绍参考:https://blog.csdn.net/fengbingchun/article/details/52203825

std::function介绍参考:https://blog.csdn.net/fengbingchun/article/details/52562918

std::bind介绍参考:https://blog.csdn.net/fengbingchun/article/details/52613910

std::unordered_map介绍参考:https://blog.csdn.net/fengbingchun/article/details/52235026

C++11中的正则表达式介绍参考:https://blog.csdn.net/fengbingchun/article/details/54835571

std::tuple介绍参考:https://blog.csdn.net/fengbingchun/article/details/72835446

std::array介绍参考:https://blog.csdn.net/fengbingchun/article/details/72809699

55. 让自己熟悉Boost(Familiarize yourself with Boost)

请记住:(1).Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演着深具影响力的角色。(2).Boost提供许多TR1组件实现品,以及其它许多程序库

GitHubhttps://github.com/fengbingchun/Messy_Test

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值