
常量正确性(Const Correctness) 是编写健壮、安全且易于理解的 C++ 代码的基石。它并非一个可选的特性,而是一种核心设计哲学。它通过类型系统向编译器和其他程序员传达你的设计意图:“这个对象或数据不应被修改”。正确地使用 const 可以避免意外的修改,使代码更安全;它可以作为文档,提高代码可读性;并且它能为编译器提供更多的优化机会。
本文将深入探讨 const 在不同语境下的含义、细微差别和最佳实践。
让我们从最基本的用法开始。
最简单的用法是定义一個常量。这意味着一旦初始化,其值便不可更改。
const int bufferSize = 1024; // 值不能修改
bufferSize = 2048; // 错误:表达式必须是可修改的左值
const std::string name = "Alice"; // 对象本身不能修改最佳实践:默认情况下,将不应被修改的变量声明为 const。这可以防止你或你的同事在后面意外地修改它。
指针涉及两个对象:指针本身和它所指向的数据。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 int* const const_ptr = &value; // const_ptr 是一个const指针,它指向一个 int
*const_ptr = 40; // 正确:可以修改所指的值
const_ptr = &const_value; // 错误:不能修改 const_ptr 本身的值(即地址) const int* const const_ptr_to_const = &const_value;
*const_ptr_to_const = 50; // 错误
const_ptr_to_const = &value; // 错误记忆口诀:const 修饰它左边的东西。如果左边没东西,就修饰右边的东西。
const int* -> (*ptr) 是 const intint const* -> (*ptr) 是 const intint* const -> ptr 是 const (pointer)将函数参数声明为指向 const 或 const 引用,是常量正确性的最重要应用之一。
void func(int x)x 不影响外部。加 const 意义不大(仅限于函数内部实现),通常省略。void func(const BigObject& obj), void func(const BigObject* obj)const &(或 const *)可以避免昂贵的拷贝,同时向调用者保证:“我绝不会修改你传入的对象”。这使函数可以接受常量和非常量实参,更加通用。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 值通常用于自定义类型,以防止返回值被意外修改,但这种用法比较少见,且有争议。
class Fraction { ... };
const Fraction operator*(const Fraction& lhs, const Fraction& rhs);
Fraction a, b, c;
...
(a * b) = c; // 如果没有顶层的const,这句代码是合法的但无意义的!
// 有了const返回值,这行代码会报错,阻止了这种无意义的操作。对于内置类型,返回 const 值没有意义。现代 C++ 中,这种用法已不常见。
将成员函数声明为 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 版本,如果存在重载)。这是一个关键区别,(Effective C++ 条款 3) 对此有精彩论述。
const 成员函数只有在不修改对象的任何非静态成员变量(bit)时,才是 bitwise const 的。const 成员函数可能会修改一些比特,但只要这些修改对用户(调用者)是不可见的(不影响对象的可观测状态),它就是逻辑上 const 的。例子:
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 成员函数中被修改。
(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 成员函数不修改对象的承诺。
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 T& 或 const T* 来避免拷贝并保证不修改源对象。const。这使你的类可以与 const 对象一起工作。mutable 成员变量来实现逻辑常量性,处理内部缓存、计数器等。const 成员函数调用其 const 版本来避免代码重复(使用转型)。constexpr,只需要运行时只读时用 const。坚持这些实践,你将能写出更安全、更清晰、更高效的 C++ 代码。常量正确性不是枷锁,而是守护你代码逻辑的利器。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。