1. 核心法则:以星号 * 为界
Text 中提到:“如果 const 出现在星号左边...如果 const 出现在星号右边...”。
这可以总结为 “左定值,右定址”(或者叫“左内容,右指针”)。
我们在星号处画一条竖线:
const在\*左边 ($\text{const } T *$ 或 $T \text{ const } *$):- 锁定的是:指针指向的数据 (Content)。
- 含义:你不能通过这个指针去修改数据,但你可以改变指针指向哪里。
const在\*右边 ($* \text{ const}$):- 锁定的是:指针本身 (Pointer itself)。
- 含义:指针一旦指向了某个地址,就不能再指向别处(像胶水粘住了一样),但你可以修改该地址上的数据。
2. 代码实例解析
让我们用刚才的法则来重新看文中的例子:
char greeting[] = "Hello";// 1. 没有任何 const
char *p = greeting;
// p 可以指向别处 (p++)
// *p 可以修改内容 (*p = 'h')// 2. const 在左边 (两种写法一样)
const char *p = greeting;
char const *p = greeting;
// p 可以指向别处 (p++) -> ✅ 指针是自由的
// *p = 'H'; -> ❌ 错误!数据是 const 的// 3. const 在右边
char * const p = greeting;
// p = &other_char; -> ❌ 错误!指针是 const 的 (粘住了)
// *p = 'H'; -> ✅ 数据可以修改// 4. 两边都有 const
const char * const p = greeting;
// p 指向哪里不能改,p 指向的数据也不能改。
// 这是最严格的限制。
3. 两种常见的写法(面试常考)
文中特别提到:
void f1(const Widget *pw);
void f2(Widget const *pw);
这两者完全等价。
- 习惯建议:虽然两者通用,但在 C++ 社区中,第一种写法 (
const Widget *) 更加普遍和流行。 - 如何解读:如果你遇到第二种写法 (
Widget const *) 觉得别扭,试着从右往左读:*(pointer to) ->const(constant) ->Widget.- "A pointer to a constant Widget."
4. 迭代器的 Const 陷阱:const iterator vs const_iterator
这是很多 C++ 程序员(甚至是老手)容易搞混的概念。因为迭代器是模仿指针行为设计的,所以它们继承了指针的 const 规则。
我们可以用简单的映射来记忆:
const std::vector<int>::iterator- 对应:
T* const(指针常量)。 - 含义:迭代器本身不能移动(不能
++iter),但它指向的数据可以修改(*iter = 10允许)。 - 实际用途:很少用。因为不能移动的迭代器通常没什么用(只能一直指着同一个位置)。
- 对应:
std::vector<int>::const_iterator- 对应:
const T*(常量指针)。 - 含义:迭代器可以移动(
++cIter允许),但它指向的数据是只读的(*cIter = 10禁止)。 - 实际用途:非常常用! 当你只需要遍历容器读取数据而不修改时,应该总是使用
const_iterator。
- 对应:
关键点:如果你想表达“在这个循环中我不想修改容器里的元素”,你应该用 const_iterator,而不是在 iterator 前面加 const。
5. 函数返回值的保护:防止“暴行”
Scott Meyers 举了一个非常经典的例子:
const Rational operator*(const Rational& lhs, const Rational& rhs);
为什么要给返回值(两个数的乘积)加上 const?
如果不加 const,返回值就是一个临时对象(Temporary Object),在 C++ 中,你是允许对临时对象赋值的(虽然这通常没有意义)。这就导致了文中提到的“暴行”:
(a * b) = c; // 如果 operator* 返回非 const,这句代码能通过编译!
为什么这很危险?
-
逻辑荒谬:给“乘积”赋值没有任何数学意义。
-
隐藏 Bug:最常见的是输入错误,原本想写判断
==,结果写成了赋值=:if (a * b = c) { ... } // 想要做比较,却变成了赋值- 如果
a和b是int,编译器会报错,因为你不能给7赋值。 - 如果
a和b是自定义类且返回值不是const,编译器就会放行,Bug 就此诞生。
- 如果
5.1现代解决方法
5.1.1 引用限定符登场
我们要达到的完美状态是:
operator*返回非 const 的临时对象(为了支持移动)。- 但是禁止在这个临时对象上调用
operator=。
解决方法:给 operator= 加上引用限定符 &。
class Rational {
public:// 注意函数末尾的 '&'// 这意味着:只有当 *this 是一个 左值 (Lvalue) 时,才能调用这个函数Rational& operator=(const Rational& rhs) & {// ... 赋值逻辑 ...return *this;}// 移动赋值同理,通常也希望只对左值赋值Rational& operator=(Rational&& rhs) & {// ...return *this;}
};// operator* 现在可以返回非 const 了,支持移动优化!
Rational operator*(const Rational& lhs, const Rational& rhs) {return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
5.1.2. 效果演示
现在编译器会这样处理:
Rational a, b, c;// 场景 1: 正常赋值
a = b;
// 'a' 是左值(有名对象),满足 operator= 的 '&' 限定。编译通过。// 场景 2: 那个“暴行”
(a * b) = c;
// 1. (a * b) 返回一个临时对象(右值)。
// 2. 试图调用 operator=。
// 3. 编译器检查:operator= 要求对象必须是左值 ('&')。
// 4. (a * b) 是右值,不匹配。
// 5. 编译报错!
这就完美了:既阻止了给临时对象赋值的荒谬行为,又保证了返回值是非 const 的,可以被高效地 Move。
5.1.3. 进阶:同时重载 & 和 &&
引用限定符不仅用于 operator=,还可以用来区分同一个函数的“左值版本”和“右值版本”,从而进行深度优化。
例子:获取内部数据的 data() 函数
class BigData {std::vector<int> numbers;
public:// 如果调用者是左值(持久对象),我们只能返回拷贝(或引用)std::vector<int> data() const & {return numbers; // 发生拷贝}// 如果调用者是右值(临时对象,马上要挂了),我们可以直接把资源 Move 出去!std::vector<int> data() && {return std::move(numbers); // 零拷贝,直接窃取}
};// 使用
BigData obj;
auto v1 = obj.data(); // 调用 data() &,发生拷贝(安全)auto v2 = BigData().data(); // 调用 data() &&,发生移动(高效!)
// BigData() 是临时对象,没必要拷贝它的 numbers,直接偷走就行
6. 避免代码重复 (Avoiding Duplication)
当你的类中同时存在 const 和 non-const 版本的同一个函数(例如 operator[]),且逻辑非常复杂时,你不想写两遍代码。
技巧:让 non-const 版本调用 const 版本。
这是一个稍微有点“反直觉”的高级技巧,需要用到两次转型:
class TextBlock {
public:const char& operator[](std::size_t position) const {// ... 假设这里有很长的边界检查和日志记录代码 ...return text[position];}char& operator[](std::size_t position) {// 1. static_cast: 将 *this 转为 const,以便调用 const 版本// 2. const_cast: 将返回值的 const 移除return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);}// 注意:反过来做(const 调用 non-const)是危险的,因为可能会在 const 函数中意外修改数据。
};