《Effective C++》学习笔记 (3rd.尽可能使用const)

在对《Effective C++》的条款二:【尽量以const、enum、inline替换#define】 进行学习之前,个人认为非常有必要先学习条款三,也就是这一节【尽可能使用const】,毕竟学C的时候用#define,学C++又觉得加了好像没用,于是这个关键字就只是看着眼熟了。

熟读本条款,你会对const的理解加深。

废话多了,马上开始正文!


呃……不过在真正进入书中正题之前,我们得先知道const是用来干嘛的。

专业的说法是允许你来定义一个语义约束,也就是指定一个不应该被改动的对象,那什么又叫不该被改动的对象?说来说去也就是常量,举个例子,现在需要你根据一个圆的半径来求它的面积,给出了这样一个函数头部:

float area(float radius);

我们还需要一个π,而π是一个固定的值,所以我们应该把它定义为常量,可以这样写:

  const float PI=3.14159;
  float my_area(float radius)
  {
         return PI*(radius*radius);
  }

至于为什么要写在全局而不是在函数体内,因为我希望能在这个函数以外的地方使用这个常量。
那么回到这段代码,也许你会想,似乎加不加const,程序都能运行?所以加了有什么用呢?先别着急,这里只是在复习一下const的使用语法,最最基础的那种。

所以const到底有什么用呢?

试想一下,现在老师让你们几个人来合写一份代码(哪位老师这么智障?),现在你将完成的部分写好了,也就是上边那几行,接下来交递给下一位,于是你同组的某位兄弟在看了看这个函数之后就想,这个π才精确到小数点后五位啊,不行,我要让它精确到后7位,于是他新写了一个函数:

   float MoreNiuBiDe_area(float radius)
     {
     PI=3.1415926;
     return my_area(radius);
     }

可你知道,其他人并不需要精确到那么高的位数,而且PI可是一个全局变量,你这么一改其他人不也就跟着改了吗?

所幸在你想到这一点跑过去砸他电脑以阻止他之前,编译器就会给他报错:

 error: assignment of read-only variable 'PI'

通过这个图样森破的例子我们可以看出,用const修饰之后,PI已经成了一个只读而不可写的变量,当其他人试图去修改PI值的时候,编译器首先就会把他给拦住:住手!你是在对一个只读的变量赋值!

很多程序员一起写代码的时候,你完全不知道其他的程序员会用你提供的代码做出什么样的操作,比起跑过去和他说:“在你用我的代码之前我们先签一个协议,你不能重新给PI这个变量赋值。” 还不如干脆用语法来限制他的行为。想要骚操作?先过编译器这关。

那么现在开始说说《Effective C++》第三条《尽可能使用const》的内容:


3.尽可能使用const

只要某个值应该保持不变,你就该说出来,因为只要说出来编译器就会帮你确保这条事实不被违反。

而const这个关键字很多变,好像什么地方都能加,既可以修饰全局、namespace作用域里的常量,也可以用来修饰文件、函数、区块作用域(用一对{}括起来的地方)中声明为static的对象、class中static和non-static的成员变量,甚至是指针。

指针的const:

 char * p = "Hello,World";      //non-const pointer,non-const data
    const char* p="Hello,World";   //non-const pointer,const data
    char* const p="Hello,World";   //const pointer,non-const data
    const char* const p="Hello,World";  //const pointer,const data

常指针、指向常量的指针、指向常量的常指针,说中文名还真容易绕混,看看上边这几行代码虽然也是能分辨清楚,但真要用的时候又怎么记得住呢?没关系,书中提到了const的语法:

如果const出现在 * 的左边,表示被指物是常量,如果在 * 右边表示指针自身是常量,如果两边都有,表示被指物和指针都是常量。

那么马上实践,如果被指物是常量该怎么写?

  const int * number;

没错,这是对的,但还有一种写法:

  int const * number;

两种写法都有不少的人使用,不要困惑,毕竟我们只需要分辨const在 * 的哪一边就可以了。

STL中iterator的const:

STL的迭代器(iterator)是以指针为根据塑造出来的,所以迭代器的作用就像是一个T*指针,声明迭代器为const 相当于声明了一个T * const ,这个迭代器不可指向别处了,但他所指向的内容是可以变的,如果想做到值不变的话,就使用const_iterator,如下边这段代码:

std::vector  Array;
...//省略了一部分代码
const std::vector::iterator itr=Array.begin();//相当于int * const
*itr=6;                                           //没问题,因为所指物不是const
++itr;                                           //错!itr是const
...//省略了一部分代码
const std::vector::const_iterator c_itr=Array.begin();//相当于const int*
*c_itr=6;                                      //错!所指物是const
++itr;                                        //没问题,因为itr不是const

函数的const

在一个函数声明式内,const可以让函数的返回值、各个参数、函数自身(如果是成员函数)产生关联。让一个函数返回一个常量, 可以降低因为使用者错误而造成的意外,又不至于放弃安全和高效性,举例:

class Rational{
...//省略了一部分代码
};
const Rational operator*(const Rational& lhs,const Rational& rhs);

为什么要返回一个const?
如果不这样做的话,那位想要改变PI值的兄弟也许会进行这样的操作:

   Rational a,b,c;
   if((a*b)=c)...//省略了一部分代码

他可能只想用来比较一下两个字是否相同,但少打了个=号,这种操作如果放在int、char等内置类型上,编译器直接会报错。

一个良好的自定义类型的特性是它们会避免无端地与内置类型不兼容(《Effective C++》后边会提及),因此允许对两值乘积做赋值的动作也就没有意义了,将operator* 返回值声明为const就能避免这个无意义的操作。

而用const修饰的参数,就像普通的const对象一样,在必要使用它们的时候使用。除非需要改动,否则请将它们声明为const ,多打六个字就可以剩下一堆error,比如==打成=。

const成员函数:

一般的语法:

class textblock
{
public:
  ...//省略了一部分代码
  const char& operator[] (std::size_t index) const//常成员函数
    {return text[index];}
  char& operator[](std::size_t index)//成员函数
    {return text[index];}
private:
  std::string text;
}

可以看到,这段代码通过const关键字对operator[]进行了重载,即基于const的重载,非const的成员会调用成员函数operator[],而const成员会调用const operator[]。
而根据不同的返回类型,就可以让non-const和const获得不同的处理:

void print(const textblock& cstr,textblock& str)
{
 std::cout<

注意,这个错误是因为const版的operator[]返回了一个const char&,且试图对它进行赋值。operator[]调用动作本身是没有问题的。

所以,成员函数如果是const意味着什么?《Effective C++》在这里引入了两个概念:

  • bitwise constness (或者称physical constness)
  • logical constness

bitwise constness正如字面意义,这个观点认为成员函数只有在不更改任何一个成员变量的值的时候才可以说是const,也就是不应该改变成员变量的任何一个bit。这也是C++对于常量性(constness)的定义。因此,const成员函数不可以更改对象内任何一个non-static成员变量。

了解这个概念后,我们看看下边这段代码:

class textblock_other
{
public:
  ...//省略了一部分代码
   char& operator[] (std::size_t index) const
    {return text[index];}
private:
  char* text;
}

如果有心的话很容易注意到,operator[]声明成了const成员函数,却返回了一个char&,这段代码是可以通过编译的,并且编译器会认为它是一个符合bitwise constness的函数,因为它不会改变text,那么再看看这个函数会容许发生什么事:

void main()
{
 const textblock_other Tb("Hello");
 char* in=&Tb[0];
 *in='N';         //Tb现在变成了Nello。 
}

我们创建了一个常量,并且只调用了const成员函数,但Tb的值还是被改变了。

这种情况催生了另一个观点,也就是logical constness。

这一派的拥护者主张,const成员函数可以修改它所处理对象内的某些bits,但只有在客户端侦测不出的情况下才能这么做。

class text_block
{
public:
  ...//省略了一部分代码
  std::size_t length() const;

private:
  char* ptext;
  std::size_t textlenth;//最近一次计算的文本长度
  bool lenthIsValid;//当前长度是否有效
}

std::size_t text_block::lenth() const
{
   if(!lenthIsValid)
   {
   textlenth=std::strlen(ptext);//错误!
   lenthIsValid=true;//错误!
   }
   return textlenth;
}

结合前边说到的bitwise constness,我们很容易看出length函数的实现并非bitwise const,虽然修改这两个数对于这个类来说可以接受,但编译器坚持bitwise constness。这个时候怎么解决?

书中给出的解决方法是:利用一个与const相关的关键字:mutable(可变的),用来释放掉对non-static成员变量的bitwise constness约束:

class text_block
{
public:
  ...//省略了一部分代码
  std::size_t length() const;

private:
  char* ptext;
  mutable std::size_t textlenth;//最近一次计算的文本长度
  mutable bool lenthIsValid;//当前长度是否有效
}

std::size_t text_block::lenth() const
{
   if(!lenthIsValid)
   {
   textlenth=std::strlen(ptext);//现在已经可以这么做了
   lenthIsValid=true;//这样做也不会出错
   }
   return textlenth;
}

在textlenth和lenthIsValid加上mutable关键字之后,即使在const成员函数内,它们也可能会被改变。

避免const和non-const成员函数中的重复

很多时候,我们并不需要 bitwise-constness,前边提到的mutable是个解决办法,但它不能解决所有的const难题。

比如,如果我们对先前的text_block重载了[]运算符,并且不单单只返回对于某个字符的引用(&),还要执行边界检验、数据完整性检验等等操作,我们同时还希望分开处理const对象和non-const对象,所以代码很容易就写成了这样:

class text_block
{
public:
  ...//省略了一部分代码
  const char& operator[](std::size_t index)const
  {
  ...//边界检验
  ...//检查数据完整性
  ...//其他操作
  return text[index];
  }
  char& operator[](std::size_t index)
  {
    ...//边界检验
    ...//检查数据完整性
    ...//其他操作
    return text[index];
    }
private:
  std::string text;
};

我想你一定从各种渠道上都了解过一个短句叫“代码重用性“,毕竟代码重复伴随着诸如编译时间增加、维护困难、代码膨胀等等一系列问题。

当然,很容易想到把那些重复的代码移到另一个成员函数中(一般是private),然后让const 和non-const两个版本的operator[]调用它,但这样还是重复了一些代码,比如函数调用,两次return语句等等。

真正该做的是实现operator[]技能一次,并使用它两次。也就是说,你需要让其中的一个调用另一个。

该怎么做呢。归根结底只是关乎const和non-const的问题,于是这促使我们移除常量性(casting away constness):

class text_block
{
public:
  ...//省略了一部分代码
  const char& operator[](std::size_t index)const
  {
  ...//边界检验
  ...//检查数据完整性
  ...//其他操作
  return text[index];
  }
  char& operator[](std::size_t index)
  {
    return 
         const_cast(static_cast(*this)[index]);
                //调用了const operator[],并移除返回值的常量性
    }
private:
  std::string text;
};

在non-const版本的operator[]中,我们为*this加上了const以便它来调用const版本的operator[],然后再将const版本operator[]的返回值移除常量性用以返回。

如你所见,我们进行了两次转型。我们希望通过non-const来调用他的const版本,但如果没有那个static_cast,这个函数大概会递归调用自己直到崩溃为止。

值得注意的是,反向的做法,也就是让const调用non-const版本——不是你该做的事。

正如先前bitwise constness所提及到的,const成员函数被指明不能改变对象的逻辑状态,毕竟编译器正看着它,non-const成员函数却并没有受到编译器的这般关注。如果在const成员函数中调用non-const成员,就是冒了这样的风险:不应该被改动的对象被改动了。

这就是为什么“const成员函数调用non-const成员函数”是一种错误的行为。

反向调用(我们之前的行为)是安全的:non-const成员函数本来就可以对其对象作出任何操作,所以在调用其中任何一个const成员函数并不会带来风险。

本章的最后,《Effective C++》希望我们记住三点:

  • 将某些东西声明为const可以帮助编译器侦测错误用法。const可以被施加于任何作用域的对象、函数参数、函数返回类型、成员函数本身。

  • 编译器强制施行bitwise constness,但你编写程序的时候应该使用“概念上的常量性”

  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可以避免代码重复。

你可能感兴趣的:(《Effective C++》学习笔记 (3rd.尽可能使用const))