2018-09-16

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

请记住
  1. 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  2. 编译器强制实施bitwise constness
  3. 当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

你可能感兴趣的:(2018-09-16)