您的位置:首页 > 编程语言 > C语言/C++

《Effective C++》 读书笔记(一) 让自己习惯C++

2013-09-10 09:22 751 查看
让自己习惯C++
Accustoming Yourself to C++

条款01:视C++为一个语言联邦
View C++ as a federation of languages.
如今的C++,是一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言联邦,而不再仅仅是C再加上一些面向对象的特性。为了充分理解C++,很有必要认识4个主要的次语言:
C。包括区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)
Object-Oriented C++ 。classes(包括构造函数和析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual等
Template C++。有关templates的考虑与设计已经弥漫整个C++,也引出了模板元编程(template metaprogramming)
STL。顾名思义,standard template library。STL的设计目标,是将不同的算法和数据结构结合在一起,并获取最佳效率,要运用好STL强大的框架和优异的效率,就必须通晓概念并小心运用。对于容器、迭代器、算法、仿函数等规约有极佳的紧密配合与协调。特别是iterator和functors(function
objects),都是利用C的指针塑造出来的。

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

条款02:尽量以const,enum,inline替换#define
Prefer consts,enums,and inlines to #defines.
一个例子:比如定义一个#define ASPECT_RATIO 1.653; ,#define的内容也许从未被编译器看见;也许在编译器开始处理源码前就被预处理器移走了;还有可能对这个ASPECT_PATIO的来源毫无印象,对于一个莫名其妙而来的1.653,肯定因为追踪它花费不少时间。而这么定义:
const double Aspect_Ratio=1.653; 就会免去上述烦恼。
并且,由于#defines并不重视作用域(scopes),一旦#define被定义,就在后面的全局区域中有效。虽说可以用#undef解除,但频繁使用也很麻烦。更麻烦的是,#defines不仅不能用来定义class专属常量,也不能提供任何封装性。

又一个例子:
class GamePlayer
{
private:
enum {NumTurns=5 };           //"the enum hack"--令NumTurns成为5的一个记号名称
int scores[NumTurns];
//...
};


"enum hack"技术正是模板元编程的基础技术。许多代码用上它,所以看到类似的,必须要认识

第三个例子:对于宏定义的函数,会因为加入很多括号,看起来很麻烦,如:
#define CALL_WITH_MAX( a , b) f( (a) > (b) ? (a) : (b) )
就算写出来了,还有更奇葩的:
int a=5,b=0;

CALL_WITH_MAX(++a,b); //a被累加二次
CALL_WITH_MAX(++a,b+10); //a被累加一次
运用inline函数,会因为C++编译器的组合方式与常规函数不同,inline函数会比常规函数的运行速度稍快,代价是需要占用更多的内存。不过如果一个函数经常要调用,而且是短小精悍的函数,毫无疑问用inline函数是非常值得的。这个优点也是#define函数无法比拟的。而且还能获得一般函数的所有可预料行为和类型安全性,只要写出template
inline函数。
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a > b ? a : b);
}


☆对于单纯常量,最好以const对象或enums替换#defines。
☆对于形似函数的宏,最好改用inline函数替换#defines。

条款03:尽可能使用const
Use const whenever possible.
const的一件奇妙事情是,它允许你指定一个语义约束(也就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束,它允许你告诉编译器和其他程序员某值应该保持不变。只要这(某值保持不变)是事实,你就该确实说出来,因为说出来可以获得编译器的襄助,确保这条约束不被违反。
如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针都是常量。
例子如下:
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

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,关于Rational类的定义式
class Rational{...};
const Rational operator* (const Rational& lhs,const Rational& rhs);


如果不返回一个const对象,而是这么写:
class Rational{...};
Rational operator* (const Rational& lhs,const Rational& rhs);
就很容易出现某些恼人的错误。。比如:
Rational a,b,c;
...
if (a * b = c) //本来想对a operator* b 与c比较是否相等,即 if (a * b == c) ...却对a*b的值做了无意义的operator=赋值。
...;

两个成员函数如果只是常量性(constness)不同,可以被重载。这实在是一个重要的C++特性。

有两个流行的概念,bitwise constness和logical constness。bitwise const正是C++对常量性(constness)的定义。也就是说它不更改对象内任何一个bit。好处是很容易侦测违反点。但不幸的是,许多成员函数虽然不十足地具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所有物”的成员函数虽然不能算是const,但如果只有指针(不包括所指物)隶属于对象,那么此函数不会引起编译器异议。将会导致反直观结果。比如下面一个例子:
class CTextBlock
{
public:
...
char& operator[](std::size_t position)const             //bitwise const声明
{return pText[position]; }                              //但其实不恰当
private:
char* pText;
};

语法上,看上去一切正常。但是,这个class不适当地将其operator[]声明为const成员函数,而该函数返回一个reference指向对象内部值。看看接下来能发生什么。
const CTextBlock cctb("Hello");             //声明一个常量对象。
char *pc=&cctb[0];                          //调用const operator[]取得一个指针
//并指向cctb的数据。
*pc='J';                                    //cctb的内容变成了"Jello"

以bitwise constness的角度,确实没有任何错误。创建了一个常量并设以某个值,而且对它只调用const成员函数,但终究还是改变了它的值。

由此引出logical constness。这一派拥护者主张,一个const成员函数可以修改它所处理对象内的某些bits,但只有在客户端侦测不出的情况下才能如此。可以利用mutable去释放non-static成员变量的bitwise constness约束。

当我们需要对一个成员函数,运用相同的接口和做相同的事,使用两个相似的版本——const成员函数和non-const成员函数时,不假思索地,可能会类似地这么写:
class TextBlock
{
public:
...
const char& operator[] (std::size_t position) const
{
...           //边界检验
...           //记录数据访问
...           //检验数据完整性
return text[position];
}
char& operator[] (std::size_t position)
{
...           //边界检验
...           //记录数据访问
...           //检验数据完整性
return text[position];
}
private:
std::string text;
};

但这么写,毫无疑问地,今后维护这种代码就会出现代码膨胀、编译时间延长等头痛的问题。为了避免代码重复所带来的困扰,可以令non-const operator[]去调用const operator[]。也就是“运用const成员函数实现出其non-const孪生兄弟”的技术。类似于下面一段代码:
class TextBlock
{
public:
...
const char& operator[](std::size_t position)const {...}//与上例一样
char& operator[](std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position] );
}
...
private:
std::string text;
};

尽管转型是一个糟糕的想法,但代码重复更糟糕。而且不能让non-const operator[]递归调用自己。更需要注意的是,const成员函数承诺绝不改变其对象的逻辑状态,其反向做法——令const版本成员函数调用non-const版本的成员函数,是一种错误的做法。

☆将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型成员函数本体。

☆编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)
☆当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

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

Make sure that objects are initialized before they're used.
读取未初始化的值会导致不明确的行为。为了避免这种行为,就要养成永远在使用对象之前先将值初始化的良好习惯。对于无任何成员的内置类型,必须手工完成。至于内置类型以外的任何其他东西,初始化责任在构造函数(constructors)身上,要做的很简单,确保每一个构造函数都将对象的每一个成员初始化。一个例子如下:
class PhoneNumber{...};
class ABEntry
{
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
public:
ABEntry(const std::string& name,const std::string& address,
const std::list<PhoneNumber>& phones);
ABEntry();
};

然后就顺理成章地。。。。不是吗?
ABEntry::ABEntry(const std::string& name,const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName=name;
theAddress=address;
thePhones=phones;
numTimesConsulted=0;
}
ABEntry::ABEntry()
{
theName="";
theAddress="";
thePhones=NULL;
numTimesConsulted=0;
}

当然不是了!!上面那些代码是赋值,而不是初始化。这个版本首先调用default构造函数为theName,theAddress和thePhones设一个我们看不见的初值,再立刻对它们进行赋值。default构造函数的一切作为因此浪费了。
以下才是真正的赋值。这种方法称为成员初始列(member initialization list)。但要求总是在成员初始列中列出所有成员变量,以免遗漏造成“不明确行为”的后果。总是使用成员初始列,这样做有时候绝对必要,又往往比赋值更高效,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。
ABEntry::ABEntry(const std::string& name,const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(name),theAddress(address),thePhones(phones),numTimesConsulted(0)
{}
ABEntry::ABEntry()
 :theName(),theAddress(),thePhones(),numTimesConsulted(0)
{}


C++有着十分固定的“成员初始化次序”——base classes更早于derived classes被初始化。但当定义于不同的编译单元内的non-local static对象的初始化时,要判断相对初始化次序相当困难。如果多个编译单元内的non-local static由模板隐式具现化(implicit template instantiation)形成的话,要判断正确的初始化次序根本不可能。

不过利用一个小小的设计就能消除这个问题。要做的是:将每个non-const static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。不过内含static对象的事实会让它们在多线程系统中有不确定性。

例子如下:

class FileSystem{...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{...};
Directory::Directory(params)
{
...
std::size_t disks=tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}


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

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

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

参考文献:
1.《Effective C++》3rd Scott Meyers 著,侯捷译
2.《C++标准程序库-自修教程与参考手册》 Nicolai M.Josuttis著,侯捷、孟岩译
3.《C++ Primer Plus》5ed Stephen Prata著,孙建春、韦强译
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: