effective C++ -- 让自己习惯C++

这段时间看了一遍<Effective C++>,从中了解了很多C++特性,以及少量设计模式的基础知识,增加了我阅读C++对象模型及设计模式这两本书的欲望,这确实是一本关于C++的非常好的书。不过正如云风所讲,“总觉得书里讲的太细,太多观点本是好的,只是局限在了 C++ 语言中。明明是 C++ 的缺陷,却让人绞尽心力的回避那些问题,或是以 C++ 独特的方式回避。在别的语言中不该存在的问题,却成了 C++ 程序员必备的知识。人生苦短,何苦制造问题来解决之。“不过我对C++并没有偏见,所以我看着觉得很有趣。我打算一章一章地总结一下,当是对这本书的回顾了。

首先区分一下声明(declaration)和定义(definition)以及初始化(initialization)以及赋值(assignment)的区别。
声明只是向编译器表明某个东西的名称和类型,但略去细节,下面都是声明式:

extern int x;            //对象(object)声明式
std::size_t numDigits( int num );  //函数(function)声明式
class Widget;            //类(class)声明式

template<class T>
class GraphNode;          //模板(template)声明式

 定义对于对象而言,是编译器为此对象分配存储空间;对函数或函数模板而言,定义提供了代码本体;对类或类模板而言,定义列出了它们的成员。通过定义式,我们知道一个类对象的占用内存大小。

对于内置类型,很大的可能是编译器并没有对它们进行初始化,也就是说,分配到的内存里面是什么,它初始值就是什么,而对于类,初始化由构造函数执行。赋值则是通过操作符=完成。因此,”初始化跟赋值是不同的操作“,这一点当时看《C++ Premier》时不是很理解,这里就说明白了,呵呵。

 

Item 1: 视C++为一个语言联邦

C++是一个多重范型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。将C++视为一个由相关语言组成的联邦语言,在其某个次语言中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当从一个次语言移往另一个次语言,守则可能改变。C++总共有四个次语言:
(1)C。说到底C++仍然是以C为基础。区块(block)、语句(statement)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)均来自C。
(2)Object-Oriented C++。即C with classes:classes(包括构造函数和析构函数)、封装、继承、多态、virtual函数的动态绑定等等。
(3)Template C++。即C++的泛型编程部分。
(4)STL。STL是个template程序库。它对容器、迭代器、算法以及函数对象的规约有极佳的紧密配合与协调。

从某个次语言切换到另一个次语言时,高效编程守则可能要求改变策略。例如对内置类型而言pass-by-value通常比pass-by-reference高效,但当从C part of C++切换到Object-oriented C++,由于用户自定义构造函数和析构函数的存在,pass-by-reference-to-const往往更好。运用Template C++时尤其如此,因为那个时候我们甚至不知道所处理的对象的类型。然而一旦进行STL,因为迭代器和函数对象都是在C指针之上塑造出来的,所以对STL的迭代器和函数对象而言,旧式的pass-by-value守则再次适用。

 

Item 2: 尽量以const,enum,inline替换#define(宁可以编译器规制预处理器)

我们知道C++的编译过程包括三个步骤:(1)预处理。预处理(包括宏定义命令、条件编译命令、头文件包含指令)完成的工作,可以说是对源程序完成”替换“的工作。(2)编译,优化。(3)链接,包括静态链接和动态链接。#define属于预处理过程,而const, enum, inline则发生在编译过程。
(1)#define ASPECT_RATIO 1.653 的问题:
由于预处理的替换,ASPECT_RATIO可能根本没有进入记号表内。于是当你运用此常量但获得一个编译错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。如果用一个常量替换上述的宏:

const double AspectRatio = 1.653

 我们便可以保证AspectRatio一定会被编译器看到从而进入记号表内。此外对浮点常量而言,使用常量可能比使用#define导致较小量的码,因为预处理器”盲目地将宏名称ASPECT_RATIO规制为1.653“可能导致目标码出现多份1.653,若改用常量AspectRatio绝不会出现相同情况。

(2)我们无法用#define创建一个class专属常量。
作为C阵营的#define,不仅不能够用来定义Object-Oriented C++的class的专属常量,也不能提供任何封装性,即没有所谓的private概念。

(3)用#define实现宏的麻烦
宏看起来像函数,但不会招致函数调用带来的额外开锁。下面这个夹带着宏实参,调用函数f:

//以a和b的较大值调用f
#define CALL_WITH_MAX(a, b) f( (a)>(b) ? (a):(b) )

 首先是我们必须为宏中的所有实参加上小括号,否则我们无法使用表达式作为实参,但纵使如此,看完下面这个宏定义,我想你就不会再想使用宏了:

int a = 4, b = 0;
CALL_WITH_MAX( ++a, b );   //a被累加两次
CALL_WITH_MAX( ++a, b+10 );  //a被累加一次

 a的递增次数取决于它被拿来跟谁比较!
我们可以用下面的template inline函数可以获得宏带来的效率以及一般函数的所有可预料行为和函数安全,同时,template inline函数是真正的函数,它遵守作用域和访问规则。

template<typename T>
inline void callWithMax( const T&a, const T& b ){
 f( a>b? a : b );
 }

  
以常量替换#define,有两种特殊情况需要注意。第一是定义常量指针,由于常量定义式通常被放在头文件内(以便被不同的源友含入),因此有必须指指针(而不只是指针所指之物)声明为const。第二是class专属常量。为了将常量的作用域限制于class内,你必须让它成为class的一个成员,而为确保此常量至多只有一份实体,你必须让它成为一个static成员。

class GamePlayer{
 //class专属常量又是static且为整数类型,特殊处理
 static const int NumTurns = 5;  //常量声明式,不分配内存
 int scores[NumTurns];
 ...
 };

也可以将初值放在定义式:
class CostEstimate{          //位于头文件内
 private:
  static const double FudgeFactor;
  ...
 };
 
const double CostEstimate::FudgeFactor = 1.35; //位于实现文件内

 另一种做法是使用enum hack,其理论基础是,”一个属于枚举类型的数值可权充int被使用“,于是GamePlayer可以定义如下:

class GamePlayer{
 private:
  enum { NumTurns = 5 };
  ...
  int scores[NumTurns];
  };

 enum hack的行为某方面说比较像#define而不像const,例如取一个const的地址是合法的,但取一个enum的地址就不合法,正如取一个#define的地址通常也不合法。

 

Item 3: 尽可能使用const
(1)如果你认为某值保持不变是事实,就应该用const说出来,因为说出来可以获得编译器的帮助。
如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边表示被指物和指针两者都是常量。如果被指物是常量,const写在类型之前或之后意义是相同的。
STL迭代器是以指针为根据产生的,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即T* const),表明这个迭代器不得指向不同的东西,但它所指的东西是可以改动的。如果希望迭代器所指东西不可被改动,需要使用const_iterator.

std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;  //OK,改变iter所指之物
++iter;    //error,iter是const,不能指向不同的东西
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;  //error,*cIter是const
++cIter;   //OK

 const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身(成员函数)产生关联。
(2)令函数返回一个常量值,往往可以降低因客户错误而造成的意外。如下面的有理数operator*声明式:

class Rational { ... }
const Rational operator* (const Rational& lhs, const Rational& rhs );   //返回自定义类型,可当左值使用

 如果有人本想比较计算结果结果不慎使用了如下语句:

Rational a, b, c;
...
if( (a*b) = c )  //结果将是在a*b的成果上调用operator=

 如果a,b是内置类型,这样的代码很容易被编译器发现不合法,而一个”良好的用户自定义类型“的特征是它们避免无端地与内置类型不兼容,因此允许两值乘积做赋值动作不应该被允许,将operator*的返回值声明为const可以预防那个赋值动作。
(3)应该将不修改相应实参的形参定义为const引用。如果将这样的形参定义为非const引用,则毫无必要地限制了该函数的使用
函数的形参分为引用和非引用类型,对于非引用类型,实际上传递给函数的是参数的副本,因此调用函数时,如果函数使用非引用的非const形参,则既可以给函数传递const实参,也可以传递非const实参。如果函数使用非引用const形参,也同样可以给函数传递const实参或非const实参。但是对于引用形参,首先我们应该了解到,在两种情况下我们会使用引用形参:(1)我们需要对实参做出相应的修改,而不是仅仅利用它的值;(2)实参很大,我们不希望发生复制,这时候,我们应该把引用形参设置成const的,否则该函数(事实上本来不对实参做任何修改)将不能接受const引用参数,因为它没有保证自己不修改该参数,而该参数却有着不被修改的义务,所以可以说它拒绝被该函数使用。
(4)const成员函数(确认该成员函数可以作用于const对象身上)
const成员函数之所以重要,基于两个理由:(1)它们使class接口比较容易被理解:得知哪个函数可以改动对象哪个函数不行是很重要的。(2)它们使用”操作const对象“成为可能。改善c++效率的一个根本办法是以pass-by-reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。
需要知道的是,两个成员函数如果只是常量性不同,可以被重载。例如下面这个取址函数:

class TextBlock{
 public:
  ...
  const char& operator[]( std::size_t position ) const{ //供const对象调用
   return text[position];
   }
  char& operator[]( std::size_t position ){  //供non-const对象调用
   return text[position];
   }
 private:
  std::string text;
  };

 有时候,像TextBlock里面的operator[]对象可能不只是返回一个reference指向某字符,它还执行边界检查,志记访问信息,甚至可能进行数据完善性检验,如果把所有这些同时放进const和non-const operator[]中,一大串的重复是我们所不乐意见到的,怎样能够实现operator[]一次并使用它两次呢?可以使用const_cast<T>:

class TextBlock{
 pubilc:
  const char& operator[]( std::size_t position ) const{
   ...   //边界检查
   ...   //日志记录
   ...   //数据完善性检验
   return text[position];
   }
  char& operator[]( std::size_t position ){
   //首先将this由non-const转换为const,调用operator[]返回const char&引用
   //再用const_cast把const char&转换为char&。
   return 
    const_cast<char&>( static_cast<const TextBlock&>(*this)[position] );
    }
   };

 

Item 4: 确定对象被使用前已先被初始化
前面已经谈过初始化和赋值的区别。对于无任何成员的内置类型,必须手工完成初始化,除此之外的其他对象,其初始化责任落在构造函数身上,规则很简单:确保每一个构造函数都将对象的每一个成员初始化,而对象的成员变量的初始化动作发生在进入构造函数本体之前。

class PhoneNumber{ ... }
class ABEntry{
 public:
  ABEntry( const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones );
 private:
  std::string theName;
  std::string theAddress;
  std::list<PhoneNumber> thePhones;
  int numTimesConsulted;
  };

 下面是ABEntry两个构造函数的不同定义版本:

//版本一:
ABEntry::ABEntry( const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones ){
//首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻对它们赋予新值
 theName = name;    //这些都是赋值,而非初始化
 theAddress = address;
 thePhones = phones;
 numTimeConsulted = 0;
 }

//版本二:
//成员初始列针对各个成员变量而设的实参调用对应对象的copy构造函数
ABEntry::ABEntry( const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones ):
 theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0){  //这些都是初始化
 }

 一般情况下使用成员初始列效率比较高。同时,如果成员变量是const或reference,就一定要通过成员初始化列表来对该成员初始化。
C++有着十分固定的“成员初始化次序”。base class早于其derived class被初始化,而class的成员变量总是以声明次序被初始化。然而C++对于定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。为避免“跨编译单元之初始化次序”问题,最好以local static对象替换non-local static对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值