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; // 未定义行为!可能崩溃!
总结与建议
核心要点
-
将某些东西声明为 const 可以帮助编译器侦测出错误用法。
const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。 -
编译器强制实施 bitwise constness,但你编写程序时应该使用 logical constness。 必要时使用
mutable。 -
当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本, 避免代码重复。
const 使用 checklist
| 场景 | 建议 |
|---|---|
| 全局/局部常量 | 使用 const 或 constexpr |
| 函数参数(只读) | 使用 const T& 或 const T* |
| 函数返回值(自定义类型) | 考虑使用 const 防止无意义赋值 |
| 不修改成员的成员函数 | 必须声明为 const |
| 缓存/计数器成员 | 使用 mutable |
| 迭代器只读遍历 | 使用 const_iterator |
经典名言
只要可能就用 const。
const 不仅是一种优化手段,更是一种设计工具。它帮助你在编译期就发现错误,明确表达设计意图,让代码更加自文档化。
参考阅读:
- 《Effective C++》Scott Meyers,条款 03
- 《C++ Primer》关于
const和constexpr的章节 - 《Effective Modern C++》关于
auto和decltype的条款
系列预告: 下一篇将深入解析条款 04------确定对象被使用前已先被初始化,探讨 C++ 初始化的复杂规则和高效率初始化技巧。
如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。