const的正确姿势:从变量、函数到成员函数

举报
码事漫谈 发表于 2025/09/02 19:19:06 2025/09/02
【摘要】 常量正确性(Const Correctness) 是编写健壮、安全且易于理解的 C++ 代码的基石。它并非一个可选的特性,而是一种核心设计哲学。它通过类型系统向编译器和其他程序员传达你的设计意图:“这个对象或数据不应被修改”。正确地使用 const 可以避免意外的修改,使代码更安全;它可以作为文档,提高代码可读性;并且它能为编译器提供更多的优化机会。本文将深入探讨 const 在不同语境下的...

常量正确性(Const Correctness) 是编写健壮、安全且易于理解的 C++ 代码的基石。它并非一个可选的特性,而是一种核心设计哲学。它通过类型系统向编译器和其他程序员传达你的设计意图:“这个对象或数据不应被修改”。正确地使用 const 可以避免意外的修改,使代码更安全;它可以作为文档,提高代码可读性;并且它能为编译器提供更多的优化机会。

本文将深入探讨 const 在不同语境下的含义、细微差别和最佳实践。

1. 基础:const 变量和指针

让我们从最基本的用法开始。

const 变量

最简单的用法是定义一個常量。这意味着一旦初始化,其值便不可更改。

const int bufferSize = 1024; // 值不能修改
bufferSize = 2048; // 错误:表达式必须是可修改的左值

const std::string name = "Alice"; // 对象本身不能修改

最佳实践:默认情况下,将不应被修改的变量声明为 const。这可以防止你或你的同事在后面意外地修改它。

指针:const 的“绕口令”

指针涉及两个对象:指针本身和它所指向的数据。const 的位置决定了谁是不可变的。

  1. 指向 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
    
  2. const 指针(Const pointer)
    这表示指针本身是常量(必须初始化,且不能再指向其他地址),但数据可以修改。

    int* const const_ptr = &value; // const_ptr 是一个const指针,它指向一个 int
    
    *const_ptr = 40; // 正确:可以修改所指的值
    const_ptr = &const_value; // 错误:不能修改 const_ptr 本身的值(即地址)
    
  3. 指向 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 -> ptrconst (pointer)

2. 函数中的 const:参数与返回值

const 参数

将函数参数声明为指向 constconst 引用,是常量正确性的最重要应用之一。

  • 值传递(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("!");            // 正确

重要规则

  1. const 对象只能调用 const 成员函数。
  2. 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 的,因为它不向用户提供修改后的文本内容。但它需要修改缓存变量 textLengthlengthIsValid,这违反了 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]
        );
    }
};

这段代码的步骤是:

  1. static_cast<const TextBlock&>(*this):将当前对象(非 const)转型为常量引用。
  2. 调用 const operator[],它返回一个 const char&
  3. 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


总结与最佳实践清单

  1. 默认 const:对于变量、指针和引用,如果它们不应被修改,优先使用 const
  2. 使用 const & 参数:对于非内置类型的输入参数,使用 const T&const T* 来避免拷贝并保证不修改源对象。
  3. const 成员函数是承诺:将不修改对象 observable state 的成员函数声明为 const。这使你的类可以与 const 对象一起工作。
  4. 理解 Bitwise vs Logical:使用 mutable 成员变量来实现逻辑常量性,处理内部缓存、计数器等。
  5. 避免重复:通过让非 const 成员函数调用其 const 版本来避免代码重复(使用转型)。
  6. 区分 const 和 constexpr:需要编译期常量时用 constexpr,只需要运行时只读时用 const
  7. 它是最好的文档:严格遵循常量正确性,你的代码意图会清晰得多,编译器也会帮你抓住许多错误。

坚持这些实践,你将能写出更安全、更清晰、更高效的 C++ 代码。常量正确性不是枷锁,而是守护你代码逻辑的利器。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。