Item 3: 尽量使用 const

const 与指针

char greeting[] = "Hello";
char* p = greeting;	//@ non-const data,non-const pointer
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 放在类型前和放在类型后是没有区别的:

//@ 等价的形式
void f1(const Widget *pw);	
void f1(Widget const *pw);	

变与不变

当指针指向的内容是常量时,表示无法通过指针修改变量的值,但是可以通过其它方式修改指针指向变量的值:

int a = 1;
const int *p = &a;
cout << *p << endl;	//@ 1
*p = 2;	//@ error, data is const
a = 2;
cout << *p << endl;	//@ 2

指针本身是常量,表示指针表示的地址是固定的,但是其指向的内容是可以改变的:

int a = 1, b = 2;
int* const p = &a;
cout << *p << endl;	//@ 1
p = &b;	//@ error, pointer is const
*p = b;
cout << *p << endl;	//@ 2

const 与迭代器

STL 迭代器以指针为原型,所以一个 iterator 在行为上非常类似于一个 T* pointer。声明一个 iterator 为 const 就类似于声明一个 pointer 为 const(也就是说,声明一个 T* const pointer):

  • iterator 本身是常量,不能将这个 iterator 指向另外一件不同的东西,但是它所指向的东西本身可以变化。
  • 如果你要一个 iterator 指向一个不能变化的东西你需要一个 const_iterator:
std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;	//@ ok,change what the iterator point to
iter++;		//@ error,iter is const

std::vector<int>::const_iterator cIter =  vec.begin();
*cIter = 10;	//@ error,*cIter is const
++cIter;		//@ ok,change cIter

const 与函数

const 可以用在函数返回值,函数的个别参数,对于成员函数,还可以用于整个函数。

函数返回 const value

函数返回 const value 常常可以在不放弃安全和效率的前提下尽可能减少客户造成的影响:

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

上面函数返回 const object 的原因,可以避免客户如下暴行:

Rational a,b,c;
...
(a * b) = c;	//@ 为两个数的乘积赋值,将返回值声明为const 可以避免此问题

函数参数是 const

无论何时,只要你能,就应该将函数的参数声明为 const 类型,除非你需要改变这个参数。

const 成员函数

  • 声明常量成员函数是为了确定哪些方法可以通过常量对象来访问,另外一方面让接口更加易懂: 很容易知道哪些方法会改变对象,哪些不会。

  • 常量对象只能调用常量方法, 非常量对象优先调用非常量方法,如不存在会调用同名常量方法。

  • 常量成员函数也可以在类声明外定义,但声明和定义都需要指定 const 关键字。

  • 成员方法添加常量限定符属于函数重载:

class TextBlock {
public:
  ...  
  //@ operator[] for const objects
  const char& operator[](std::size_t position) const  
  { return text[position]; }                          

  //@ operator[] for non-const objects
  char& operator[](std::size_t position)           
  { return text[position]; }                          

private:
   std::string text;
};

//@ 使用
TextBlock tb("Hello");
std::cout << tb[0];	//@ calls non-const TextBlock::operator[]
                                       
const TextBlock ctb("World");
std::cout << ctb[0];  //@ calls const TextBlock::operator[]

const objects 在实际程序中最经常出现的是作为这样一个操作的结果:passed by pointer- or reference-to-const:

void print(const TextBlock& ctb)       // in this function, ctb is const
{
  std::cout << ctb[0];                 // calls const TextBlock::operator[]
  ...
}
//@ 对 const 和 non-const 的 TextBlocks 做不同的操作
std::cout << tb[0];   //@ fine — reading a non-const TextBlock
tb[0] = 'x';         //@ fine — writing a non-const TextBlock
std::cout << ctb[0]; //@ fine — reading a const TextBlock
ctb[0] = 'x';       //@ error! — writing a const TextBlock

这里的错误只与被调用的 operator[] 的返回类型有关,而调用 operator[] 本身总是正确的。错误出现在企图为 const char& 赋值的时候,因为它是 const 版本的 operator[] 的返回类型。

再请注意 non-const 版本的 operator[] 的返回类型是一个 char 的引用而不是一个 char 本身。如果 operator[] 只是返回一个简单的 char,下面的语句将无法编译:

tb[0] = 'x';

比特常量和逻辑常量

比特常量(bitwise constness):如果一个方法不改变对象的任何非静态变量,那么该方法是常量方法。 比特常量是C++定义常量的方式,然而一个满足比特常量的方法,却不见得表现得像个常量,尤其数据成员是指针时:

class CTextBlock {
public:
  ...
  char& operator[](std::size_t position) const   // inappropriate (but bitwise
  { return pText[position]; }                    // const) declaration of
                                                 // operator[]
private:
  char *pText;
};

看看 operator[] 的实现,它并没有使用任何手段改变 pText。结果,编译器愉快地生成了 operator[] 的代码,因为毕竟对所有编译器而言,它都是 bitwise const 的,但是我们看看会发生什么:

const CTextBlock cctb("Hello");   //@ declare constant object
char *pc = &cctb[0];  //@ call the const operator[] to get a pointer to cctb's data
*pc = 'J'; //@ cctb now has the value "Jello"

这里确实出了问题,你用一个确定值创建一个常量对象,然后你只是用它调用了 const 成员函数,但是你还是改变了它的值!

这一点不合理之处引发了 逻辑常量(logical constness)的讨论:常量方法可以修改数据成员, 只要客户检测不到变化就可以。可是常量方法修改数据成员C++编译器不会同意的!这时我们需要 mutable 限定符:

class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;

  //@ these data members may always be modified, even in const member functions
  mutable std::size_t textLength;        
  mutable bool lengthIsValid;             
};    

std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);      //@ now fine
    lengthIsValid = true;                 //@ also fine
  }

  return textLength;
}

避免常量/非常量方法的重复

通常我们需要定义成对的常量和普通方法,只是返回值的修改权限不同。 当然我们不希望重新编写方法的逻辑。最先想到的方法是常量方法调用普通方法,然而这是C++语法不允许的。 于是我们只能用普通方法调用常量方法,并做相应的类型转换:

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&>(
        static_cast<const TextBlock&>(*this)[position]);
  }
...
};
  • *this 的类型是 TextBlock ,先把它强制隐式转换为 const TextBlock,这样才能调用常量方法。
  • 调用 operator[](size_t) const ,得到的返回值类型为 const char&。
  • 把返回值去掉 const 属性,得到类型为 char& 的返回值。

总结

  • const 与指针:
    • const 在前表示指针指向的内容是常量。
    • 指针在前表示指针本身是常量。
  • const 与迭代器:
    • const 修饰迭代器时表示迭代器本身是常量。
    • 迭代器指向的内容是常量时应该使用 const_iterator。
  • const 与函数
    • 函数返回值为 const 可以避免一些意外赋值的情况发生。
    • 尽可能的将函数参数声明为 const。
    • 常量对象只能调用常量方法, 非常量对象优先调用非常量方法,如不存在会调用同名常量方法。
    • 如果一个方法不改变对象的任何非静态变量,那么该方法是常量方法。
    • mutable 限定符对于即使是 const 的对象也可以做修改。
    • 可以利用 const 成员函数实现等价的非 const 成员函数,以避免书写更多的重复代码。
posted @ 2019-12-31 09:32  刘-皇叔  阅读(183)  评论(0编辑  收藏  举报