const 允许你告诉编译器和其他程序员某值应该保持不变。
关键字const 多才多艺。 第一, 你可以用它在class外部修饰global或namespace(见条款2)作用域中的变量, 或修饰文件、函数、或区块作用域(block scope)中被声明为static 的对象。 第二, 你可以用它修饰class 内部的static和non-static成员变量。 对于指针, 你可以指出指针自身、指针所指物, 或者两者都(或都不)是const:
char greeting[] = "Hello"; // non-const pointer, non-const data
char *p = greeting; // non-const pointer, const data
const char *p = greeting; // const pointer, non-const data
const char* const p = greeting; // const pointer, const data
const 出现在星号左边, 表示被指物是常量; 如果出现在星号右边, 表示指针自身是常量,; 如果出现在星号两边, 表示被指物和指针两者都是常量。
如果被指物是常量,const写在类型之前, 或者写在类型之后、星号之前。这下面两种写法的意义相同。
void f1(const Widget* pw); // f1 获得一个指针, 指向一个常量的(不变的)Widget对象
void f2(Widget const * pw); // f2 和f1 同
STL迭代器系以指针为根据被造出来, 所以迭代器的作用就像个T* 指针。 声明 迭代器为const 就像声明指针为const 一样(即声明一个T* const指针), 表示这个迭代器不得指向不同的东西, 但它所指的东西的值是可以改动的。 如果你希望迭代器所指的东西不可被移动(即希望STL模拟一个const T * 指针), 你需要的是const_iterator:
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); // iter 的作用像个T * const
*iter = 10; // 没问题, 改变iter所指物
++iter; // 错误! iter是const
std::vector<int>::const_iterator cIter = vec.begin(); // cIter的作用像个const T *
*cIter = 10; // 错误! *cIter是const
++cIter;// 没问题, 改变cIter
const 最具威力的用法是面对函数声明时的应用。 在一个函数声明内, const 可以和函数的返回值、各参数、函数自身(如果是成员函数)产生关联。
令函数返回一个常量值, 可以降低因客户错误而造成的意外, 而又不至于放弃安全性和高效性。 例如, 有理数(rational numbers, 条款24)的operator* 声明式:
class Rational {...};
const Rational operator*(const Rational &lhs, const Rational & rhs);
为什么返回一个const对象? 原因式如果不这样客户就能实现这样的暴行:
Rational a, b, c;
(a * b) = c; // 在 a*b的结果上调用operator=
我不知道为什么会有人想对两个数值的乘积再做一次赋值(assignment), 但我知道许多程序员会在无意识中那么做, 只因为单纯的打字错误(以及一个可被隐式转换为bool的类型):
if (a *b = c) ...
如果 a和b都是内置类型, 这样的代码直接了当就是不合法。 而一个"良好的用户自定义类型"的特征是它们避免无端地与内置类型不兼容(见条款 18), 因此允许对两值乘积做赋值动作也就没什么意思了。 将operator*的回传值声明为const 可以预防那个"没意思的赋值动作", 这就是该那么做的原因。
const 成员函数
将const实施于成员函数的目的, 是为了确认该成员函数可作用于const对象身上。 这一类成员函数之所以重要, 基于两个理由。
第一, 它们使class接口比较容易被理解。 得知哪个函数可以改动对象内容而哪个函数不行。
第二, 它们使“操作const对象”成为可能。 因为如条款20所言, 改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象, 而此技术可行的前提是, 我们有const成员函数可用来处理取得(并经修饰而成)的const对象。
class TextBlock{
public:
const char & operator[](std::size_t position) const // operator[] for
{
return text[position]; // const 对象
}
char & operator[](std::size_t position) { // operator[] for
return text[position]; // non-const 对象
}
private:
std::string text{};
};
// TextBlock的operator[]s可被这么使用:
TextBlock tb("Hello");
std::cout << tb[0]; // 调用non-const TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0]; // 调用 const TextBlock::operator[]
真实程序中const 对象 大多用于passed by pointer-to-const 或 passed by reference-to-const 的传递结果。 所以下面比较真实:
void print(const TextBlock & ctb) { // 此函数ctb是const
std::cout << ctb[0]; // 调用 const TextBlock::operator[]
}
只要重载operator[] 并对不同版本给予不同的返回类型, 就可以令const 和 non-const TextBlocks获得不同的处理:
std::cout << tb[0]; // 读一个non-const TextBlock
tb[0] = 'x'; // 写一个non-const TextBlock
std::cout << ctb[0]; // 读一个const TextBlock
ctb[0] = 'x'; // 错误! 写一个const TextBlock
上述错误只因operator[] 的返回类型以致, 至于 operator[] 调用动作自身没问题。 错误起因于企图对一个"由 const 版之operator[] 返回"的const char & 施行赋值动作。
non-const operator[]的返回类型是个reference to char, 不是 char。 如果 operator[]只是返回一个char, 下面这样的句子就无法通过编译:
tb[0] = ‘x’;
如果函数的返回类型是个内置类型, 那么改动函数返回值从来就不合法。 纵使合法, C++以by value 返回对象这一事实(见条款20)意味被改动的其实是tb.text[0] 的一个副本, 不是tb.text[0]自身, 那不会是你想要的行为。
成员函数如果是const 意味什么? 有两个流行概念: bitwise constness(又称physical constness) 和 logical constness。
bitwise const 阵营的人相信, 成员函数只有在不更改对象的任何变量(static)时才可以说是const。 也就是说它不更改对象内的任何一个bit。这种论点的好处是很容易侦测违反点: 编译器只需寻找成员变量的赋值动作就行。 bitwise constness 正是C++ 对常量性(constness)的定义, 因此const 成员函数不可以更改对象内任何non-static成员变量。
不幸的是许多成员函数虽然不十足具备const性能却能通过bitwise测试。 更具体地说, 一个更改了"指针所指物"的成员函数虽然不算是const, 但如果只有指针(而非其所指物)隶属于对象, 那么称此函函数为bitwise const 不会引发编译器异议。 这导致反直观的结果。 假设我们有一个TextBlock-like class, 它将数据存储为char *而不是string, 因为它需要和一个不认识string对象的 C API沟通:
class CTextBlock {
public:
char & operator[](std::size_t position) const // bitwise const声明, 但其实不适当
{
return pText[position];
}
private:
char * pText{};
};
该class不适当地将其operator[] 声明为const成员函数, 而该函数却返回一个reference指向对象内部值。 假设暂时不管这个事实, operator[] 实现代码并不更改pText。 于是编译器很开心地为operator[]产出目标码。 它是bitwise const, 所有编译器都这么认定。 但是看看它允许发生什么事:
const CTextBlock cctb("Hello"); // 声明一个常量对象。
char* pc = &cctb[0]; // 调用const operator[] 取得一个指针, 指向cctb的数据
*pc = 'J'; // cctb现在有了"Jello"这样的内容
一个const成员函数可以修改它所处理的对象内的某些bits, 但只有在客户端侦测不出的情况下才得如此。例如你的CTextBlock class有可能高速缓存(cache)文本区块的长度以便应付询问。
Class CTextBlock {
public:
std::size_t length() const;
private:
char * pText;
std::size_t textLength; // 最近一次计算的文本区块长度。
bool lengthIsValid; // 目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // 错误!在const成员函数内不能赋值给textLength和lengthIsValid
lenghIsValid = true;
}
return textLength;
}
length 的实现当然不是bitwise const, 因为textLength和lengthIsValid都有可能被修改。 这两笔数据被修改对const CTextBlock对象而言虽然可接受, 但编译器不同意。 它们坚持bitwise constness。 解决方式: 利用 C+的一个与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:
Class CTextBlock {
public:
std::size_t length() const;
private:
char * pText;
mutable std::size_t textLength; // 最近一次计算的文本区块长度。
mutable bool lengthIsValid; // 目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // 错误!在const成员函数内不能赋值给textLength和lengthIsValid
lenghIsValid = true;
}
return textLength;
}
在 const 和 non-const 成员函数中避免重复
class TextBlock {
public:
const char & operator[](std::size_t position) const
{
// bounds checking
// log access data
// verify data integrity
return text[position];
}
char & operator[] (std::size_t position) {
// bounds checking
// log access data
// verify data integrity
return text[position];
}
private:
std::string text;
};
class TextBlock {
public:
const char & operator[](std::size_t position) const
{
// bounds checking
// log access data
// verify data integrity
return text[position];
}
char & operator[] (std::size_t position) {
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
总结:
- 将某些东西声明为const 可帮助编译器侦测出错误用法。 const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness, 但你编写程序时应该使用“概念上的常量性”。
- 当const 和 non-const 成员函数有着实质等价的实现时, 令non-const 版本调用const 版本可避免代码重复。