Effective C++ 条款03:尽可能使用 const

Effective C++ 条款03:尽可能使用 const

const 允许你告诉编译器和其他程序员,某个值应该保持不变。只要可能,就应该使用它。

开篇引言

const 是 C++ 中最强大的关键字之一,也是最容易被误用的关键字之一。很多开发者知道 const 表示"常量",但对其深层语义和实际威力却一知半解。

Scott Meyers 在《Effective C++》第三条强调:只要可能就用 const。 这不是一条可有可无的风格建议,而是关乎代码正确性、安全性和可维护性的核心准则。

const 的基本语义

const 的核心含义是:一个特定的对象不应该被修改,而编译器将强制执行这一约束。

const 与指针的三种组合

这是 C++ 中最容易混淆的部分,必须彻底理解:

cpp 复制代码
const char* p = greeting;       // 常量数据,非常量指针
char* const p = greeting;       // 非常量数据,常量指针
const char* const p = greeting; // 常量数据,常量指针

记忆技巧: 如果 const* 左侧,表示数据是常量 ;如果在右侧,表示指针是常量

声明 可读性写法 含义
const char* p char const* p 指向常量的指针(指针可变,内容不可变)
char* const p --- 常量指针(指针不可变,内容可变)
const char* const p char const* const p 常量指针指向常量(都不可变)

代码示例:

cpp 复制代码
#include <iostream>

void pointer_const_demo() {
    char str1[] = "Hello";
    char str2[] = "World";
    
    // 1. 指向常量的指针
    const char* p1 = str1;
    // p1[0] = 'h';     // 编译错误!不能修改指向的内容
    p1 = str2;          // 可以,指针本身不是常量
    
    // 2. 常量指针
    char* const p2 = str1;
    p2[0] = 'h';        // 可以,内容不是常量
    // p2 = str2;       // 编译错误!不能修改指针本身
    
    // 3. 常量指针指向常量
    const char* const p3 = str1;
    // p3[0] = 'h';     // 编译错误!
    // p3 = str2;       // 编译错误!
}

STL 迭代器中的 const

STL 迭代器的行为类似于指针,理解 const 对迭代器的应用至关重要:

cpp 复制代码
#include <vector>
#include <iostream>

void iterator_const_demo() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 非常量迭代器:可以修改内容
    std::vector<int>::iterator it = vec.begin();
    *it = 10;  // 可以修改
    ++it;      // 可以移动
    
    // const_iterator:不能修改内容,但可以移动
    std::vector<int>::const_iterator cit = vec.begin();
    // *cit = 10;  // 编译错误!
    ++cit;        // 可以移动
    
    // C++11 推荐:使用 auto 和 cbegin/cend
    for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
        std::cout << *it << " ";  // 只读访问
    }
}

一些 STL 实现中,const iterator(注意不是 const_iterator)表示迭代器本身不能移动,但指向的内容可以修改。这种用法较少见,了解即可。

const 与函数声明

函数返回值使用 const

cpp 复制代码
class Rational {
public:
    Rational(int n = 0, int d = 1) : numerator(n), denominator(d) {}
    
    // 返回 const 值,防止无意义的赋值操作
    const Rational operator*(const Rational& rhs) const {
        return Rational(numerator * rhs.numerator, 
                       denominator * rhs.denominator);
    }
    
private:
    int numerator;
    int denominator;
};

// 使用
Rational a(1, 2), b(3, 4);
Rational c = a * b;      // 正常
// (a * b) = c;          // 编译错误!防止这种无意义的操作

返回 const 值在自定义类型中很有用,可以防止像 (a * b) = c 这样荒谬的代码通过编译。

const 成员函数

这是 const 最重要的应用之一。const 成员函数承诺不修改对象的状态

cpp 复制代码
class TextBlock {
public:
    TextBlock(const std::string& str) : text(str) {}
    
    // 非常量版本
    char& operator[](std::size_t position) {
        return text[position];
    }
    
    // 常量版本
    const char& operator[](std::size_t position) const {
        return text[position];
    }
    
    std::size_t length() const;  // 承诺不修改对象
    
private:
    std::string text;
    mutable std::size_t textLength;  // 即使 const 函数中也可修改
    mutable bool lengthIsValid;
};

// 使用
TextBlock tb("Hello");
const TextBlock ctb("World");

tb[0] = 'h';       // 调用非常量版本
// ctb[0] = 'w';   // 编译错误!const 对象只能调用 const 成员函数
char c = ctb[0];   // 调用常量版本,只读访问

为什么需要 mutable?

有时需要在 const 成员函数中修改某些成员(如缓存),但这些修改在逻辑上不影响对象的外部可见状态:

cpp 复制代码
std::size_t TextBlock::length() const {
    if (!lengthIsValid) {
        textLength = text.length();  // 在 const 函数中修改!
        lengthIsValid = true;
    }
    return textLength;
}

mutable 打破了物理常量性(physical constness),但保持了逻辑常量性(logical constness)。

const 的深层威力

1. 使代码可被 const 对象使用

这是 const 成员函数最重要的实际意义:

cpp 复制代码
void print(const TextBlock& ctb) {
    std::cout << ctb[0];  // 如果 operator[] 不是 const,这里编译失败!
}

// 传递 by const reference 是 C++ 高效编程的核心技巧
void processLargeObject(const BigObject& obj);  // 高效且安全

2. 函数重载与 const 正确性

cpp 复制代码
class String {
public:
    // 非常量版本:可能返回可修改的引用
    char& operator[](std::size_t index) {
        // 可以添加写时复制(Copy-on-Write)逻辑
        return data[index];
    }
    
    // 常量版本:保证不修改
    const char& operator[](std::size_t index) const {
        return data[index];
    }
    
private:
    std::string data;
};

// 实现技巧:非常量版本调用常量版本,避免代码重复
char& String::operator[](std::size_t index) {
    return const_cast<char&>(
        static_cast<const String&>(*this)[index]
    );
}

注意:反向操作(const 版本调用非 const 版本)是不安全的,因为它承诺了不修改却可能修改。

3. 编译器优化

const 允许编译器进行更多优化:

cpp 复制代码
const int size = 100;
int arr[size];  // 编译器知道 size 是常量,可以优化内存布局

void process(const int* data, int n) {
    // 编译器知道 data 指向的内容不会被修改
    // 可以进行更激进的缓存和指令重排优化
    for (int i = 0; i < n; ++i) {
        use(data[i]);
    }
}

实际应用场景

场景 1:接口设计中的 const 正确性

cpp 复制代码
class DatabaseConnection {
public:
    // 查询操作:应该是 const
    QueryResult executeQuery(const std::string& sql) const;
    
    // 修改操作:不应该是 const
    void beginTransaction();
    void commit();
    void rollback();
    
    // 获取连接信息:const
    std::string getConnectionString() const;
    bool isConnected() const;
};

// 使用
void analyzeDatabase(const DatabaseConnection& db) {
    // 编译器保证这里不会修改数据库状态
    auto result = db.executeQuery("SELECT * FROM users");
    // db.beginTransaction();  // 编译错误!安全!
}

场景 2:线程安全与 const

cpp 复制代码
#include <mutex>
#include <vector>

class ThreadSafeData {
public:
    // 读取操作:const + 锁定
    int getValue(int index) const {
        std::lock_guard<std::mutex> lock(mutex_);  // mutable mutex
        return data_[index];
    }
    
    // 写入操作:非 const
    void setValue(int index, int value) {
        std::lock_guard<std::mutex> lock(mutex_);
        data_[index] = value;
    }
    
private:
    std::vector<int> data_;
    mutable std::mutex mutex_;  // mutable 允许在 const 函数中锁定
};

场景 3:智能指针与 const

cpp 复制代码
#include <memory>

void smart_ptr_const_demo() {
    auto ptr = std::make_shared<int>(42);
    
    // const shared_ptr:指针本身不能变,指向的内容可以变
    const std::shared_ptr<int> cptr = ptr;
    *cptr = 100;        // 可以
    // cptr.reset();    // 编译错误!
    
    // shared_ptr to const:指针可变,内容不可变
    std::shared_ptr<const int> ptc = std::make_shared<const int>(42);
    // *ptc = 100;      // 编译错误!
    ptc.reset();        // 可以
    
    // const shared_ptr to const:都不可变
    const std::shared_ptr<const int> cptc = std::make_shared<const int>(42);
    // *cptc = 100;     // 编译错误!
    // cptc.reset();    // 编译错误!
}

const 正确性常见问题

问题 1:顶层 const 与底层 const

cpp 复制代码
int a = 10;
int* p1 = &a;
const int* p2 = p1;   // 可以:添加底层 const
// int* p3 = p2;      // 错误:不能移除底层 const

int* const p4 = p1;   // 顶层 const 不影响类型转换
const int* const p5 = p2;  // 可以

问题 2:const 与 constexpr

cpp 复制代码
const int size1 = getSize();     // 运行期常量(如果 getSize() 不是 constexpr)
constexpr int size2 = 100;       // 编译期常量

const int* ptr1 = nullptr;       // 指向常量的指针
constexpr int* ptr2 = nullptr;   // 常量指针(constexpr 在 * 右侧)

问题 3:const_cast 的正确使用

cpp 复制代码
// 安全的用法:为已知的非 const 对象添加 const,再移除
void legacy_api(char* data);  // 旧 API 没有 const

void safe_wrapper(const std::string& str) {
    // 我们知道 legacy_api 不会修改数据
    legacy_api(const_cast<char*>(str.c_str()));
}

// 危险的用法:修改真正的 const 对象
const int x = 10;
const_cast<int&>(x) = 20;  // 未定义行为!可能崩溃!

总结与建议

核心要点

  1. 将某些东西声明为 const 可以帮助编译器侦测出错误用法。 const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

  2. 编译器强制实施 bitwise constness,但你编写程序时应该使用 logical constness。 必要时使用 mutable

  3. 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本, 避免代码重复。

const 使用 checklist

场景 建议
全局/局部常量 使用 constconstexpr
函数参数(只读) 使用 const T&const T*
函数返回值(自定义类型) 考虑使用 const 防止无意义赋值
不修改成员的成员函数 必须声明为 const
缓存/计数器成员 使用 mutable
迭代器只读遍历 使用 const_iterator

经典名言

只要可能就用 const。

const 不仅是一种优化手段,更是一种设计工具。它帮助你在编译期就发现错误,明确表达设计意图,让代码更加自文档化。


参考阅读:

  • 《Effective C++》Scott Meyers,条款 03
  • 《C++ Primer》关于 constconstexpr 的章节
  • 《Effective Modern C++》关于 autodecltype 的条款

系列预告: 下一篇将深入解析条款 04------确定对象被使用前已先被初始化,探讨 C++ 初始化的复杂规则和高效率初始化技巧。


如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。

相关推荐
程序员佳佳1 小时前
我在 Windows 和低配 Linux 上做 RAG:Milvus、FAISS、向量 API 中转的中立实测
linux·人工智能·windows·gpt·aigc·milvus·faiss
光影6271 小时前
Python接口自动化测试----Requests库基础入门
开发语言·python·测试工具·pycharm·自动化
程序媛_1 小时前
【Python】连接PostgreSQL获取手机验证码
开发语言·python·postgresql
加成BUFF1 小时前
第六天 ROS 《Action 通信实验》
linux·机器人·ros
ShineWinsu1 小时前
对于Linux:进程信号的解析—下
linux·运维·服务器·面试·笔试·进程·信号
ch.ju1 小时前
Java Programming Chapter 4——Inherited call
java·开发语言
YIN_尹1 小时前
【Linux系统编程】基础IO第二讲——文件描述符
android·linux·服务器
信看1 小时前
Jetson Orin Quectel QMI 拨号上网
开发语言·python
小欣加油1 小时前
Leetcode31 下一个排列
数据结构·c++·算法·leetcode·职场和发展