Const可以修饰什么?

 

Const 关键字是万能的,在类外部,你可以用它修饰全局的或者命名空间范围内的常量,也可以用它来修饰文件,函数和块作用域的静态常量。在类内部,你可以使用它来声明静态或者非静态的数据成员。对于指针来说,你可以指定指针本身是不是const,指针指向的数据是不是const,两者可以同时为const或者两者同时为非const.

 Char greeting[]=”Hello”;
Char *p = greeting;//non-const pointer non-const data
Const char *p = greeting; //non-const pointer const data
Char *const p = greeting;//const pointer non-const data
Const char *const p = greeting;//const pointer const data.

在指针中const的位置说明

这项语法并不像看上去那么反复无常,如果关键字const出现在星号的左侧,那么指针指向的内容为const,如果关键字const出现在星号的右侧,那么指针本身是const;如果星号出现在两侧,那么两者都为const.

当指针指向的内容为const时,一些程序员将const放在类型之前,一些程序员将const放在类型之后,星号之前,这两者在意义上是等同的,下面的两个函数有相同的参数类型:

void f1(const Widget *pw);
void f2(Widget const *pw);

因为两种形式在代码中都是存在的,因此应该能够识别两者。

Const在迭代器中应用

STL迭代器是模仿的指针,因此迭代器的行为非常像一个指针,声明一个迭代器的const就像声明一个指针的const: 迭代器不允许指向不同的东西,但迭代器指向的东西是可以改变的。如果你想让迭代器指向的东西不能被修改,这时你就需要一个const_iterator.

std::vector<int> vec;

const std::vector<int>::iterator iter= vec.begin();//iter acts like a T*const
*iter = ;//OK
++iter;//error
std::vector<int>::const_iterator citer = vec.begin();//citer acts like a T const *
*citer = ;//error
++citer;//OK

Const修饰函数返回值和函数参数

Const 的一些强大的用法来源于在函数声明上的应用。在一个函数声明中,const可以修饰函数返回值,函数参数,对于成员函数来说,可以修饰整个函数。

一个函数返回一个const值在一般情况下是不合适的。但有时在保证安全和效率的情况下可以减少客户端的出错率。举个例子,为有理数声明一个operator*函数。

 class Rational{…};
const Rational operator*(const Rational&lhs,const Rational&rhs);

为什么operator*的结果是一个const对象?因为如果不是const,客户端可以提交以下的暴行:

 Rational a,b,c;

(a*b)=c;//invoke operator= on the result of a*b

我不知道为什么程序员会对两个数的乘积进行赋值,但我确实知道程序员虽然不想这么做,但确实这么做了。所有都是因为一个简单的输入错误(一个可以隐式转换成bool的类型):

If(a*b=c)…

如果a和b是内建类型,那么上面的代码是不合法的,一个好的用户自定义类型的特点是它们避免同内建类型的无端的不兼容(上面的代码如果内建类型不合法,那么用户自定义类型也应该不合法)。因此对两个数的乘积进行赋值也就没有理由这么做了。将operator*的返回值声明成const可以阻止这种赋值操作。

对于const参数来说没有什么特别新的,它们就像local的const对象,你应该在任何可以使用它们的时候使用它。除非你像更改一个参数或者本地对象,否则确保它被声明成const,敲打六个字符的努力会可以使你免于类似上面的错误的打搅。

Const 成员函数介绍

将const作用于成员函数上面的意图是能够确认哪些成员函数可以通过const对象被调用。这类成员函数很重要,原因有两条:一,它们使得类的接口更容易被理解,知道哪些函数可以改变对象哪些不可以,这一点很重要。二,它们使得和const对象一起工作成为可能,这是编写高效代码的很重要的方面,因为Item20解释道,提高c++程序性能的基本方法是按const引用传递对象。这项是可行的前提是,const成员函数能够处理const修饰的对象。

许多人忽略了一个事实,不同常量性的两个成员函数是可以重载的,这时c++的重要特性。考虑一个表现文本块的类:

 class TextBlock

 {

   public:

     …

     const char&operator[](std::size_t positioin) const

     {return text[position];}

     char&operator[](std::size_t position)

     {return text[position];}

   private:

      std::string text;
};

TextBlock 的operator[]函数可以像下面这样使用:

 TextBlock tb(“Hello”);

 Std::cout<tb[];//call non-const operator[]

 Const TextBlock ctb(“World”);

 Std::cout<<ctb[];//call const TextBlock::operator[]

顺便说一下,将指向const对象的指针或者指向const对象的引用作为参数经常出现在真实世界的程序中。上面的关于ctb的例子过于造作。下面的例子更符合实际:

void Print(const TextBlock &ctb)

{
std::cout<<ctb[];//call const TextBlock::operator[]
}

通过重载operator[],给不同的函数版本不同的返回值类型,可以对 const和非const TextBlocks进行不同处理:

 std::cout << tb[]; // fine — reading a non-const TextBlock

 tb[] = ’x’; // fine — writing a non-const TextBlock

 std::cout << ctb[]; // fine — reading a const TextBlock

 ctb[] = ’x’; // error! — writing a const TextBlock

注意这里错误的出现是由于调用operator[]的返回值引起的,调用operator[]本身没有问题。问题出现在尝试给一个const char&类型的变量进行赋值操作, const char&是const 版本的operator[]的返回值类型。

同时需要注意non-const operator[]的返回值类型是指向char的引用,如果operator[]返回一个char,那么下面的句子将不能通过编译:

 tb[] = ‘x’;

因为尝试修改内建类型的函数返回值是不合法的。即使是合法的,c++中的按值返回(Item20)意味着我们修改的是tb.text[0]的拷贝,而不是tb.text[0]本身,这不是你需要的行为。

Const成员函数的两个派别

将成员函数声明成const意味这什么,对于这个问题有两种流行的见解:bitwise
constness
(also known as physical constness) and logical constness

Bitwise  const阵营相信当且仅当成员函数不修改对象的任何数据成员(不包括statics数据成员)时它才是const成员函数,举个例子:成员函数没有修改对象内部的任意bits.bitwise
const的一个好处是能够比较容易的识别出反例:编译器只要寻找对数据成员的赋值就可以了。事实上,bitwise
const是常量性的c++定义,const成员函数不允许修改对象的任何非静态数据成员。

不幸的是,许多成员函数没有表现的特别常量性,而不能通过bitwise-const测试。特别的,一个成员函数修改指针指向的内容没有表现出常量性。但是如果指针是在对象内部,这个函数是bitwise
const的,编译器不会发出抱怨。这会导致一个违反直觉的行为。举个例子,我们有个像testBlock的类,用char*而不是string来存储数据,因为它需要同一个不是识别string的C-API进行通讯

 class CTextBlock {

 public:

 ...

 char& operator[](std::size_t position) const // inappropriate (but bitwise

 { return pText[position]; }                  // const) declaration of operator[]

 private:

 char *pText;

 };

 

这个类(不恰当的)将operator[]声明为一个const成员函数,而函数返回的是指向对象内部数据的引用(更深层次的讨论见Item28)。把它抛到一边,注意到operator[]的实现没有以任何方式修改pText。因此,编译器很高兴的为operator[]生成了代码,它毕竟是bitwise const的,这时编译器检查的所有东西,但是看一下允许发生什么: 

 const CTextBlock cctb("Hello"); // declare constant object

 char *pc = &cctb[];  // call the const operator[] to get a pointer to cctb’s data

 *pc = ’J’;                                       // cctb now has the value “Jello”

 

当你用特定值创建一个const对象的时候,这里出现了错误,你只是调用了对象的const成员函数,但是值仍然被修改了!

因此产生了logical constness的概念,这种观点的拥护者辩论到,一个const成员函数可以修改对象内部的一些bits,但是只有在客户端不能够内侦测到的情况下才行。

logical constness的两个问题

  • 问题一
 class CTextBlock {

 public:

 ...

 std::size_t length() const;

 private:

 char *pText;

 std::size_t textLength;     // last calculated length of textblock

 bool lengthIsValid;          // whether length is currently valid

 };

 std::size_t CTextBlock::length() const

 {

 if (!lengthIsValid) {

 textLength = std::strlen(pText); // error! can’t assign to textLength

 lengthIsValid = true;                    // and lengthIsValid in a const

 }                                               // member function

 return textLength;

 }

Length()的这种实现当然不是bitwise const的,textLength和lengthIsValid都有可能被修改。但是看上去这个函数对const CTextBlock对象来说应该是有效的。编译器不同意。它们坚持bitwise constness。该怎么做?

解决方法比较简单:利用c++常量相关的扭动空间,也就是mutable,mutable将数据成员从bitwise constness的约束中释放出来:

 class CTextBlock {

 public:

 ...

 std::size_t length() const;

 private:

 char *pText;

 mutable std::size_t textLength;               // these data members may

 mutable bool lengthIsValid;                    // always be modified, even in const member functions

 std::size_t CTextBlock::length() const

 {

 if (!lengthIsValid) {

 textLength = std::strlen(pText);          // now fine

 lengthIsValid = true;                            // also fine

 }

 return textLength;

 }

 

  • 问题二

Mutable对于bitwise-constness不是很介意的问题是一个好的解决方法,但是它不能够解决所有的const相关的难题。例如:假设TextBlock中的operator[]不仅返回一个指向合适字符的引用,它同时执行边界检查,为访问信息打印日志,可能甚至会检查数据完整性。将所有这些同时放在const和non-const operator[](Item30)函数中,产生了下面这种怪胎:

 class TextBlock {

 public:

 ...

 const char& operator[](std::size_t position) const

 {

 ... // do bounds checking

 ... // log access data

 ... // verify data integrity

 return text[position];

 }

 char& operator[](std::size_t position)

 {

 ... // do bounds checking

 ... // log access data

 ... // verify data integrity

 return text[position];

 }

 private:

 std::string text;

 };

你能说出代码重复,编译时间变长,不易维护,代码膨胀等令人头痛的问题么?当然,可以把边界检查等所有代码移到一个单独的成员函数中(当然是private的),两个版本的opeator[]都会调用这个函数,但是仍然会有重复调用函数,仍然会有重复的返回语句。

你真正需要的是之实现operator[]一次,而使用两次。也就是你需要用一个版本的operator[]去调用另一个版本的operator[]。这把我们带到了如何将constness去掉(casting)的问题。

作为一个通用的规则,casting是一个坏的方法,我用了一整个条款(Item27)来告诉你不要使用casting,但是代码重复也是不令人愉快的。operator[]的const版本做了non-const版本要做的所有事情,唯一的区别是有一个const返回类型。去掉返回值的常量性是安全的,任何人调用non-const operator[]必须在第一个位置有一个non-const的对象。否则它们不能够被叫做non-const函数。所以non-const operator[]调用const版本是防止代码重复的安全的方法,即使需要cast.下面是代码,你在读完后面的解释后可能会更加清楚。

class TextBlock {

public:

...

const char& operator[](std::size_t position) const // same as before

{

...

...

...

return text[position];

}

char& operator[](std::size_t position)                          // now just calls const op[]

{

return

const_cast<char&>(                                                  // cast away const on op[]’s return type;

static_cast<const TextBlock&>(*this) [position]//add const to *this’s type call const version of op[]

);

}

...

};

你所看到的是,代码使用了两个cast,我们想让operator[]的non-const版本调用const版本,但是如果在non-operator[]内部,我们只是调用operator[],我们会递归的调用自己。为了防止无限的递归调用,必须指定我们调用的是const operator[],但是没有直接的方法做到这一点。因此我们需要将*this从原生类型TextBlock& 转换(cast)成const TextBlock&。是的,我们使用cast来添加const.因此我们会使用两个cast:一个为*this添加const,另一个将const从operator[]的返回值中移除。

添加const的cast只是强制进行安全转型(从non-const转换成const),所以我们使用static_cast.将常量性移除只有通过const_cast才能完成,我们没有别的选择。(从技术上来说,一个C风格的cast也能达到目的,但是,正如Item27所描述的,这样的cast不是正确的选择。如果你不熟悉static_cast或者const_cast,参考Item27).

在这个例子中,我们调用的是一个operator,所以语法看起来有些奇怪。这个结果不能赢得选美大赛,但是依赖const版本的函数来实现non-const版本的函数,达到了避免代码重复的效果。为了达到目标而写出如此难看的语法,是你能够决定的,但是依靠const函数来实现non-const成员函数的技术是绝对值得了解的。

更值得了解的是,反向调用-也就是通过const来调用non-const版本的函数来避免代码重复-不是你应该做的。需要记住,const成员函数承诺绝不修改对象的逻辑状态,但是non-const函数并没有这样的承诺。如果你从一个const成员函数中调用一个non-const成员函数,就会出现你承诺不应该被修改的东西最终被修改的风险,这也是为什么用const成员函数调用non-const成员函数是错误的:对象有可能被改变。事实上,为了是代码通过编译,你需要使用const_cast将*this的常量性去除,这是会出现麻烦的迹象。相反的调用顺序是安全的,一个non-const成员函数可以对一个对象做任何它想做的,所以调用const成员函数是没有危险。这也是为什么static_cast可以应用在*this上的原因:没有const相关的危险。

 

读书笔记 effective c++ Item 3 在任何可能的时候使用 const的更多相关文章

  1. 读书笔记 effective c++ Item 38 通过组合(composition)为 “has-a”或者“is-implemented-in-terms-of”建模

    1. 什么是组合(composition)? 组合(composition)是一种类型之间的关系,这种关系当一种类型的对象包含另外一种类型的对象时就会产生.举个例子: class Address { ...

  2. 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

    1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...

  3. 读书笔记 effective c++ Item 1 将c++视为一个语言联邦

    Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...

  4. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  5. 读书笔记 effective c++ Item 11 在operator=中处理自我赋值

    1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...

  6. 读书笔记 effective c++ Item 12 拷贝对象的所有部分

    1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...

  7. 读书笔记 effective c++ Item 13 用对象来管理资源

    1.不要手动释放从函数返回的堆资源 假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来, class Investment { ... ...

  8. 读书笔记 effective c++ Item 14 对资源管理类的拷贝行为要谨慎

    1. 自己实现一个资源管理类 Item 13中介绍了 “资源获取之时也是初始化之时(RAII)”的概念,这个概念被当作资源管理类的“脊柱“,也描述了auto_ptr和tr1::shared_ptr是如 ...

  9. 读书笔记 effective c++ Item 15 在资源管理类中提供对原生(raw)资源的访问

    1.为什么需要访问资源管理类中的原生资源  资源管理类是很奇妙的.它们是防止资源泄漏的堡垒,没有资源泄漏发生是设计良好的系统的一个基本特征.在一个完美的世界中,你需要依赖这样的类来同资源进行交互,绝不 ...

随机推荐

  1. VS中碰到的问题

    1.调试的时候,语句已经注释掉了,但是在执行的时候还是运行了(或者某些变量值改变后,程序依然用的之前数据). 右键解决方案-->清理,然后重新生成.

  2. 解析php addslashes()与addclashes()函数的区别和比较

    一. addslashes() 函数 addslashes(string) 函数在指定的预定义字符前添加反斜杠.这些预定义字符是:•单引号 (')•双引号 (")•反斜杠 (\)•NULL  ...

  3. What Is The Promiscuous Mode

    What Is The Promiscuous Mode? Some Network Interface Cards (NICs) may not allow network traffic afte ...

  4. oracle 11g用户名的大小写问题

    oracle 11g 用户名和密码默认区分大小写,可更改alter system set sec_case_sensitive_logon=false 设置改为不区分大小写.

  5. [HNOI2002]跳蚤 【容斥】

    题目描述 Z城市居住着很多只跳蚤.在Z城市周六生活频道有一个娱乐节目.一只跳蚤将被请上一个高空钢丝的正中央.钢丝很长,可以看作是无限长.节目主持人会给该跳蚤发一张卡片.卡片上写有N+1个自然数.其中最 ...

  6. Hbase(六) hbase Java API

    一. 几个主要 Hbase API 类和数据模型之间的对应关系: 1. HBaseAdmin关系: org.apache.hadoop.hbase.client.HBaseAdmin作用:提供了一个接 ...

  7. Python高级语法总结

    1.Python面向对象 创建类 使用class语句来创建一个新类,class之后为类的名称并以冒号结尾,如下实例: class ClassName: '类的帮助信息' #类文档字符串 class_s ...

  8. Hdu1828 Picture

    Picture Time Limit: 6000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Sub ...

  9. mobiscroll 案例git

    https://github.com/zhoushengmufc/iosselect

  10. T25990 [Wind Festival]Running In The Sky

    T25990 [Wind Festival]Running In The Sky 题目背景 [Night - 20:02[Night−20:02 P.M.]P.M.] 夜空真美啊--但是--快要结束了 ...