(整理自Effctive C++,转载请注明。整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/

关键字const多才多艺,语法变化多端。关于const的基本用法,可以参见我的博客@http://www.cnblogs.com/hust-ghtao/p/3735941.html

本篇博客主要讲了应用const时应该注意的地方。

1 令函数返回一个常量值

令函数返回一个常量值,往往可以降低客户因错误而造成的意外,而又不至于放弃安全性和高效性。例如有理数operator*的声明式:

  1. 1: class Rational {...};

  1. 2: const Rational operator*(const Rational& lhs , const Rational& rhs );

这样做是为了防止客户实现这样的行为:

  1. 1: Rational a , b , c ;

  1. 2: ...

  1. 3: (a*b) = c ;//在a*b的结果上调用operator=

对两个数的乘积再做一次赋值的行为本身很诡异,但程序员会在无意识中那么做,只因为单纯的打字错误:

  1. 1: if( a*b = c )...//哦,原来只是想做个比较

如果a和b都是内置类型,这样的代码直接了当就是不合法。而一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容。将函数operator*的返回值声明为const可以预防那个“没意思的赋值动作”。

至于const参数,就像local const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。多打几个字符,却可以省下恼人的错误。

2 const 成员函数

2.1 用时注意什么

将const 实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。它之所以重要有两个原因:

(1)使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。

(2)使“操作const对象”成为可能。这对编写高效代码是个关键,因为改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,有const成员函数可用来处理取得的const对象。

两个成员函数如果只是常量性不同,可以被重载。例如,考虑以下class,用来表现一大块文字:

  1. 1: class TextBlock

  1. 2: {

  1. 3: public:

  1. 4: ...

  1. 5: const char& operator[]( size_t position ) const //operator[] for const 对象

  1. 6: {

  1. 7: return text[position] ;

  1. 8: }

  1. 9: 

  1. 10: char& operator[]( size_t position ) //operator[] for non-const 对象

  1. 11: {

  1. 12: return text[position] ;

  1. 13: }

  1. 14: 

  1. 15: private:

  1. 16: string text ;

  1. 17: };

TextBlock的operator[]s可被这么使用:

  1. 1: TextBlock tb("Hello") ;

  1. 2: cout << tb[0] ; //调用non-const TextBlock::operator[]

  1. 3: 

  1. 4: const TextBlock ctb("World") ;

  1. 5: cout << ctb[0] ; //调用const TextBlock::operator[]

上述例子太过造作,因为在真实程序中const大多用于passed by pointer-to-const 或 passed by reference-to-const 的传递结果。下述例子比较真实:

  1. 1: void print ( const TextBlock& ctb )

  1. 2: {

  1. 3: ...

  1. 4: cout << ctb[0] ; //调用const TextBlock::operator[]

  1. 5: }

注意:只要重载operator[]并对不同版本给予不同的返回类型,就可以令const和non-const TextBlock获得不同的处理:

  1. 1: cout << tb[0] ; //OK

  1. 2: tb[0] = 'x' ; //OK

  1. 3: 

  1. 4: cout << ctb[0] ; //OK

  1. 5: ctb[0] = 'x' ; // WRONG!试图写一个const TextBlock

上述错误是因为operator[]的返回类型导致,至于operator[]调用自身没问题。错误起因于企图对一个“由const版本之operator[]返回”的const char&施行赋值动作。

另外,non-const operator[]的返回类型是个refercnce to char ,不是char。如果operator[]只是返回一个char,下面这样的句子就无法通过编译:

  1. 1: tb[0]='x' ;

那是因为,如果函数的返回类型是个内置类型,那么改动函数的返回值从来就不合法。纵使合法,C++以by-value返回对象这一事实意味被改动的其实是tb.text[0]的一个副本,不是它本身,那不是你想要的行为。

2.2 bitwise constness 和 logical constness

其实在我们谈论const成员函数的时候,对于其中const的真正含义,还没有一个统一的认识,有两种流派:bitwise constness 和 logical constness。

(1)bitwise constness

此阵营的人认为,成员函数只有在不改变对象之任何成员变量(static除外)时才可以说是const。也就是说它不更改对象内的任何一个bit。这种论点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness 正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。不幸的是许多成员函数虽然不具备十足const性质却能通过bitwise测试。更具体的说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针隶属于对象,那么此函数为bitwise const不会引发编译器异议。这导致反直观结果。

例如:

  1. 1: class CTextBlock{

  1. 2: public:

  1. 3: ...

  1. 4: char& operator[]( size_t position ) const //bitwise const 声明,但其实不恰当

  1. 5: { return pText[position] ; }

  1. 6: private:

  1. 7: char* pText ;

  1. 8: };

请注意,operator[]实现代码并不更改pText。于是编译器为operator[]产生目标代码。它是bitwise const,所有编译器都这么认定。但是看看它允许编译器发生什么事情:

  1. 1: const CTextBlock cctb("Hello") ; //声明一个常量对象

  1. 2: char* pc = &cctb[0] ; //调用const operator[]取得一个指针,指向cctb的数据。

  1. 3: *pc = 'J' ; //cctb现在有了"Jello"这样的内容

这其中当然不该有任何错误:你创建一个常量对象并设以某值,而且只对它调用const成员函数。但终究你还是改变了它的值。这种情况导出所谓的logical constness 。

(2)logical constness

    这一派拥护者主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户侦测不出的情况下才得以如此。例如CTextBlock class有可能高速缓存文本区的长度以便应付查询:

  1. 1: class CTextBlock{

  1. 2: public:

  1. 3: ...

  1. 4: size_t length() const ;

  1. 5: private:

  1. 6: char* pText ;

  1. 7: size_t textLength ; //最近一次计算的文本区块的长度。

  1. 8: bool lengthIsValid ; //目前的长度是否有效。

  1. 9: };

  1. 10: 

  1. 11: size_t CTextBlock::length() const

  1. 12: {

  1. 13: if(!lengthIsValid ){

  1. 14: textLength = strlen(pText ) ; //错误!在const成员函数内不能赋值给textLength 和lengthIsValid

  1. 15: lengthIsValid = true ;

  1. 16: }

  1. 17: return textLength ;

  1. 18: }

length的实现当然不是bitwise const ,因为textLength和lengthIsValid都可能被修改。这两笔数据对于const CTextBlock对象而言虽然可接受,但编译器不同意。

编译器坚持bitwise constness ,但有时客户需要logical constness ,为了解决这个矛盾:利用C++的一个与const相关的摆动场:mutable。mutable释放掉non-static成员变量的bitwise constness约束。

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

假设TextBlock内的operator[]不单只是返回一个reference指向某字符,也执行边界检查、志记访问信息、甚至可能进行数据完善行检验。把所有这些同时放进const和non-const中将会导致大量的代码重复。

  1. 1: class TextBlock{

  1. 2: public:

  1. 3: ...

  1. 4: const char& operator[]( size_t position ) const

  1. 5: {

  1. 6: ... //边界检查

  1. 7: ... //志记数据访问

  1. 8: ... //检查数据完整性

  1. 9: return text[position] ;

  1. 10: }

  1. 11: 

  1. 12: char& operator[]( size_t position )

  1. 13: {

  1. 14: ... //边界检查

  1. 15: ... //志记数据访问

  1. 16: ... //检查数据完整性

  1. 17: return text[position] ;

  1. 18: }

  1. 19: private:

  1. 20: string text ;

  1. 21: };

你能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等问题吗?当然,将边界检查…等所代码移到另一个成员函数中并令两个版本的operator[]调用它,是可能的,但你还是重复了一些代码,例如函数的调用、两次return语句等。

真正应该做的是实现operator[]的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们常量性转除(casting away constness)。本例中,const

operator[]完全做掉了non-const版本该做的一切,唯一不同的是返回类型多了一个const资格修饰。这种情况下如果将返回值的返回值的const转除是安全的,因为不论谁调用non-const operator[]都一定首先有个non-const对象,否则就不能够调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免代码重复的安全做法---即使过程中需要一个转型动作。

  1. 1: class TextBlock{

  1. 2: public:

  1. 3: ...

  1. 4: const char& operator[](size_t position) const

  1. 5: {

  1. 6: ...

  1. 7: ...

  1. 8: ...

  1. 9: return text[position] ;

  1. 10: }

  1. 11: 

  1. 12: char& operator[](size_t position)

  1. 13: {

  1. 14: return

  1. 15: const_cast<char&>( //将op[]返回值的const转除

  1. 16: static_cast<const TextBlock&>(*this) //为*this加上const

  1. 17: [position] //调用const op[]

  1. 18: );

  1. 19: }

  1. 20: ...

  1. 21: };

这份代码有两个转型动作:第一次用来为 *this 添加const(这使得接下来调用operator[]时得以调用const版本),第二次则是从const operator[]返回之中移除const。

值得了解的是,反向做法——令const版本调用non-const版本以避免重复——并不是你应该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const 函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。

反向调用才是安全的:non-const成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数不会带来风险。

请记住:

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

(2) 编译器强制实施bitwise constness ,但你编写的程序应该使用logical constness。

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

Effective C++_笔记_条款03_尽可能使用const的更多相关文章

  1. Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数, ...

  2. Effective C++_笔记_条款02_尽量以const、enum、inline替换#define

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许#d ...

  3. Effective C++_笔记_条款01_视C++为一个语言联邦

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++的各种能力和特性使它成为一个无可匹敌的工具,但也可能引发某 ...

  4. Effective C++阅读笔记_条款2:尽量以const,enum,inline替换#define

    1.#define缺点1 #define NUM 1.2 记号NUM可能没有进入记号表,在调试或者错误信息中,无法知道1.2的含义. 改善:通过const int NUM = 1.2; 2.#dein ...

  5. Effective C++_笔记_条款11_在operator=中处理“自我赋值”

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 为什么会出现自我赋值呢?不明显的自我赋值,是“别名”带来的结果: ...

  6. Effective C++_笔记_条款09_绝不在构造和析构过程中调用virtual函数

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 为方便采用书上的例子,先提出问题,在说解决方案. 1 问题 1: ...

  7. Effective C++_笔记_条款08_别让异常逃离析构函数

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码 ...

  8. Effective C++_笔记_条款07_为多态基类声明virtual析构函数

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 这个规则只适用于polymorphic(带多态性质的)base ...

  9. Effective C++_笔记_条款06_若不想使用编译器自动生成的函数,就该明确拒绝

    (整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 通常如果你不希望class支持某一特定机能,只要不声明对应函数就 ...

随机推荐

  1. 畅通工程续(Dijkstra算法)

    对Dijkstra算法不是很熟悉,写一下思路,希望通过写博客加深理解 Description 某省自从实行了很多年的畅通工程计划后,终于修建了很多路.不过路多了也不好,每次要从一个城镇到另一个城镇时, ...

  2. IntelliJ IDEA 控制台 乱码 有效解决办法

    在Run -> Edit Configuration -> 你的运行Server -> Startup/Connection -> Environment Variables ...

  3. 如何使用银联卡充值美元到BTC-E以及比特币搬砖教程

    1,名词解释 搬砖:就是在价格低的平台买入比特币,然后转移到价格高的平台卖出, 一般而言,BTC-E是国外三大比特币交易所中比特币单价最低的一个站,因为其需要用美元充值,相对不方便.之前国内比特币价格 ...

  4. 一年四个P(Project)

    盼望着,盼望着,提高班众多革命同胞的假期终于来了.伴随着校园之中越来越多的同学身影,暑假学习时的那份静谧一散而去,恍然间在提高班学习的第二个年头也已经过去了(~_~),而自己的大学生涯也就像秋后的蚂蚱 ...

  5. javascript 动态推断html元素

    在javascript中为了针对不同的元素运行不同的操作,须要在javascript中对触发事件的元素进行推断,然后运行不同的操作. 样例: html <input type='button' ...

  6. 侧滑UI

    1.视图 activity_main.xml <com.zyhui.cehua.SlidingMenu xmlns:android="http://schemas.android.co ...

  7. xmlns:android="http://schemas.android.com/apk/res/android的作用是

    xmlns:android="http://schemas.android.com/apk/res/android的作用是 这个是xml的命名空间,有了他,你就可以alt+/作为提示,提示你 ...

  8. Multiple bindings were found on the class path(转)

    Multiple bindings were found on the class path SLF4J API is designed to bind with one and only one u ...

  9. 基于visual Studio2013解决C语言竞赛题之0803报数

       题目

  10. HDU 3790 最短路径问题 (SPFA)

    转载请注明出处:http://blog.csdn.net/a1dark 分析:比一般最短路多了一个花费.多加一个判断即可.用的SPFA.这道题让我搞清楚了以前定义INF为啥爆的问题.受益颇多. #in ...