View C++ as a federatiom of languages.
将C++视为一个有相关语言组成的联邦而非单一语言,在其某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。次语言共有4个:
当从某个次语言切换到另一个,导致高效编程守则要求也需要改变,如对于内置类型而言,pass-by-value通常比pass-by-reference高效,但是Object-Oriented C++中由于用户自定义构造函数和析构函数的存在pass-by-reference-to-const更高效,使用Template C++也是如此。但是STL中对于迭代器和函数对象而言,pass-by-value更高效,这是因为迭代器和函数对象都是在C指针上塑造出来的。
Prefer consts,enums,and inlines to #define
让编译器替换预处理器,如下面
#define ASPECT_RaTIO 1.653
记号名称ASPECT_RaTIO在编译器处理源码之前就会被预处理器移走,此记号名称ASPECT_RaTIO无法加入到记号表(symbol table)内,对于使用此常量的地方都会替换为1.653,如果出现编译错误,错误信息会提到1.653而不是ASPECT_RaTIO,而且常量一般定义在头文件中,如此很难地位问题的位置。
解决办法为用一个常量替换上述的宏(#define):
const double AspectRatio = 1.653;
作为一个常量,编译器肯定可以看到,也会进入到记号表中。而且使用常量可比使用#define生成较小量的码,因为预处理器会盲目的将宏名称ASPECT_RaTIO替换成1.653,导致目标码出现多份1.653。
对于已常量替换#define时,有两种特殊情况。
第一是定义常量指针(constant pointers)由于常量定义式通常被放在头文件内,因此需要将指针声明为const,如果char*- based是字符串,则应该写成如下:
const char * const AuthorName = “Socott Meyers”;
更好的写法为:const std::string AuthorName(“Socott Meyers”);
第二是class专属常量。为了将常量的作用域(scope)限制于class内,必须让它成为class的一个成员(member);而为确保此常量至多只有一份实体,必须为一个static成员:
class GamePlayer{
private:
static const int NumTurns = 5;
int scores[NumTurns];
};
但是目前看到的是NumTurns的声明式而非定义式。通常C++要求对所有使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如ints,chars,bools),则需要特殊处理。只要不取它们的地址,可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,或者编译器需要提供一个定义式,则必须另外提供定义式如下:
const int GamePlayer:: NumTurns;
此语句应放在实现文件中,而且由于在定义时以对其设置了初值,此处不可以再设初值。
比较旧的编译器有可能不支持此语法,应该改为如下语句:
class GamePlayer{
private:
static const int NumTurns;
};
const int GamePlayer:: NumTurns = 5;
但是如果在class编译期间需要一个class常量值,此时可以使用枚举类型来实现,如下:
class GamePlayer{
private:
enum {}NumTurns = 5 };
int scores[NumTurns];
};
这样实现由于取一个enum的地址是不合法的,而取#define的地址通常也不合法,如此则更加完美的替代了#define。
对于使用#define另一个误用情况为以它实现宏(macros)。宏看起来像函数,但不会产生函数调用(function call)带来的额外开销。如下比较两个数大小的例子:
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
此写法会很难让人看懂,而且必须给每个实参加上小括号,否则会出现一些奇怪的错误,但是即使加上小括号也会出现一些奇怪的错误如下:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加2次
CALL_WITH_MAX(++a, b+10); //a被累加1次
对于此种情况可以使用template inline函数改写:
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
这个template产出一整群函数,每个函数都接受两个同类型对象,并以其中较大者调用f,这里不需要给参数加上括号,也不需要操心参数被核算多次等等问题。此外由于callWithMax是个真正的函数,它遵守作用域(scope)和访问规则。例如可以写一个class内的private inline函数,一般宏无法实现。
虽然有了consts、enums和inlines,但是对预处理器(特别是#define)的需求降低了,但是并未完全消除。#include仍然是必需品,而#ifdef、#ifndef还会继续扮演控制编译的重要角色。
Use const whenever possible
const允许你指定一个语义约束(就是指定一个“不该被改动”的对象),而编译器会强制实施这项约束。它允许你告诉编译器或者其他程序员某个值应该保持不变。
可以用在classer外部修饰global或者namespace作用域中的常量,或者修饰文件、函数、区块作用域(block scope)中被声明为static的对象。你也可以用它修饰classes内部的static和non-static成员变量。面对指针,可以指出指针自身,指针所指物,或者两者都是const。如下:
char greeting[] = “Hello”;
char* p = greeting;
const char* p = greeting; //指针更改,但指向的内存区域不可更改。
char const * p = greeting; //指针更改,但指向的内存区域不可更改。
char* const p = greeting; //指针不可更改,但指向的内存区域可更改。
const char* const p = greeting; //指针不可更改,指向的内存区域也不可更改。
STL的迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T* 指针。声明迭代器为const和声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但是它指的东西的值可以改动。如果希望迭代器所值的东西不可被改动(即模拟一个const T* 指针),则需要使用const_iterator:
std::vector<int> vec;
…
const std::vector<int>:: iterator iter = vec.begin(); //iter的作用像个T* const
*iter = 10; //改变iter所指物
++iter; //错误,iter是const的不能改变。
std::vector<int>:: const_iterator citer = vec.begin(); //iter的作用像个const T*
*citer = 10; //错误,不能改变所指物
++citer; //没问题
const最具威力的用法是在面对函数声明时。在一个函数声明式内,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,就可以让用户自定义类型和内置类型不兼容,此情况下,编译是编译器会提早报告错误,更快的处理程序错误。
const用于成员函数,可以确认该成员函数可作用于const对象身上,如此做有两个好处,第一,使class接口比较容易被理解。因为得知那个函数可以改动对象,那个函数不可以是很重要的。第二,使操作const对象成为可能,对编写高效代码是个关键,因为改善C++代码程序效率的一个根本办法就是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得的const对象。
两个成员函数如果只是常量性(constness)不同,可以被重载。如下:
class TextBlock {
public:
const char& operator[] {std::size_t position} const {return text[position];} //const对象
char& operator[] {std::size_t position} {return text[position];} //非const对象
private:
std::string text;
};
对于TextBlock的operator[]s可被如下使用:
TextBlock tb(“Hello”);
std::cout<<tb[0]; //调用非const TextBlock::operator[].
const TextBlock ctb(“World”);
std::cout<<ctb[0]; //调用const TextBlock::operator[].
tb[0] = ‘x’; //可正常修改值,
ctb[0] = ‘x’; //错误,因为ctb对象为const,无法修改
由于一般const对象大多用passed by pointer-to-const或passed by reference-to-const传值。上述ctb应该改为:
void print(const TextBlock& ctb){ std::cout<<ctb[0]}; //调用const TextBlock::operator[].
但是如果上述的TextBlock类中如果只是重载了char& operator[],未重载const char& operator[],成员为char*此时,执行下面代码:
const TextBlock ctb(“World”);//声明一个常量对象
char * pc= &ctb[0]; //调用const operator[]取得一个指针。
*pc = ‘J’; //cctb内容变成了Jorld。
本意为不可更改的值,但是由于代码实现的原因,值又可以被更改。
对于如果想在const函数中改变某些成员变量的值,可使用mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束。如下代码:
class CtextBlock {
public:
std::size_t length() const;
private:
char * pText;
mutable std::size_t textLength; //变量的值总会被改变,即使在const成员函数内。
};
std::size_t CtextBlock::length() const
{
textLength = std::strlen(pText); //即使在const函数中,也可以改变值。
}
在const和non-const成员函数中避免重复。可以使用转型进行实现如下:
class TextBlock {
public:
const char& operator[] {std::size_t position} const
{ …
return text[position];
}
char& operator[] {std::size_t position}
{
return const_cast<char&>( /*将op[]返回值的const移*除,*/
static_cast<const TextBlock&>(*this)[position]); //为*this加上const调用const op[]。
}
private:
std::string text;
};
此处转型使用了两次,第一次为*this添加const使可以调用const版本的op,第二次是将返回值中的const移除。如果反向使用的话,是不可以的,因为const成员函数内不改变其对象的逻辑状态,而non-const成员函数可对其对象做任何动作;const成员函数调用non-const成员函数会破坏const的规则,所以不被允许如此做。
Make sure that object are initialized before they’re used.
对于将对象初始化,C++在特定的语境下会有不同的结果。通常如果使用C part of C++而且初始化会导致运行期成本,那么就不能保证会默认初始化。但是对于non-C parts of C++,情况就会有些变化,如数值不保证其内容被初始化,但是vector却可以保证内容被默认初始化。
最好的解决办法就是在使用对象之前先将它初始化,对于无任何成员的内置类型,必须手动完成初始化。如:
int x = 0; //对int进行手动初始化。
const char* text = “A C”; //对指针进行手动初始化。
double d; std::cin>>d; //以读取input stream的方式完成初始化。
对于内置类型外的任何其他东西,初始化应该在构造函数中完成,确保每个构造函数都对对象的每一个成员初始化。
但是在执行时,不要混淆了赋值(assignment)和初始化(initialization)。例如:
class PhoneNumber {…};
class ABEntry {
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构造函数内,其成员都是在被赋值。初始化发生的时间更早,较佳的写法是使用member initialization list(成员初始化列表)替换赋值动作:
ABEntry:: ABEntry(const std::string& name, const std::list<PhoneNumber>& phones)
:theName(name), thePhones(phones),numTimesConsulted(0){ }
这个构造函数和上一个的最终结果相同,但是通常效率较高。因为上一个首先调用default构造函数为成员变量设初值,然后再对它们赋予新值。Default构造函数的作为因此浪费了。使用成员初值列的做法避免了这一个问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量的构造函数的实参。
有些情况下即使面对成员变量属于内置类型,也一定得使用初始列,比如成员变量是const或references,它们就一定需要初值,不能被赋值。
如果classes拥有多个构造函数,每个构造函数有自己的成员初值列,如果这种classes存在许多成员变量或base classes,这种情况下,可以将使用初值列和赋值表现一样好的成员变量,移往某个函数(通常为private),供所有构造函数调用。
C++对于成员初始化次序是固定的,base classes早于其drived classes被初始化,而class的成员变量总是以其声明次序被初始化(与在成员初值列中的次序无关)。
多个编译单元内的non-local static对象经由“模板隐式具现化(implicit template instantiations)”形成,不但不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。将每个non-local static对象搬到自己的专属函数内(该对象再次函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接访问这些对象。即non-local static对象被local static对象替换了。Singleton模式就是使用此方式实现的。如下:
class FileSystem {…};
FileSystem& tfs() //函数用来替换tfs对象,它在FileSystem class中可能是个static。
{
static FileSystem fs; //定义并初始化一个local static对象,
return fs; //返回一个reference指向上述对象。
}
Class Directory {…};
Directory::Directory(params)
{
….
std::size_t disk = tfs().numDisks(); //使用tfs()调用FileSystem的成员函数。
…
}
Directory& tempDir()
{
static Directory td;
return td;
}
如此实现,系统程序使用函数返回的“指向static对象”的references,而不再使用static对象自身。
下一篇:C++进阶_Effective_C++第三版(二) 构造/析构/赋值运算 Constructors,Destructors,and Assignment Operators