Effective C++ 条款22:将成员变量声明为 private
切记将成员变量声明为 private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
一、引言:封装是面向对象的基石
在 C++ 类设计中,有一个看似简单却至关重要的原则:将所有成员变量声明为 private。这不是风格偏好,而是专业素养的分水岭。
很多初学者会问:"public 不是更方便吗?直接访问省去了 getter/setter 的麻烦。" 答案是:今天的便利,会成为明天的枷锁。
二、为什么 public 数据成员是危险的?
2.1 语法一致性的破坏
cpp
// ❌ 糟糕的设计:public 数据成员
class BadTemperatureSensor {
public:
double currentTemperature; // 直接暴露
double minTemperature;
double maxTemperature;
};
// 使用方式混乱
BadTemperatureSensor sensor;
sensor.currentTemperature = 36.5; // 直接赋值
// 客户需要记住哪些是变量、哪些是函数
当类中既有 public 数据成员,又有 public 成员函数时,客户必须记住:访问数据用 . 不加括号,访问函数用 . 加括号。这种不一致性增加了使用负担和出错概率。
2.2 实现细节被锁定
cpp
// ❌ public 成员锁定了实现
class BadStudentRecords {
public:
Student students_[1000]; // 固定数组,public!
size_t count = 0;
};
// 一旦要改为 vector,所有客户端代码都要修改!
一旦数据成员是 public,任何内部实现的变更都会影响到所有客户端代码。这意味着你失去了优化的自由、重构的自由、演进的自由。
三、private 带来的三大核心价值
3.1 访问控制的一致性
cpp
// ✅ 优秀的设计:行为导向接口
class GoodTemperatureSensor {
public:
double readTemperature() const {
if (!isCalibrated_) {
throw std::logic_error("传感器未校准");
}
return currentTemperature_;
}
bool isInSafeRange() const {
return currentTemperature_ >= minTemperature_
&& currentTemperature_ <= maxTemperature_;
}
void calibrate() {
// 复杂的校准逻辑...
isCalibrated_ = true;
}
private:
double currentTemperature_;
double minTemperature_;
double maxTemperature_;
bool isCalibrated_ = false;
};
客户唯一需要记住的是:所有交互都通过成员函数完成。 这种一致性大大降低了心智负担。
3.2 精确的读写控制
通过 private + 成员函数,你可以实现细粒度的访问控制:
| 控制级别 | 实现方式 | 应用场景 |
|---|---|---|
| 只读访问 | const getter |
计算属性、状态查询 |
| 只写访问 | setter(无 getter) | 密码、密钥等敏感数据 |
| 读写验证 | getter + setter | 需要校验的业务数据 |
| 内部计算 | 无直接访问 | 缓存、延迟计算属性 |
cpp
class BankAccount {
public:
// 只读访问------余额不能被直接修改
double getBalance() const {
std::lock_guard<std::mutex> lock(mutex_);
return balance_;
}
// 受控写入------业务规则校验
void deposit(double amount) {
if (amount <= 0) {
throw std::invalid_argument("存款金额必须为正");
}
std::lock_guard<std::mutex> lock(mutex_);
balance_ += amount;
logTransaction("存款", amount);
}
// 条件操作------封装业务逻辑
bool withdraw(double amount) {
if (amount <= 0) {
throw std::invalid_argument("取款金额必须为正");
}
std::lock_guard<std::mutex> lock(mutex_);
if (balance_ >= amount) {
balance_ -= amount;
logTransaction("取款", amount);
return true;
}
return false; // 余额不足
}
// 计算属性------不存储,实时计算
bool isOverdrawn() const {
return getBalance() < 0;
}
private:
double balance_ = 0.0;
mutable std::mutex mutex_; // mutable 允许在 const 函数中锁定
void logTransaction(const std::string& type, double amount);
};
3.3 不变式的维护
cpp
class Rectangle {
public:
void setWidth(double w) {
if (w <= 0) throw std::invalid_argument("宽度必须为正");
width_ = w;
updateArea(); // 维护不变式:area = width * height
}
void setHeight(double h) {
if (h <= 0) throw std::invalid_argument("高度必须为正");
height_ = h;
updateArea();
}
double getArea() const { return area_; }
private:
double width_;
double height_;
double area_; // 缓存的面积值,必须始终保持一致
void updateArea() {
area_ = width_ * height_;
}
};
💡 不变式(Invariant):类在任何时候都必须满足的条件。通过 private 成员 + 受控接口,你可以在每次修改时验证并维护这些不变式。
四、protected 并不比 public 好多少
很多开发者认为 protected 是一个折中方案------比 public 安全,又比 private 灵活。但 Scott Meyers 明确指出:protected 成员几乎和 public 一样缺乏封装性。
cpp
// ❌ protected 的封装幻觉
class Base {
protected:
int internalData_; // 以为比 public 好?
std::vector<int> implementationDetails_;
};
class Derived : public Base {
public:
void messUp() {
internalData_ = -999; // 任意修改,破坏不变式
implementationDetails_.clear(); // 破坏基类假设
}
};
// 问题:一旦修改 Base 的 protected 成员,所有派生类都可能需要修改!
// 封装性实际上和 public 差不多差
设计原则 :如果派生类确实需要访问基类的某些数据,应该通过 protected 的成员函数 提供受控访问,而非直接暴露数据成员。
cpp
// ✅ 更好的设计
class WellDesignedBase {
public:
virtual ~WellDesignedBase() = default;
protected:
// 为派生类提供受控的扩展点
virtual void doProcess(int data) {
implementationDetail_ = data;
}
// 只读访问
const std::vector<int>& getInternalData() const {
return internalData_;
}
private:
int implementationDetail_;
std::vector<int> internalData_;
};
五、实际应用场景
5.1 延迟初始化与缓存
cpp
class DocumentProcessor {
public:
void setContent(const std::string& content) {
content_ = content;
invalidateCaches(); // 状态变化时清理缓存
}
// 计算属性:看起来像数据访问,实则是计算
const WordCount& getWordCount() const {
if (!wordCountCache_) {
wordCountCache_ = std::make_unique<WordCount>(analyzeWords());
}
return *wordCountCache_;
}
private:
std::string content_;
mutable std::unique_ptr<WordCount> wordCountCache_; // mutable 允许延迟初始化
mutable bool isAnalyzed_ = false;
void invalidateCaches() {
wordCountCache_.reset();
isAnalyzed_ = false;
}
WordCount analyzeWords() const {
// 昂贵的分析操作
return WordCount(/* ... */);
}
};
5.2 Pimpl 惯用法:极致封装
cpp
// 头文件:接口完全稳定
class WellEncapsulatedClass {
public:
WellEncapsulatedClass(const std::string& name);
~WellEncapsulatedClass();
void performCalculation(double parameter);
double getAverage() const;
bool isValid() const;
private:
class Impl; // 前向声明
std::unique_ptr<Impl> pImpl_; // 实现完全隐藏
};
// 实现文件:Impl 定义在这里,客户端完全不可见
class WellEncapsulatedClass::Impl {
public:
std::string name_;
std::vector<double> measurements_;
// ... 任何实现细节变更都不影响头文件
};
Pimpl(Pointer to Implementation)是 private 封装思想的极致体现:客户端甚至看不到类的成员变量有哪些!
5.3 线程安全封装
cpp
class ThreadSafeCounter {
public:
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
}
int get() const {
std::lock_guard<std::mutex> lock(mutex_);
return count_;
}
// 原子性的复合操作
int fetchAndAdd(int value) {
std::lock_guard<std::mutex> lock(mutex_);
int old = count_;
count_ += value;
return old;
}
private:
int count_ = 0;
mutable std::mutex mutex_; // mutable 允许 const 方法加锁
};
六、常见误区
| 误区 | 真相 |
|---|---|
| "为每个字段写 getter/setter 就是封装" | 过度封装等于没有封装,应该提供行为接口而非数据接口 |
| "protected 比 public 安全" | protected 的封装性几乎和 public 一样差 |
| "struct 默认 public,所以用 struct 更方便" | struct 更适合纯数据聚合(POD),class 更适合封装对象 |
| "内联 getter 有性能优势,所以应该暴露数据" | 编译器可以内联访问函数,性能与直接访问相同 |
七、总结
核心原则
- 所有数据成员都应该是 private------无一例外
- 提供行为接口,而非数据接口------表达"做什么"而非"是什么"
- protected 并不比 public 好多少------需要访问时用 protected 成员函数
- 封装的价值在于未来的自由------你可以随时改变实现而不影响客户端
设计检查清单
cpp
class WellDesignedClass {
public:
// 稳定的公有接口
void performAction();
State getState() const;
bool isValid() const;
// 计算属性
double getDerivedValue() const;
protected:
// 为派生类提供的受控扩展点
virtual void onStateChanged();
private:
// 所有数据成员都是 private
std::string name_;
std::vector<double> data_;
std::unique_ptr<Implementation> pImpl_;
mutable std::mutex mutex_;
};
📌 记住:封装的价值不在于今天能做什么,而在于明天能改变什么。将成员变量声明为 private,是面向未来软件设计的第一步。
参考与延伸阅读
- 《Effective C++》第三版,Scott Meyers,条款22
- 《C++ Primer》第五版,关于类访问控制的章节
- Sutter's Mill: GotW #100: Compilation Firewalls
如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、留言 💬!你的支持是我持续输出的动力!