Item3: Use const whenever possible

      使用const允许你指定一个语义限制---特定的对象不允许被修改---编译器会强制这项约束。它允许你告诉编译器或其他编码人员某个值应该保持不变。

 

      关键字const是多才多艺的。你可以用它在class外修饰global或namespace scope作用域内的常量,或者修饰文件、函数、区块作用域中被声明为static的对象。你也可以修饰class内部的static和non-static数据成员。对于指针,你可以指定是该指针本身为常量,还是其指向的数据为常量,还是二个都是常量,抑或全都不是:

看起来很复杂吧,呵呵,其实也没有那么复杂。如果关键字const出现在*号的左边,那么指针指向的数据是常量;如果关键字const出现在*号的右边,指针本身是常量。如果const出现在*的二边,则指针和数据都是常量。

 

      当指向的数据是常量时,一些编程人员将const列在类型前面,有些人呢,会把它写在类型之后但在*号之前。这两种写法意义相同,所以下面函数接受相同的参数类型:

 

因为二种形式都存在于实际代码中,你要让你自己习惯于这二种表示方法。

 

      STL迭代器(iterators)是根据指针建模的。所以一个迭代器的作用就像一个T*指针。声明一个迭代器iterator const就像声明了一个指针为const(如声明一个T* const 指针):该迭代器不允许指向不同的东西,但是它指向的东西是可以修改的。如果你想让一个迭代器指向一个不允许被修改的东西时(即希望STL模拟一个const T*指针),你需要一个const_iterator:

const最强力的用法是用在函数声明时。在一个函数声明内,const可以修饰返回值、各参数和函数自身。

 

       让一个函数返回一个常量值通常可以降低因客户错误而造成的意外,同时也不失安全性和高效性。例如,考虑有理数(rational numbers)的operator*声明:

许多程序员第一次看到这个不免斜眼说,唔,为什么operator*返回的值是一个const对象呢?原因是,如果它不是一个const对象,那么用户也许会粗鲁地这样操作:

 

真不晓得为什么一些程序员想要给二个数的乘积赋值,但是我知道有很多程序员无意识中那么做,有时候仅仅是一个打字错误:

如果a和b都是内置类型,这样的代码直截了当就是一个不合法。良好用户定义类型的一个特征就是它们避免了无端地与内置类型不兼容。因此对二个数的乘积进行赋值动作也没有什么意思了。将operator*的返回值声明为const可以防止“没有意义地赋值”。

 

      对于const参数,也没啥新的东西---它们不过是像local const对象一样,你应该在必要的时候使用之。除非你需要改动一个参数或局部对象(local object),你应该将其声明为const。这仅花费你打六个字母的时间。却可以省下恼人的错误。比如说想键入"=="却意外地键入了"="。

 

const Member Functions

 

     将一个成员函数声明为const,主要是为了确认该成员函数可以被用在const对象上。这种成员函数很重要,有二个原因:

1、它们使一个类的接口(interface)更好理解。你可以很清楚地知道哪些函数可以修改对象而哪些函数不可以。

2、它们使“操作const对象”成为可能。

这对编写高效代码是一个关键。正如条款20所说的那样,改善C++程序执行效率的一个根本方法是以pass-by-reference-to-const方式传递对象,而这个技术当且仅当我们有const成员函数可用来处理const对象时,才是可行的。

 

许多人忽略了一个事实:两个成员函数如果只在常量性(constness)上面不同,是可以被重载的。这是C++的一个重要特性。考虑一个实现一大块文字的类:

 

TextBlock's operator[]可以这样使用:

顺带讲一句:const对象在实际程序中大多是由指针或reference-to-const来传递的。所以上面ctb这个例子太假了,来个实际点的:

通过重载operator[]对不同的版本给出不同的返回值,你可以令const和non-const TextBlocks获得不同的处理:

 

 

上面的错误是由于operator[]的返回类型所致,而operator[]调用本身是没问题的。错误在于企图对一个const char&进行赋值。

 

同样注意的是non-const operator[]的返回值类型是一个reference to a char---不是char本身。如果返回值是一个char,那么接下来这句通不过编译:

 

 

那是因为对于一个返回内置类型(built-in type)的函数,修改其返回值从来都是不被允许的。即使它是合法的,C++以by value返回对象这一事实(条款20)意味着被改动的其实是tb.text[0]的一个副本,而不是tb.text[0]本身,而那不会是你想要的行为。

 

成员函数如果是const意味着什么?有两个流行概念:bitwise constness(又称pysical constness)和logical constness。

 

bitwise const 阵营的人相信,一个成员函数是const当且仅当它不修改对象的任何数据成员(除了那些是static的)。也就是说它不更改对象内的任意一个bit。bitwise constness的好处就是可以很轻松地检测到违反点(violation):编译器只需要寻找成员变量的赋值动作即可。事实上,bitwise constness正是C++对常量(constness)的定义。一个const成员函数不允许修改对象内任何non-static的数据成员。

 

不凑巧的是,许多 成员函数不具备const资格却通过了bitwise测试。特别是,一个修改了指针所指物的成员函数通常不能称为const,但如果这个指针在对象内部,那这个成员函数就是bitwise const,而且会通过编译。这会导致反直觉行为。举个例子,假定我们有一个类似于TextBlock的类,其将数据存储为char*而不是string,因为它想要与一个C API通信,而这个C API又不懂string:

这个类(不适当地)声明operator[]为一个const成员函数,即使这个函数返回一个reference 指向对象内部数据。暂且不管这个,请注意operator[]的实现并没有修改pText,于是编译器很开心地为operator[]产生代码;它是bitwise const,所有的编译器都这么认为。但是来看看它允许发生什么事:

这其中肯定哪里出错了,你创建一个常量对象并设以某个值。而且只对它调用了const成员函数,但你终究还是改变了它的值。

 

这种情况导致了logical constness。这一派人主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只在客户端侦查不到的情况下才是如此。例如,你的CTextBlock类可能会缓存文本块的长度以应付询问:

 

 

Length的实现很明显不是bitwise const---textLength和lengthIsValid二个都可以被修改---虽然修改对于const CTextBlock对象而言是可以接受的,但是编译器不同意。它坚持要bitwise constness。那该咋办哩?

 

解决的办法很简单:利用C++里面与const相关的摆动场(wiggle room):mutable。mutable释放non-static数据成员的bitwise constness限制:

 

Avoiding Duplication in const and Non-const Member Functions

    对于“bitwise constness非我所欲”的问题,mutable是一个很好的解决方案,但它不能解决所有与const相关的难题。例如,假定TextBlock(和CTextBlock)里面的operator[]不仅返回一个reference指向相应的字符,也执行边界检测(bounds checking),志记访问信息(logged access information),甚至可能进行数据完善性检验,把这些所有都放进const和non-const operator[]函数中(暂且不管这将会成为一个“长度颇为可议”的隐喻式inline函数---参见条款30)会导致这样的怪物:

 

哎哟!你能说出代码重复以及伴随着的编译时间、维护、代码膨胀等令人头痛的问题吗?当然啦,将边界检测...等所有代码移到另一个成员函数(往往是private),并令两个版本的Operator[]调用它,这是可能的。但是你还是重复了一些代码,例如函数调用、两次return语句。

 

    你真正需要做的是实现operator[]功能一次而调用它两次。这就是说你必须令其中一个调用另一个。这就是传说中的常量性移除(casting away constness)。

 

    按一般守则来讲,转型(casting)是一个糟糕的想法,我将用一整条款来讲这个事情(条款27),告诉你不要那样做。然而代码重复也不是什么令人愉快的经验。本例中const operator[]完全把non-const版本该做的一切都实现了,唯一的不同就是其返回类型多了一个const修饰。在本例中把返回值上的const给casting掉是安全的,因为不论谁调用一个non-const operator[]都要有一个non-const对象,否则就不能调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免代码重复的安全做法---即使过程中需要一个转型动作。下面是代码,稍后会有更详细的解释:

 

你可以看到,这段代码中有二个casts,而不是一个。我们希望non-const operator[]调用其const兄弟,但是在non-const operator[]中若我们只是单纯地调用operator[],会递归调用自己。那大概...唔...进行一百万次。为了避免无限递归,我们必须明确地指出调用const operator[],但C++缺乏直接的语法这样做。因此将*this从其原始类型TextBlock&转型为const TextBlock&。是滴,我们使用转型作为让它加上const!所以一共有二次转型:一次是给*this加上const(这样我们调用const operator[]),第二次是将const operator[]的返回值前面的const去除。

 

     添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),所以我们使用static_cast。移除const的那个动作只可以藉由const_cast完成,没有其它的选择。

 

    至于其他动作,由于本例调用的是操作符,所以语法有一点奇特。恐怕无法赢得毛选美大赛,但却有我们渴望的“避免代码重复”效果,因为它运用const operator[]实现出non-const版本。为了达到那个目标而写出如此难看的语法是否值得,只有由你来决定,但“运用const成员函数实现出其non-const孪生兄弟”的技术是值得了解的。

 

    更值得一提的是,反向做法----令const版本调用non-const版本以避免重复---并不是你该做的事。

如果在const函数内调用non-const函数,就是冒这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么"const成员函数调用non-const成员函数"是一种错误行为。

 

    本条款一开始就提醒你,const是一个奇妙且非比寻常的东西。在指针和迭代器身上:在指针、迭代器及references指向的对象身上;在函数参数和返回类型身上;在local变量身上;在成员函数身上,林林总总不一而足。const是个威力强大的助手。尽可能使用之。你会为你的作为感到高兴地。

 

请记住:

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

2、编译器强制实施bitwise constness,但你编写程序时应该使用"概念上的常量性"(conceptual constness)。

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

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值