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 成员函数,以避免书写更多的重复代码。