Effective C++ 条款1、2、3、4

以下内容均来自Scott Meyers大师所著Effective C++ version3,如有错误地方,欢迎指正!相互学习,促进!!

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

理解C++,须认识其主要的次语言:

1、C。C++是以C为基础。区块、语句、预处理器、内置数据类型(build-in data types)、数组、指针等都来自C嘛!以C++内的C成分工作时,高效编程守则昭出C语言的局限:没有模板(谈不上泛型)、没有异常(exceptions,C++ primer_v5中第5章有涉及,还不是太熟悉),没有重载(overloading)等等。

2、Object-Oriented C++。这部分也就是C with Classes 诉求的:classes(包括构造和析构函数),封装(encapsulation),继承(inheritance),多态(polymorphism),virtual函数(动态绑定)...等等。面向对象的古典守则。

3、Template C++。C++的泛型编程部分。了解不太多。暂放一下。

4、STL(好用)。STL是个template程序库,是非常特殊的一个。对容器、迭代器、算法以及函数对象(不太熟)的规约有极佳的紧密配合与协调,然而templates及程序库也可以其他想法建置出来。STL有自己特殊的办事方式,使用时,必须遵守它的规约。当然Scott Meyers也写了一本effective STL,就是这么优秀。

条款2 尽量以const、enum、inline替换#define(另一种说法:宁可以编译器替代预处理器)

很明显,从条款名称上可以看出,#define工作在预处理阶段(名称不存在记号表内,于是出错时,编译器提示不了名称,多过的#define、#ifdef、#endif影响阅读代码),而其它工作在编译阶段。

万一编译器(错误地)不允许“static整数型class常量”完成“in class初值设定”,可改用所谓的“the enum hack”补偿做法。其理论基础:一个属于枚举类型(enumerated type)的数值可权充ints被使用(不太理解)。举例如下:

class GamePlayer {
private:
	enum{NumTurns=5};//“the enum hack”——令NumTurns成为5的一个记号名称
	int socres[NumTurns];//下标虽然是需要常值,但这样做也OK
}; 

使用enum hack的好处:

1、enum hack的行为某方面比较像#define而不像const,例如,取一个const的地址是合法,但取一个enum的地址就不合法(都说了enum像#dedine嘛,所以不存在取地址的说法)。

2、Enums和#define一样绝不会导致非必要的内存分配(这点好)。

3、认识enum hack纯粹是为了实用主义。由于许多代码用了它,所以看到它就必须认识它(这玩意就这么横)。“enum hack”是template metaprogramming(模版无编程)的基础技术。

作者忠告:

对于单纯常量,最好以const对象或enums替换#define

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

条款3 尽可能使用const

const允许指定一个语义约束,而编译器会强制实施这项约束。所以只要某值保持不变是事实,就该确实说出来,因为说出来可以获得编译器的襄助,确保这条约束不被违反。在某些地方,我见const修饰对象最好的解释为只读特性(绝不应该为常值特性)。

i、很重要的一句话:如果const出现在星号(解引用符)左边,表示所指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。举例如下:

char greeting[] = "hello";
char* p = greeting;
const char* p = greeting;//非常量指针,所指对象数据是常量
char* const p = greeting;//常量指针,但所指对象数据不是常量
const char* const p = greeting;//常量指针,所指对象是常量
ii、此处需要强调一下const修饰迭代器iterator以及const_iterator的区别
const std::vector<int>::iterator iter = ...//声明迭代器为const就像声明指针为const一样(即T* const 指针)
*iter=10//正确
++iter//错误
std::vector<int>::const_iterator citer = ...//希望迭代器所指的东西不可被改动(即const T* 指针)
*citer = 10//错误
++citer//正确

iii、const成员函数

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。

事实一:两个成员函数如果只是常量性(constness)不同,可以被重载。如下所示:

class TestBlock {
public:
	...
	//以下成员函数可根据常量性被重载??????
	
	const char& operator[](size_t position) const { return test[position]; }//operator[] for const 对象
	const& operator[](size_t position) { return text[position]; }//operator[] for non-const 对象
private:
	string text;
};
TextBlock tb("Hello");
cout << tb[0];//调用non-const TextBlock::operator[]
const TextBlock ctb("World");
cout << ctb[0];//调用const TextBlock::operator[]
/*特别需要注意的是返回值是引用类型,是因为有时需要改动函数返回值,如果是以by value返回对象,意味着
被改动的其实是tb.text[0]的一个对象副本,而不是tb.text[0]自身,这不会是想要的行为*/

iv、bitwise constness(又称physical constness)和logical constness

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

常量成员函数,不允许修改类内的成员变量,除非该成员变量有mutable修饰。

class CTextBlock {
public:
	size_t length() const;
private:
	char* ptext;
	size_t textLength;
	bool lengthIsValid;
};
size_t CTextBlock::length() const {
	if (!lengthIsValid) {
		textLength = strlen(pText);//失败啦,常量成员函数不允许修改成员变量,
		lengthIsValid = true;      //除非mutable size_t textLength;和mutable bool lengthIsValid;
								   //有了mutable修饰才可以摆脱成员函数常量特性的束缚。
	}
}

条款4 确定对象被使用前已先被初始化(太重要啦)

永远记住在使用对象之前先将它初始化。

i、对于无任何成员的内置类型,必须手工完成此事;对于自定义类,有构造函数可使用哈!

不要混淆赋值和初始化,实例如下:

#include<string>
#include<list>
class PhoneNumber{...};
class ABEntry {
public:
	ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
private:
	string theName;
	string theAddress;
	list<PhoneNumber> thePhones;
	int numTimesConsulted;
};
ABEntry::ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones) {
	//以下这些都是赋值,而非初始化
	theName = name;
	theAddress = address;
	thePones = phones;
	numTimesConsulted = 0;
}
//以下才是ABEntry构造函数初始化过程(利用成员初值列表)
ABEntry::ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones):
	theName(name), theAddress(address), thePones(phones), numTimesConsulted(0) {}
ii、规则:总是在初值列表中列出所有成员变量,以免还得记住哪些成员变量(如果它们在初值列表中被遗漏的话)可以无需初值。(记住一点哈,内置类型一定要初始化,否则可能开启“不明确行为”的潘多拉盒子,哈哈!)
ABEntry::ABEntry() :
	theName(), theAddress(), thePones(), numTimesConsulted(0) {}

iii、C++有着十分的固定的“成员初始化次序”,base classes更早于derivedclass被始化(析造则反之),而class的成员变量总是以其声明次序被初始化。

iv、“不同编译单无内定义的non-local static对象”的初始化次序

至少两个源码文件,每一个内含至少一个non-local static对象。产生的问题是:如果某编译单无内的某个对象,它所用到的这个对象可能能尚未被初始化(灾难产生啦!),因为C++对“定义于不同编译单无内的non-local static对象”的初始化次序并无明确定义

解决方法(单例模式):将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。(此专属函数被称为reference-returning函数)此方法保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象定义式”时被初始化。所以如果以“函数调用”(返回一个reference指向local static对象)替换“直接访部non-local static对象”,就获得保证,保证所获得的那个reference将指向一个历经初始化的对象。如果从未调用 non-local static对象的“仿真函数”,就绝不会引发构造和析构函数的成本。举例如下:

class FileSystem {//来自自己的程序库
public:
	size_t numDisks() const;//众多成员函数之一
};
extern FileSystem tfs;//全局声明变量,预备给客户使用的对象;tfs代表“the file system”
//--------------------
class Directory {//由程序库客户建立
public:
	Directory(...)
};
Directory::Directory(...) {
	size_t disks = tfs.numDisks();//使用tfs对象。(完蛋!如果在tfs初始化之前构成,将产生灾难)
}
Directory tempDir(params);//(完蛋!如果在tfs初始化之前构成,将产生灾难)
//-------------------------------------------
//解决之方法(单例模式(目前还不是很了解))
//-------------------------------------------
class FileSystem{...};//同上
FileSystem& tfs() {//专属函数替换tfs对象,它在FileSystem class中可能是个static
	static FileSystem fs;//定义并初始化一个local static对象
	return fs;//返回一个reference指向上述对象
}
//--------------------
class Directory{...};
Directory::Directory(params) {
	size_t disks = tfs().numDisks();
}
Directory& tempDir() {//专属函数用来替换tempDir对象,它在Directory class中可能是个static
	static Diretory td;//定义并初始化local static对象
	return td;//返回一个reference指向上述对象
}
记住:任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。而处理这个麻烦的一种做法是:在程序的单线程启动阶段(single-threaded startup portion)手工 调用所有reference-returning函数,这可消除与初始化有关的“竞速形势(race conditions)”。

猜你喜欢

转载自blog.csdn.net/tt_love9527/article/details/80347025
今日推荐