const允许你指定一个语义约束(指定一个不该被改动的对象),而编译器会强制实施这项约束。只有某值保持不变是事实,你就该说出来,以获得编译器的帮助,确保这条约束不被违反。
const可以用来修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象。你也可以用它修饰类内的static和non-static成员变量。对于指针,你也可以指定指针自身、指针指向的对象,或两者都是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可以和返回值,各参数,函数自身(成员函数)产生关联。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数的 operator*
声明式:
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
为什么返回一个const对象?原因是如果不这样客户就能实现这样的暴行:
Rational a, b, c;
...
(a * b) = c; // 在a * b上调用operator=
许多程序员会在无意识中这么做,可能只因为单纯的打字错误(以及一个可被隐式转换为bool的类型):
if (a * b = c) ... // 其实是想做一个比较
将 operator*
的返回值声明为const可以预防上述的赋值操作。
至于const参数,它们就像局部const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。
将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象上。const成员函数可以使接口比较容易被理解,并使操作const对象成为可能。
两个成员函数如果只是常量性不同,可以被重载。考虑以下class,用来表现一大块文字:
class TextBlock {
public:
...
const char& operator[](std::size_t position) 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[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) {
std::cout << ctb[10]; // const TextBlock::operator[]
...
}
只要重载 operator[]
并对不同版本给予不同版本的返回类型,就可以令const和non-const TextBlock获得不同的处理:
std::cout << tb[0]; // read only
tb[0] = 'X'; // write only
std::cout << ctb[0]; // read only
ctb[0] = 'Y'; // error! write
错误是因为企图对一个const版本的 operator[]
返回的 const char&
进行赋值。
bitwise constness指成员函数只有在不更改对象的任何成员变量时才可以是const。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。
许多成员函数虽然不完全具备const性质却能通过bitwise测试。一个更改了指针指向的对象的成员函数虽然不能算是const,但如果只有指针属于对象,那么称此函数为bitwise const不会引发编译器异议。假设我们有一个CTextBlock对象,它将数据存储为char*而不是string:
class CTestBlock {
public:
...
char& operator[](std::size_t position) const // bitwise const声明
{ return pText[position]; }
private:
char* pText;
};
这个class不适当地将 operator[]
声明为const成员函数,而该函数却返回一个引用指向对象内部指。请注意,operator[]
实现代码并不更改pText。于是编译器认为是bitwise const。看看它允许发生什么事:
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';
这当然没有任何的错误:你创建了一个常对象,只调用了它的常成员函数,但你终究还是改变了它的值。
logical constness指一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户侦测不出的情况下才得如此。例如你的CTextBlock有可能高速缓存文本区块的长度以便应付询问:
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); // error
lengthIsValid = true; // error
}
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;
mutable std::size_t textLength; // 最近一次计算的文本区块长度
mutable bool lengthIsValid; // 目前的长度是否有效
};
std::size_t CTextBlock::length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // ok
lengthIsValid = true; // ok
}
return textLength;
}
对于bitwise constness的非我所欲的问题,mutable是个解决办法,但它不能解决所有的const相关难题。假设TextBlock(和CTextBlock)内的 operator[]
不但只是返回一个引用,也执行边界检查、日志访问信息,甚至可能进行数据完善性检验。把所有这些放进const和non-const operator[]
中,导致这样的怪物:
class TextBlock {
public:
const char& operator[](std::size_t position) const {
... // 边界检查
... // 日志访问
... // 检验数据完整性
return text[position];
}
char& operator[](std::size_t position) {
... // 边界检查
... // 日志访问
... // 检验数据完整性
return text[position];
}
private:
std::string text;
};
你真正该做的是实现 operator[]
的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们将常量性转除。
令non-const operator[]
调用其 const operator[]
是一个避免代码重复的安全做法——即使过程中需要一个转型动作。下面是代码:
class TextBlock {
public:
const char& operator[](std::size_t position) const {
... // 边界检查
... // 日志访问
... // 检验数据完整性
return text[position];
}
char& operator[](std::size_t position) {
return const_cast<char&>( // op[]返回值的const移除
static_cast<const TextBlock&>(*this) // 为*this加上const
[position] // 调用const op[]
);
}
...
};
值得了解的是,反向做法——令const版本调用non-const版本以避免重复——并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。实际上若要令这样的代码通过编译,你必须使用 const_cast
将 *this
的const性质解放掉。
请记住
- 将某些东西声明为const可帮助编译器侦测错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用概念上的常量性。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。