Effective c++第三版
让自己习惯C++
条款01:视C++为一个语言联邦
- C++是一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言
- 了解四个次语言
C
Object-Oriented C++
Template C++
STL
C++高效编程守则视情况而变化,取决于你使用C++的哪一个部分
条款02:尽量以const、enum、inline替换#define
- 尽量不使用#define
#define ASPECT_RATIO 1.653
ASPECT_RATIO可能没有进入记号表内,当运用这个常量时获得一个编译错误的信息时,可能会出现问题,因为错误信息可能会只提到1.653这个数值而不是ASPECT_RATIO。如果ASPECT_RATIO被定义在非自己所写的文件里,排查错误将非常麻烦。
- 解决方法
用一个常量替换上述的宏:
const double AspectRatio = 1.653;
AspectRatio是一个语言常量,肯定能够被编译器所看见,即会进入记号表中。且使用常量可能比使用#define导致较小量的码。
- 两种特殊情况
1.定义常量指针。常量指针通常放在头文件中,因此有必要将指针所指的地址声明为const。则就要用指向常量的常指针。如:
const char* const authorName = "S
2.class专属常量。为了将常量的作用域限制于class内,你必须让他成为class的一个成员。而为了确保常量只有一个实体,你必须使他成为一个static成员:
class GamePlayer
{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns];
...
};
其中NumTurns是一个声明式,而不是一个定义式。如果要取某个class专属常量的地址,必须提供定义式如下:
const int GamePlayer::NumTurns; //NumTurns的定义;
//下面说明为什么没有给予数值
这个式子放入实现文件(cpp文件)而非头文件。由于class常量在声明时获得初值,因此不可以再设初值。
注意,我们无法利用#define创建一个class专属常量,因为#define不重视作用域。一旦宏被定义,它就在其后的编译过程中有效。#define不能够用来定义class专属常量,也不能够提供任何封装性。
如果编译器不支持上述语言,可作如下改变:
class CostEstimate
{
private:
static const double FudegeFactor; //static class常量声明
... //位于头文件内
};
const double CostEstimate::FudegeFactor = 1.35; //位于实现文件内
但是上述中的GamePlayer::scores的数组必须要知道常量值。可以改用所谓的the enum hack补偿做法。
其理论依据是:“一个属于枚举类型的数值可权充ints被使用”,于是GamerPlayer可定义如下:
class GamePlayer
{
private:
enum { Numturns = 5}; //"the enum hack" 令NumTurns成为5的一个记号名称就没问题了
int scores[NumTurns]
...
};
enum hack的行为比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,取#define的地址通常也不合法。
如果你不像让别人活得一个指针或者引用指向你的某个整数常量,enum可以帮助你实现这个目标。
enum hac—>实用主义,是模板元编程的基础技术。
当宏带着宏实参时可以用模板内联函数取代:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
//可被取代为:
template
inline void callWithMax(const T& a,const T& b) //不知道T是什么,因此采用常引用采值的方式
{
f(a > b ? a : b);
}
callWithMax是个真正的函数,它遵守作用域和访问规则。即可以用于class中,作为一个private inline 函数。
有了const、enum、inline,我们队预处理器的需求降低了。
- 对于单纯常量,最好以constexpr、const对象或者enum替换#define。
- 对于形似函数的宏,最好改用inline函数替换#define。
条款03: 尽可能使用const
const可修饰它在class外部global或namespace作用域的常量,或修饰文件、函数、区块作用域中被声明为static的对象。也可修饰class内部中static和non-static成员变量。对于指针,可修饰指针本身不可变,指针所指物不可变,或者两者都不是或都是const:
char greeting[ ] = "hello";
char* p = greeting; //非常量指针
const char* p = greeting; //指向常量的指针
const char* const p = greeting; //指向常量的常指针
const出现在星号左边,表示被指物是常量。如果出现在星号右边,则表示指针是常量,如果出现在星号两边,表示被指物和指针都是常量。
const int * = int const *;
STL迭代器以指针为根据塑模出来,迭代器的作用就像个T指针。声明迭代器为const就相当于声明指针为const一样(T const指针),表示一个常指针,如果你希望STL模拟一个const T*指针,你需要的是const_iterator:
std::vector vec;
...
const std::vector::iterator iter = vec.begin(); //iter的作用像个T* const
*iter = 10; //没问题,改变iter所指物
++iter; //错误! iter是const
std::vector::cosnt_iterator cIter = vec.begin();
*cIter = 10; //错误!cIter是const
++cIter; //正确,改变cIter
面对函数声明时,const可以和函数返回值、各参数、函数自身产生关联。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,且保证了安全性和高效性。例如:
有理数的operator*声明式。
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational &rhs);
为什么要返回一个const对象?因为用户可能会产生这样的行为:
Rational a, b, c;
...
(a * b) = c; //在a*b的成果上调用operator=
程序员可能在做bool运算时,不经意间这么做:
if( a*b = c)... //其实只是想做一个比较的操作
如果a,b是内置类型,显然不合法,而一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容(见条款18),因此才将operator*的回传值声明为const,预防上面的操作。
我们应该在必要的时候使用const,避免出现键入“==”却意外键成“=”的错误。
const成员函数
const实施于成员函数的目的是为了确认该成员函数可作用于const对象。这一类成员函数之所以重要,原因如下:
1.它们使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行。
2.它们使“操作const对象”成为可能。因为如条款20所言,改善c++程序效率的根本方法是以pass by reference-to-const方式传递对象,而此技术可行的前提条件是,我们有const成员函数可以用来处理取得的const对象。
两个成员函数如果只是常量性不同,可以被重载。
class TextBlock
{
public:
...
const char& operator[ ] (std::size_t position) const
{return text[position];} //const对象
char& operator[ ] (std::size_t position)
{return text[position];} //non-const对象
private:
std::string text;
};
void print(const TextBlock& ctb)
{
std::cout<
只要重载operator[ ]并对不同的版本给予不同的返回类型,就可以令const和non-const TextBlocks获得不同的处理:
std::cout << tb[0]; //没问题
tb[0]= 'x'; //没问题
std::cout<< ctb[0]; //没问题
ctb[0] = 'x' //错误
上述错误只因operator[ ]的返回类型导致的,错在对于一个返回值为const char&实施赋值。
对于想要在const成员函数中改变类中成员变量的值,此时应该将该成员变量声明为mutable。
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText); //错误!在const成员函数内
lengthIsValid = true; //不能赋值给lengthIsValid和textLength
}
return textLength;
}
//改
class CTextBlock
{
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //此成员变量可以在const成员函数里更改
mutable bool lengthIsValid //此成员函数也可以在const成员函数中更改
};
std:: size_t CTextBlock::length() const
{
if(!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true;
return textLength;
}
在const和non-const成员函数中避免重复
class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
...
...
...
return text[positon];
}
char& ooperator[ ] (std::size_t position)
{
...
...
...
return text[position];
}
private:
std::string text;
};
两个版本的operator[]函数重复了一些代码,应该做的是实现operator[ ]的技能一次并使用它两次。令另一个调用另一个。即:
class TextBlock
{
public:
...
const char& operator[ ] (std::size_t position) const
{
...
...
...
return text[position];
}
char& operator[ ](std::size_t position)
{
return const_cast(static_cast(*this)[positon]);
}
...
};
第一次将TextBlock& 转型为const TextBlock&。第二次则是从const operator[ ]的返回值中移除const
请记住
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象被使用前已被初始化
C++中的对象初始化反复无常。如果这样写:
int x;
在某些语境下x保证被初始化(为0),但在其他语境中却不能保证。
class Point
{
int x, y;
};
...
Point p;
p的成员变量由时候被初始化(为0),有时候不会。
读取为初始化的值会导致不明确行为。有可能导致程序终止运行。导致一些不可测的行为。
好的处理方式是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。例如:
int x = 0; //对int进行手工初始化
const char* text = "A C-style string"; //对指针进行手工初始化
double d;
std::cin>>d; //以读取input stream的方式完成初始化
至于内置类型之外的任何其他东西,初始化责任落在构造函数身上。即:确保每一个构造函数都将对象的每一个成员初始化。
注意:不能混淆赋值和初始化。
class PhoneNumber {...};
class ABEntry
{
public:
ABEntry(const std::string& name, const std::string& address,
const std::list& phones;
private:
std::string theName;
std::string theAddress;
std::list thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list& phones)
{
theName = name;
theAddress = address; //这些都是赋值而非初始化
thePhones = phones;
numTimesConsulted = 0;
}
这会导致ABEntry对象带有你期望的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName、theAddress和thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时。
ABEntry构造函数的一个较好的写法是,使用所谓的成员初始值列表化替换赋值操作:
ABEntry::ABEntry(const std::string& name, const std::string& address, //现在这些都是初始化
const std::list& phones)
:theName(name),theAddress(address),thePhones(phones),numTimesConsulted(0)
{ }
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本首先调用默认构造函数为theName、theAddress和thePhones设初值,然后立刻对他们赋予新值。default构造函数的一切作为因此浪费了。成员初始化列表的做法避免了这个问题,因为初始列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。
对于内置对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化。
ABEntry::ABEntry( )
:theName(), theAddress(), thePhones(), //调用theNmae、theAddress、thePhones的默认构造函数
numTimesConsulted(0) //记得将numTimesConsulted显示初始化为0
{...}
- 我们立下一个规则:总是在初始列中列出所有成员变量。
有些情况下即使面对的成员变量属于内置类型,也一定要使用初值列(int x(5))。如果成员变量是const或引用,它们就一定需要初值,不能被赋值(见条款5)。
为避免需要记住成员变量何时必须在成员初始化列表,何时不需要。一个简单的做法是:
- 总是使用成员初始化列表。
当许多class拥有多个构造函数,存在许多成员变量和或者base class,可以合理地在成员初始化列表中遗漏哪些“赋值表现和初始化一样好”得成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。当成员变量的初值是由文件或数据库读入时特别有用。
比起由赋值操作完成的“伪初始化”,通过成员初始化列表完成的“真正初始化”通常更加可取。
C++有着固定的“成员初始化次序”。基类应该比其子类更先被初始化(见条款12),而class的成员变量总是以其声明次序依次被初始化。如上述中的ABEntry类,其中theName、theAddress、thePhones依次被初始化。
注意:两个成员变量的初始化或许必须带有次序性。例如初始化数组时要指定大小,即代表大小的成员要先被初始化。
当内置型成员变量明确地加以初始化,而且也确保你的构造函数运用“成员初始化列表”初始化基类和成员变量,剩下的就是不同编译单元内定义的非局部静态对象的初始化次序。
static对象:存在时间是从被构造出来知道程序结束为止,因此stack和hea-based对象都被排除。static对象包括全局对象、定义域namespace作用域内的对象、在class内、函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象称为non-local static对象。程序结束时static对象会自动销毁,即在main()结束时自动调用其的析构函数。
编译单元:单一目标文件的源代码。即:单一源码文件加上其所含的头文件。
- non-local static对象在main()开始之前就已经被构造出来了
现在,我们关心的问题设计两个源码文件,每一个至少有一个non-local static对象。
问题:如果某编译单元内的某个non-loacl static 对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能尚未初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
实例如下:
class FileSystem
{
public:
...
std::size_t numDisks() const; //众多成员函数之一
...
};
extern FileSystem tfs; //预备给客户使用的对象;
//另一个class
class Directory
{
public:
Directory( params );
...
};
Directory::Directory( params)
{
...
std::size_t disks = tfs.numDisks( ); //使用tfs对象
...
}
//进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:
Directory tempDir( params ); //为临时文件而做出的目录
现在,初始化次序的重要性就体现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到未初始化的tfs。由于tfs和tempDir是不同的人在不同的时间于不同的源代码建立恰里的,他们是定义于不同编译单元内的non-local static对象。如何确定tfs在tempDir之前被初始化呢?
答案是无法确定。
此问题的解决办法:即将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),这些函数返回一个引用指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是单例模式的一个常见实现手法。
此技术在tfs和temp身上,结果如下:
class FileSystem {...};
FileSystem& tfs() //使用函数来代替tfs对象。
{
static FileSystem fs; //定义并初始化一个local static对象,返回一个引用指向上述对象。
return fs;
}
class Directory {...}; //同上
Directory::Dorectory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td; //同上
return td;
}
这样修改之后,系统程序的客户完全可以像以前一样地使用它。不同之处是tfs、tempDir变成了tfs()\tempDir()。这种做法可能在多线程的情况下有问题。
运用引用-返回函数防止“初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序。即:如果对象A的初始化必须在B之前初始化,但是A能否初始化成功却又受制于B是否已初始化。这样就存在问题。
为了避免在对象初始化之前过早地使用它们,你需要做三件事。
1.手工初始化内置型的非成员对象。
2.使用成员初始化列表
请记住
- 为内置型对象进行手工初始化。
- 构造函数最好使用成员初始化列表,不要在构造函数本体内使用赋值操作。初始化列表中的成员变量的次序应该和它们在class