C++进阶_Effective_C++第三版(一) 让自己习惯C++ Accustoming Yourself to C++

1、视C++为一个语言联邦

View C++ as a federatiom of languages.
将C++视为一个有相关语言组成的联邦而非单一语言,在其某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。次语言共有4个:

  • C:C++仍是以C为基础的。区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in
    data types)、数组(arrays)、指针(pointers)等都来自C。许多时候C++对问题的解法其实不过是较高级的C解法。
  • Object-Oriented C++:这部分为C with
    Classes,classes(包括构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)等等。这部分是面向对象设计的古典守则在C++上的最直接实施。
  • Template C++:为++的泛型编程(generic
    programming)部分,Template的相关考虑与设计已经弥漫整个C++,它为C++带来了崭新的编程范型(programming
    paradigm)。TMP相关规则很少与C++主流编程互相影响。
  • STL:是个template程序库,对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function
    objects)的规则有极佳的紧密配合与协调。

当从某个次语言切换到另一个,导致高效编程守则要求也需要改变,如对于内置类型而言,pass-by-value通常比pass-by-reference高效,但是Object-Oriented C++中由于用户自定义构造函数和析构函数的存在pass-by-reference-to-const更高效,使用Template C++也是如此。但是STL中对于迭代器和函数对象而言,pass-by-value更高效,这是因为迭代器和函数对象都是在C指针上塑造出来的。

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

2、尽量以const,enum,inline替换#define

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还会继续扮演控制编译的重要角色。

  • 对於单纯常量,最好以const对象或enums替换#defines。
  • 对于形如函数的宏(macros),最好改用inline函数替换#defines。

3、尽可能使用const

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的规则,所以不被允许如此做。

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

4、确定对象被使用前已先被初始化

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++不保证初始化它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  • 为免除“跨编译单元的初始化次序”问题,以local static对象替换non-local static对象。

下一篇:C++进阶_Effective_C++第三版(二) 构造/析构/赋值运算 Constructors,Destructors,and Assignment Operators

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章