码迷,mamicode.com
首页 > 编程语言 > 详细

[Effective C++]让自己习惯C++

时间:2015-07-20 18:27:33      阅读:213      评论:0      收藏:0      [点我收藏+]

标签:

转自:http://philoscience.iteye.com/blog/1402852

条款01: 视C++为一个语言联邦

请记住:

  • C++高效编程守则视状况而变化,取决于你是用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守则再次适用。

条款02:尽量以const,enum,inline替换#define(宁可以编译器代替预处理器)

请记住:

  • 对于单纯常量,最好以const对象或enums替换#define。
  • 对于形似函数的宏,最好改用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的地址通常也不合法。

条款03:尽可能使用const

请记住:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用哉内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用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可以和函数返回值、各参数、函数自身(成员函数)产生关联。

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] );   } };

 补:const的使用情况(待完整)

1)const使用在一般变量中

定义const变量一定要同时初始化并且不可以再更改。

const int i=1;    //fine
const int j;      //error,变量j需要初始值设定项
j=2;              //error,表达式必须是可修改的左值,j不可修改

 2)const使用在指针变量中

const既可限定指针本身,也可限定所指向的对象,也可以同时限定指针本身和指向的对象。

char ch=‘a‘;
const char* ptr=&ch;
char* const ptr=&ch;
const char* const ptr=&ch;

 3)const应用在函数声明中

const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。

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

请记住:

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初始化列表,而不要在函数体内使用赋值操作。初始列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

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

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对象。

[Effective C++]让自己习惯C++

标签:

原文地址:http://www.cnblogs.com/kona/p/4661621.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!