首页 > 代码库 > 《Effective C++》学习笔记(二)

《Effective C++》学习笔记(二)

原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38706483


闲谈

这两天都是先在地铁里看了会《Effective C++》,然后再回公司继续看,起先是因为地铁里太多无聊,手机也没信号,可看着看着发现里面的内容越来越“接地气”,开始有点无法自拔起来。

今天依旧是翻阅着找自己感兴趣的开始看,先是回到条款02,接下来把设计与声明给看完了,来看看今天的收获把。


条款02:尽量以const,enum,inline替换#define

在此之前,#define一直是我比较喜欢用的一种方式,基本上常量都得提出来#define,MAX、MIN、PI之流常量几乎天天见面,还时常对不使用#define定义出常量的朋友“指指点点”。开个玩笑,说的有点夸张了,但#define确实在不少程序员中还是广为使用的。

#define实际上是一个预处理命令,在预处理阶段将调用它的地方直接替换成它的值,所以#define并没有参与编译,编译器也无从知道#define的存在,好了,那么开始“批斗”下#define的缺点。

#define PI = 3.1415927

如果#define了一个PI常量,然后在PI这里出现了错误,在编译器错误提示的地方只能看到3.1415927,这时就得满世界寻找3.1415927,如果PI定义在一个并非你写的头文件中,你肯定对它毫无概念,于是将因为追踪这个常量而大费周章。

解决之道是用一个常量替换掉上述宏:

const double PI = 3.1415927;

当我们使用const替换#define时有两种特殊情况。

第一种是定义常量指针,例如一个char*字符串,你得写两次const:

const char * const desc =  "Hello world!";

好吧,其实string比char*要好用,所以往往定义成这样更好:

const std::string desc("Hello world!");

第二种特殊情况是class的专属常量,为了确保此常量最多只有一份存在,所以记得将其声明成一个static成员。

说到这不难发现#defne的另一个缺点,那就是无法定义class专属常量,因为并没有private #define这种东西……

如果你的编译器不允许“static整数型class常量”完成“in class初值设定”,可以改用“the enum hack”。

enum像#define,但是并不是#define,比如它也不可以取地址,但是enum定义的常量并不是在预处理的时候进行替换,而是在编译的时候。

#define用的比较多的另一个地方是定义看起来像函数的宏,它的好处是不会带来函数调用造成的额外开销,如:

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) 

这样的宏太多缺点,比如你必须给所有实参都给加上小括号,不然有可能会使得函数出现并非你想见到的效果。

这个时候你可以以template inline函数来替代

template<typename T>
inline void callWithMax(const T &a, const T &b)
{
	f(a > b ? a : b);
}

它可以有自己专属的作用域,也不用担心给所用实参加上小括号。

但预处理命令们依旧有他们的用武之地,不能一棒子打死,但尽可能的去避免使用他们。


总结:

1)对于单纯的常量,最好以const对象或enum替代#define。

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


条款03:尽可能使用const(未完,待续)

使用const将一个对象约束为不可改动,在很长一段时间,如果将对象设成private一样,是不被我理解的,但是随着写的程序越来越多,开始有了新的看法——你的程序不是为了自己一个人而写。
你得手动指定一个对象是否可改动,以免当别人使用的时候出现你意想不到的错误,但是编译器并未报错,使用者也就不会意识到,相反,当做一个正常行为继续下去。
const的位置决定了将什么置为不可改动,但其实有一个规律,如果const放置在星号左边,那么指针指向的东西是不可改动的,如果放在星号右边,则十,指针无法改变。如果左右都有,那么指针和被指物都无法改变。
STL迭代器是以指针为根据塑模出来的,所以在const用法上也可以参照指针用法。
在这个条款中不得不提的一个问题,那就是令函数返回一个常量值,虽然这个说法看上去会让人不解。举个例子来说:

class Rational { … }
const Rational operator* (const Rational &lhs, const Rational &rhs);

Rational a, b, c;
(a * b) = c; // 在a*b的成果上调用operator=
if ((a * b) = c) …; // 其实仅仅实现做一个比较,只是少写了一个=
解决办法是,将operator*的回传值声明为const可以预防这个没有含义的赋值操作。

总结:

1)将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加与任何作用域内的对象、函数参数、函数返回类型、成员函数本题。
2)编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
3)当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。


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

在C++中,对象初始化这件事情有点反复无常,简单来说就是,有的对象编译器会帮你默认初始化,有的不会。当然,这和对象、编译器都有关系。

看上去这是一个无法决定的状态,而最佳的处理办法就是:永远在使用对象之前将它初始化。对于无任何成员的内置类型,必须手工完成此事。

对于内置类型意外的任何其他东西,初始化的重任则交给了对应的构造函数,这样规则就简单多了,确保每一个构造函数都将对象的每一个成员初始化。重要的是不要混淆了赋值和初始化。好吧,其实在之前我一直也弄不清这个概念……

Class PhoneNumber {…}
Class  ABEntry // ABEntry = "Address Book Entry"
{
public:
	ABEntry(const std::string &name, const std::list<PhoneNumber> &phones);
private:
	std::string theName;
	std::list<PhoneNumber> thePhones;
	int numTimesConsulted;
};
// 版本一
ABEntry::ABEntry(const std::string &name, const std::list<PhoneNumber> &phones)
{
	theName = name;
	thePhones = phones;
	numTimesConsulted = 0;
}

写到这,是不是如释重负,觉得已经完成了初始化?但是不得不说的是……ABEntry构造函数内部的代码都是赋值,而不是初始化。

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,其实关于ABEntry构造函数有个较佳的写法,利用成员初始化列替换赋值操作:

// 版本二
ABEntry::ABEntry(const std::string &name, const std::list<PhoneNumber> &phones)
	:theName(name),
	thePhones(phones),
	numTimesConsulted(0) ( }

版本一首先调用默认构造函数对theName等成员初始化,然后再给他们赋新值。而版本二则是以name为初值对theName进行拷贝构造,其他同理。显然版本二效率要高。

对于内置对象初始化和赋值的成本相同,但是为了一致,所以保持一样的书写。

同样如果只需要默认构造,可以这样:

ABEntry::ABEntry()
	:theName(),
	thePhones(),
	numTimesConsulted(0) { }

如果成员变量没有再成员初值列中指定初值,那么回自动调用默认构造函数,但是一般还是规定在初值列中列出所有成员变量。

成员初值列的初始化顺序和成员变量声明的顺序相同,和在成员初值列中摆放次序无关,所以最好以成员变量声明顺序来摆放成员初始值次序。

并且,派生类总在其基类初始化后再进行初始化。

对于不同编译单元内定义的非本地静态(non-local static)对象,他们的初始化次序并无明确规定,所以如此一来初始化先后顺序也容易造成不少问题,解决的方案是将非本地静态对象,以本地静态对象进行替换。好吧,其实这就是设计模式中说的单例模式(Singleton)。

// 版本一:
class FileSystem
{
public:
	…
	std::size_t numDisks() const;
	…
}
extern FileSystem tfs;
class Directory
{
public:
	…
	Directory();
	…
};
Directory::Directory()
{
	…
	std::size_t disks = tfs.numDisks();
	…
}

// 版本二:
class FileSystem { … }
FileSystem & tfs()
{
	static FileSystem fs;
	return fs;
}
class Directory { … }
Directory::Directory()
{
	…
	std::size_t disks = tfs().numDisks();
	…
}

总结:

1)为内置对象进行手工初始化,因为C++不保证初始化它们。

2)构造函数最好使用成员初值列,而不是在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中声明次序相同。

3)为免除”跨编译单元之初始化次序“问题,请以local static对象替换non-local static对象。