const的正确姿势:从变量、函数到成员函数
常量正确性(Const Correctness) 是编写健壮、安全且易于理解的 C++ 代码的基石。它并非一个可选的特性,而是一种核心设计哲学。它通过类型系统向编译器和其他程序员传达你的设计意图:“这个对象或数据不应被修改”。正确地使用 const
可以避免意外的修改,使代码更安全;它可以作为文档,提高代码可读性;并且它能为编译器提供更多的优化机会。
本文将深入探讨 const
在不同语境下的含义、细微差别和最佳实践。
1. 基础:const 变量和指针
让我们从最基本的用法开始。
const 变量
最简单的用法是定义一個常量。这意味着一旦初始化,其值便不可更改。
const int bufferSize = 1024; // 值不能修改
bufferSize = 2048; // 错误:表达式必须是可修改的左值
const std::string name = "Alice"; // 对象本身不能修改
最佳实践:默认情况下,将不应被修改的变量声明为 const
。这可以防止你或你的同事在后面意外地修改它。
指针:const 的“绕口令”
指针涉及两个对象:指针本身和它所指向的数据。const
的位置决定了谁是不可变的。
-
指向 const 的指针(Pointer to const)
这表示数据是常量,但指针本身可以指向别处。const int* ptr; // ptr 是一个指针,它指向一个 const int int const* ptr; // 等价的写法,同样是指向 const int 的指针 int value = 10; const int const_value = 20; ptr = &value; // 正确:允许将非常量地址赋给指向常量的指针(承诺不会通过ptr修改value) ptr = &const_value; // 正确 *ptr = 30; // 错误:不能通过 ptr 修改它所指的值 value = 30; // 正确:因为 value 本身不是 const
-
const 指针(Const pointer)
这表示指针本身是常量(必须初始化,且不能再指向其他地址),但数据可以修改。int* const const_ptr = &value; // const_ptr 是一个const指针,它指向一个 int *const_ptr = 40; // 正确:可以修改所指的值 const_ptr = &const_value; // 错误:不能修改 const_ptr 本身的值(即地址)
-
指向 const 的 const 指针(Const pointer to const)
两者都是常量,既不能修改指针,也不能通过指针修改数据。const int* const const_ptr_to_const = &const_value; *const_ptr_to_const = 50; // 错误 const_ptr_to_const = &value; // 错误
记忆口诀:const
修饰它左边的东西。如果左边没东西,就修饰右边的东西。
const int*
->(*ptr)
是const int
int const*
->(*ptr)
是const int
int* const
->ptr
是const (pointer)
2. 函数中的 const:参数与返回值
const 参数
将函数参数声明为指向 const
或 const
引用,是常量正确性的最重要应用之一。
- 值传递(Pass-by-value):
void func(int x)
- 传入的实参会被拷贝,在函数内修改
x
不影响外部。加const
意义不大(仅限于函数内部实现),通常省略。
- 传入的实参会被拷贝,在函数内修改
- 引用/指针传递(Pass-by-reference/pointer):
void func(const BigObject& obj)
,void func(const BigObject* obj)
- 这是关键! 使用
const &
(或const *
)可以避免昂贵的拷贝,同时向调用者保证:“我绝不会修改你传入的对象”。这使函数可以接受常量和非常量实参,更加通用。 - (Effective C++ 条款 20):宁以
pass-by-reference-to-const
替换pass-by-value
。对于内置类型和 STL 迭代器、函数对象,pass-by-value 往往更合适。
- 这是关键! 使用
错误示范:
void badFunction(std::string& str); // 这个函数承诺会修改 str
std::string myString = "Hello";
const std::string myConstString = "World";
badFunction(myString); // 正确
badFunction(myConstString); // 错误!不能将 const 引用绑定到非 const 引用参数上
正确示范:
void goodFunction(const std::string& str); // 这个函数承诺不会修改 str
goodFunction(myString); // 正确:非常量可以转换为常量
goodFunction(myConstString); // 正确
// 两个调用都适用,函数是通用的。
const 返回值
返回 const
值通常用于自定义类型,以防止返回值被意外修改,但这种用法比较少见,且有争议。
class Fraction { ... };
const Fraction operator*(const Fraction& lhs, const Fraction& rhs);
Fraction a, b, c;
...
(a * b) = c; // 如果没有顶层的const,这句代码是合法的但无意义的!
// 有了const返回值,这行代码会报错,阻止了这种无意义的操作。
对于内置类型,返回 const
值没有意义。现代 C++ 中,这种用法已不常见。
3. 核心战场:const 成员函数
将成员函数声明为 const
是常量正确性的另一核心,它表明“这个函数不会修改对象的可观测状态”。
语法与含义
class TextBlock {
public:
// const 成员函数
std::size_t length() const {
return text.length();
}
// 非 const 成员函数
void append(const std::string& extra) {
text += extra;
}
private:
std::string text;
};
// 使用
const TextBlock ctb("Hello");
TextBlock tb("World");
std::cout << ctb.length(); // 正确:const 对象可以调用 const 成员函数
ctb.append("!"); // 错误:const 对象不能调用非 const 成员函数
std::cout << tb.length(); // 正确:非 const 对象可以调用 const 成员函数
tb.append("!"); // 正确
重要规则:
const
对象只能调用const
成员函数。- 非
const
对象可以调用任何成员函数(优先调用非const
版本,如果存在重载)。
Bitwise constness vs Logical constness
这是一个关键区别,(Effective C++ 条款 3) 对此有精彩论述。
-
Bitwise constness(物理常量性):
- 编译器强制执行的标准:一个
const
成员函数只有在不修改对象的任何非静态成员变量(bit)时,才是 bitwise const 的。 - 它检查的是“对象存储的比特位是否被改变”。
- 编译器强制执行的标准:一个
-
Logical constness(逻辑常量性):
- 程序员所追求的:一个
const
成员函数可能会修改一些比特,但只要这些修改对用户(调用者)是不可见的(不影响对象的可观测状态),它就是逻辑上 const 的。 - 例如,修改一个“缓存”或“访问计数”的 mutable 成员。
- 程序员所追求的:一个
例子:
class CTextBlock {
public:
// 这是一个 bitwise const 函数,但它合适吗?
const char& operator[](std::size_t position) const {
return pText[position];
}
// 非 const 版本
char& operator[](std::size_t position) {
return pText[position];
}
// 一个逻辑上应该是 const 的函数,但需要修改缓存长度
std::size_t getLength() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 错误!不能在 const 成员函数内修改成员
lengthIsValid = true; // 错误!
}
return textLength;
}
private:
char* pText;
std::size_t textLength; // 上一次计算的文本长度
bool lengthIsValid; // 当前长度是否有效
};
上面的 getLength
函数在逻辑上应该是 const
的,因为它不向用户提供修改后的文本内容。但它需要修改缓存变量 textLength
和 lengthIsValid
,这违反了 bitwise constness。
解决方案:使用 mutable
关键字。
class CTextBlock {
public:
std::size_t getLength() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 现在正确了:mutable 成员可以在 const 成员函数中被修改
lengthIsValid = true;
}
return textLength;
}
private:
char* pText;
mutable std::size_t textLength; // 使用 mutable
mutable bool lengthIsValid; // 使用 mutable
};
mutable
将成员变量从 bitwise constness 的约束中释放出来。任何被声明为 mutable
的成员变量都可以在 const
成员函数中被修改。
避免 const 和非 const 成员函数的代码重复
(Effective C++ 条款 3) 也提到了一个常见问题:const
和 非 const
版本的 operator[]
几乎做同样的事情,代码重复。
解决方案:使用 转型(cast)。让非 const
版本调用 const
版本。
class TextBlock {
public:
const char& operator[](std::size_t position) const { // 一如既往
// ... 边界检查、日志记录等 ...
return text[position];
}
char& operator[](std::size_t position) {
// 使用 static_cast 将 *this 转为 const TextBlock&,以调用 const 版本
// 然后使用 const_cast 移除返回值的 const 属性
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]
);
}
};
这段代码的步骤是:
static_cast<const TextBlock&>(*this)
:将当前对象(非 const)转型为常量引用。- 调用
const operator[]
,它返回一个const char&
。 const_cast<char&>(...)
:移除返回结果的const
属性,使其与函数的返回类型char&
匹配。
注意:反向操作(在 const 版本中调用非 const 版本)是错误且危险的,因为这违背了 const 成员函数不修改对象的承诺。
4. constexpr:编译期的 const
C++11 引入了 constexpr
,它表示“常量表达式”(constant expression)。它的核心目标是让计算在编译期发生,而不是运行期。
const
:“只读”。这个值在运行时初始化后不可修改。constexpr
:“常量”。这个值必须在编译期就是可知的。
const int size = 100; // 运行时常量(也可能被优化为编译期常量)
constexpr int compileTimeSize = 100; // 绝对是编译时常量
std::array<int, size> arr1; // 可能可行,取决于编译器
std::array<int, compileTimeSize> arr2; // 绝对可行
// constexpr 函数:如果传入编译期常量,它将在编译期计算出结果
constexpr int square(int x) { return x * x; }
int array[square(5)]; // 创建一个大小为 25 的数组,计算在编译期完成
int runtime_val = 10;
int array2[square(runtime_val)]; // 错误!runtime_val 不是编译期常量,square无法在编译期计算。
最佳实践:对于所有需要在编译期确定值的场合(如数组大小、模板参数、case 标签等),使用 constexpr
。对于只需要运行时不改变的值,使用 const
。
总结与最佳实践清单
- 默认 const:对于变量、指针和引用,如果它们不应被修改,优先使用
const
。 - 使用 const & 参数:对于非内置类型的输入参数,使用
const T&
或const T*
来避免拷贝并保证不修改源对象。 - const 成员函数是承诺:将不修改对象 observable state 的成员函数声明为
const
。这使你的类可以与const
对象一起工作。 - 理解 Bitwise vs Logical:使用
mutable
成员变量来实现逻辑常量性,处理内部缓存、计数器等。 - 避免重复:通过让非
const
成员函数调用其const
版本来避免代码重复(使用转型)。 - 区分 const 和 constexpr:需要编译期常量时用
constexpr
,只需要运行时只读时用const
。 - 它是最好的文档:严格遵循常量正确性,你的代码意图会清晰得多,编译器也会帮你抓住许多错误。
坚持这些实践,你将能写出更安全、更清晰、更高效的 C++ 代码。常量正确性不是枷锁,而是守护你代码逻辑的利器。
- 点赞
- 收藏
- 关注作者
评论(0)