第3条:尽可能使用 const

来源:互联网 发布:mac 安装lnmp php7 编辑:程序博客网 时间:2024/06/13 01:47
const的用处就是:可以通过它来指定一个语义上的约束(一个特定的不能被更改的对象)这由编译器来保证。通过一个const,可以让编译器和其他程序员知道,你的程序中有一个数值需要保持恒定不变。
const用途十分广泛。可以在类外部修饰全局(global)的或者名字空间域(namespace)的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。你可以在灰内部使用它来修饰静态的或者非静态的数据成员。对于指针,你可以制定一个指针是否是 const 的,其所指的数据是否是 const 的,或者两者都是或者两者都不是。如下:
char greeting[]= "Hello";char *p =greeting;                    // 非 const 指针,非 const 数据const char *p =greeting;              // 非 const 指针, const 数据char * const p =greeting;             // const 指针,非 const 数据const char *const p = greeting;       // const 指针, const 数据//const修饰采用就近原则


上面语法看起来混乱,但并不复杂。如果const出现在星号左边,表示被指的物是常量。如果const出现在星号右边,说明指针自身是常量。同理若出现在两边,则被指的物和指针自身都是常量。
当所指向的物为常量时,一些程序员喜欢把 const 放在类型之前;有些则喜欢放在类型后边,但都要在* 的前边。这两种写法的意义相同:
 
void f1(const Widget *pw);  // f1 传入一个指向 Widget 对象常量的指针void f2(Widget const *pw);  // f2 同上

上面两种写法我们都应该熟悉。
 
STL迭代器是依照指针模型创建的,所以迭代器的作用就如T*指针。声明迭代器为const就像声明指针为const一样。(即声明一个T* const指针),表示iterator不能指向不同的东西,但指向的东西的内容是可以修改的。如果你希望iterator所指的东西不能被修改(即const T*指针),此时需要一个const_iterator。
std::vector<int>vec;…conststd::vector<int>::iterator iter=vec.begin(); //iter就如T* const*iter=10;             //正确,改变iter所指物++iter;               //错误,iter不能被修改 std::vector<int>const_iteratoriter2=vec.begin(); //iter2就如const T**iter2=10;        //错误,itre2所指内容不能被修改++iter2;          //正确,iter2可以改变
 
const 在函数声明方面用途强大。在一个函数声明的内, const 可以应用在返回值、各参数,对于成员函数,可以将其本身声明为 const 的。
函数返回一个常量通常可以避免错误的发生,同时兼顾了安全和效率的问题。如有理数乘法函数( operator* )的声明
class Rational{…};const Rational operator*(const Rational& lParm,consst Rational& rParam);
很多人不明白为什么要返回一个const对象?但如果不这样就可能发生下面的问题:
Rational a,b,c;…(a*b)=c;        //在a*b的基础上调用oprerator=


尽管不明白为什么会对两数的乘积再一次赋值(assignment),但很多人在无意识中确实那么做了,只是单纯的输入错误(本意也许是一个bool表达式):if(a*b=c)…        //只是想进行一次比较显然,如果 a 和 b 是内置数据类型,那么这样的代码就是非法的。一个“良好的用户自定义类型”的特点之一是它们避免了与内置类型的不兼容“(另请参见第 18 项),而允许为两数乘积赋值毫无意义。声明 operator* 函数时如果让其返回一个 const 对象可以避免这一错误,这就是返回值声明为const的原因。
对于const参数,没有新概念,就像localconst对象一样,在必要的时候尽可能使用他们,除非你需要修改某个参数或局部对象,其他的所有情况最好声明为 const,仅仅需要多打6个字母,却省少了好多事情。(比如上面的“==”不小心打成了“=”)const 成员函数对成员函数使用 const 的目的是指明这些成员函数可以被 const 对象调用。这一类成员函数很重要主要有两个理由。第一,它们使class接口更易于理解。知道函数是否可以修改对象内容很重要。第二,它们使“操作const对象”成为可能。这对于高效编码十分重要,这是由于(将在第 20 项中展开解释)提高 C++ 程序效率的一条基本的途径就是:传递对象的 const 引用(以reference-to-const方式传递对象)。而这的前提是:我们有const成员函数可以用来处理取得(并经修饰而成)的const对象。如果若干成员函数之间的区别仅仅为“是否是 const 的”,那么它们也可以被重载。很多人都忽略了这一点,这是 C++的一个重要特性。考虑下面代码,这是一个文字块的类:
class Text{       public:              ...              const char& operator[](std::size_tpos) const                     {                            return text[pos];//operator[] for const对象  用于返回相应位置的字符                     }              char& operator[](std::size_tpos)                     {                            returntext[pos];  //operator[]for non-const对象                     }       private:              std::string text; };Text类的operator[]可以这么使用:Texttx1("Hello");std::cout<<tx1[0];     //调用no-const Text::operator[] const Texttx2("Word");std::cout<<tx2[0];     //调用const Text::operator[]


顺便说下,真实程序中const对象大多用于passedby point-to-const或passed byreference-to-const,下面这个例子真实情况中常会出现:
void print(constText& tx)       // 在这个函数中 tx 是 const 的{  std::cout << tx[0];        // 调用 const 的 Text::operator[]  ...}

只要重载operator[]并对不同的版本给予不同的返回值类型。就可以让const或非const的Text获得不同的处理:
std::cout<<tx1[0];   //正确,读一个non-const Texttx1[0]=’x’;         //正确,写一个non-const Textstd::cout<<tx2[0];   //正确,读一个const Texttx2[0]=’x’;         //错误,写一个const Text

要注意的是,这一错误只与所调用的 operator[] 的返回值的类型有关,错误在于:企图为一个 const char& 赋值,而 const char& 则是 operator[] 的 const 版本的返回值类型。
同时要注意的是,operator[]返回的是一个reference-to-cha(char的引用),而不是一个简单的char,如果返回的是char,那下面的代码无法通过编译:
tx[0]=’x’;
这是因为,企图修改一个返回内置数据类型的函数的返回值本来都是非法的。假设这样做合法,而 C++ 是通过传值返回对象的,所修改只是 tx.text[0] 复的一份副本,而不是 tx.text[0] 本身,不会得到预期的效果。
我们暂停一下,把一个成员函数声明为 const 的有什么意义?这里有两个流行的说法:按位恒定(也可叫做物理恒定)和逻辑恒定。
按位恒定坚信:当且仅当一个成员函数不修改任何对象的数据成员( static 数据成员除外)时,才需要将这一成员函数声明为 const 的,换句话说,将成员函数声明为 const 的条件是:成员函数不对对象内部做任何的改动。按位恒定的好处之一就是,它使得错误检查便得更轻松:编译器仅需要查找对数据成员的赋值。实际上,按位恒定就是 C++ 对于constness的定义,因此const成员函数不可以更改对象内任何non-tatic成员。不幸的是,大多数不完全是 const 的成员函数也可以通过bitwise constness的检验。具体的说,一个更改了“指针所指对象”的成员函数虽然不能是const的。但只要这个指针存在于一个对象中(而非所指对象),这个函数就是bitwise constness的,这时编译器不会报错。这样会导致编成的行为不符合常规习惯。比如说,我们手头有一个类似于 Tex 的类,其中保存着 char*类型的数据而不是string ,因为这段代码有可能要与一些C 语言的 API 交互,但是 C 语言中没有 string 对象一说。
class Text{public:  ...  char& operator[](std::size_t pos) const  // operator[] 不恰当的(但是符合按位恒定规则)定义方法  { return pText[pos]; }private:  char *pText;};

尽管 operator[] 返回一个对象内部数据的引用,这个类仍(不恰当地)将其声明为 const 的 成员函数(第 28 项将深入讨论这个问题)。先忽略这个问题,请注意 operator[] 的实现中并没有以任何形式修改 pText 。于是编译器便会欣然接受这样的做法,毕竟,所有的编译器所检查的是“代码是否符合按位恒定规则”。但是请观察,在编译器的纵容下,还会有什么样的事情发生:
const Text ctx(“Hello”);  //声明对象常量char*pc=&ctx[0];     // 调用 const 的 operator[]*pc=’J’;      //ctx值为Jello

这其中当然不该有错误,创建一个常量并设以某值,而且只对它调用const成员函数。但终究还是改变了它的值。
辑恒定应运而生。坚持这一宗旨的人认为,一个 const 的成员函数可以修改其所处对象内的某些bits,但是仅仅以客户端无法察觉的方式进行。比如说,你的 Text 类可能需要保存文字块的长度,以便在需要的时候调用:
 
class Text {public:  ...  std::size_t length() const;private:  char *pText;  std::size_t textLength;      // 最近一次计算出的文本区的长度  bool lengthIsValid;          // 当前长度是否可用};std::size_t Text::length()const{  if (!lengthIsValid) {textLength =std::strlen(pText);    // 错误!不能在 const 成员函数中lengthIsValid =true;          // 对 textLength 和 lengthIsValid 赋值  }  return textLength;}

length的实现当然不是bitwise const,因为textLength和lengthIsValid 都可能被改动。这个改动对const Text对象而言虽然可以接受,但编译器不通过。它们坚持bitwise const。那我们怎么办。
解决方法很简单:利用 C++ 中 与 const 相关的灵活性,使用可变的( mutable )数据成员。 mutable 可以使非静态数据成员不受bitwise const的约束:
class Text {public:  ...  std::size_t length() const;private:  char *pText;  mutable std::size_t textLength;// 这些数据成员在任何情况下均可修改  mutable bool lengthIsValid;     // 在 const 成员函数中也可以}; std::size_tCTextBlock::length() const{  if (!lengthIsValid) {    textLength = std::strlen(pText);    // 现在可以修改了    lengthIsValid = true;               // 同上  }  return textLength;}


避免const和non-const成员函数的重复
对于bitwise const问题,虽然mutable是个解决方法,但它不能解决所有问题。比如Text中的 operator[] 不仅仅返回一个对恰当字符的引用,同时还要进行边界检查、记录访问信息,甚至还要进行数据完整性检测。如果将所有这些统统放在 const 或非 const 函数(我们现在会得到过于冗长的隐式内联函数,不过不要惊慌,在第 30 项中这个问题会得到解决)中,看看我们会得到什么样的庞然大物:
class Text{public:  ...  const char& operator[](std::size_tposition) const  {    ...                                 // 边界检查    ...                                 // 记录数据访问信息    ...                                 // 确认数据完整性    return text[position];  }  char& operator[](std::size_t position)  {    ...                                 // 边界检查    ...                                 // 记录数据访问信息    ...                                 // 确认数据完整性    return text[position];  }private:   std::string text;};


上面出现的问题:重复代码,以及随之而来的编译时间增长、维护成本增加、代码膨胀、等等……当然,像边界检查这一类代码是可以移走的,它们可以单独放在一个成员函数(私有的)中,然后让这两个版本的 operator[] 来调用它,但是你的代码仍然有重复的函数调用,以及重复的 return 语句。
对于 operator[] 你真正需要的是:一次实现,两次使用。也就是说,你需要一个版本的 operator[] 来调用另一个。这样便可以通过转型来消去函数的恒定性。(casting away constness)
通常情况下转型是一个坏主意,(第 21 项)将说明为什么不用转型,但是代码重复也是个头痛的问题。在这种情况下, const 版的 operator[] 与非 const 版的 operator[] 所做的事情完全相同,不同的仅仅const版的返回值是 const 的。通过转型来消去返回值的恒定性是安全的,这是因为任何人调用这一non-const的 operator[] 首先必须拥有一个non-const 的对象,否则它就不能调用non-const 函数。所以尽管需要一次转型,令non-const operator[]调用其const可以安全地避免代码重复。下面是实例代码,会有更详细的解释
class Text{public:  ...  const char& operator[](std::size_tposition) const     // 同上  {    ...    return text[position];  }  char& operator[](std::size_tposition)  // 现在仅调用 const 的 op[]  {    return      const_cast<char&>( static_cast<constText&>(*this) [position]);// 通过对 op[] 的返回值进行转型,消去 const ;// 为 *this 的类型添加 const ;// 调用 const 版本的 op[]  }...};


如你所见,上面的代码进行了两次转型。我们要non-const 的 operator[] 去调用 const 版本,但是如果在non-const 的 operator[] 的内部,我们只调用 operator[] 而不标明 const ,那么函数将对自己进行递归调用。那将是成千上万次的毫无意义的操作。为了避免无穷递归的出现,我们必须要指明我们要调用的是 const 版本的 operator[] ,但是手头并没有直接的办法。我们可以用 *this 从 Text& 转型到 const Text& 来取代。是的,我们使用了一次转型添加了一个 const !这样我们就进行了两次转型:一次为 *this 添加了 const (于是对于 operator[] 的调用将会正确地选择 const 版本),第二次转型消去了 const operator[] 返回值中的 const 。
添加 const 的那次转型是为了保证转换工作的安全性(从一个非 const 对象转换为一个 const 的),这项工作的关键字是 static_cast 。消去 const 的工作只可以通过 const_cast 来完成,所以在这里我们实际上并没有其他的选择。(从技术上讲是有的。 C 语 言风格的转型在这里也能工作,但是,就像我在第 27 项中所讲的,这一类转型在很多情况下都不是好的选择。如果你对于 static_cast 和 const_cast 还不熟悉,第 27 项中有详细的介绍。)至于其他动作,本例调用的是操作符,语法相对奇怪。但是通过以 const 版本的形式实现non-const 版本的 operator[] ,可以避免代码重复。为达到这一目标而写下看似笨拙的代码,这样做是否值得在于你的选择,但是,以 const 版本的形式来实现非 const 的成员函数是值得了解的。 更值得你了解的是按反方向完成上面的工作——通过让 const 版本的函数调用non-const 版本来避免代码重复——一定不要这样做。请记住,一个 const 成员函数保证永远不会更改其对象逻辑状态,non-const 的成员函数并没有这一类的保证。如果你在一个 const 函数中调用了一个non-const 函数,曾保证不会被改动的对象就有被修改的风险。这就是为什么说让一个 const 函数调用一个non-const 函数是错误的:因为对象有可能会被修改。实际上,为了使代码能够得到编译,你还需要使用一个 const_cast 来消去 *this 的 const 属性,显然这是不必要的麻烦。上一段中相反的调用次序才是安全的:non-const 成员函数可以对一个对象做任何想做的事情,因此调用一个 const 成员函数不会带来任何风险。这就是为什么 static_cast 在没有与 const 相关的危险的情况下可以正常工作的原因。 就像本项最开始所说的, const 是一个令人赞叹的东西。对于指针和迭代器,以及指针、迭代器和引用所涉及的对象,函数的参数和返回值,局部变量,成员函数来说, const 都是一个强大的助手。只要可能就可以使用它。你会对你所做的事情感到高兴的。 
需要记住的
1.将一些东西声明为 const 的可以帮助编译器及时发现错误语法。 const可以施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本身。2.编译器严格遵守bitwise constness(按位恒定),但是你应该在需要时应用逻辑恒定(conceptual constness)。3.当 const 和non-const 成员函数的实现在本质上相同时,可以通过使用一个non-const 版本来调用 const来避免代码重复。
0 0
原创粉丝点击