首页 > 代码库 > 《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对象。