Effective C++ 条款22:将成员变量声明为 private Effective C 条款22将成员变量声明为 private切记将成员变量声明为 private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证并提供 class 作者以充分的实现弹性。一、引言封装是面向对象的基石在 C 类设计中有一个看似简单却至关重要的原则将所有成员变量声明为 private。这不是风格偏好而是专业素养的分水岭。很多初学者会问“public 不是更方便吗直接访问省去了 getter/setter 的麻烦。” 答案是今天的便利会成为明天的枷锁。二、为什么 public 数据成员是危险的2.1 语法一致性的破坏// ❌ 糟糕的设计public 数据成员classBadTemperatureSensor{public:doublecurrentTemperature;// 直接暴露doubleminTemperature;doublemaxTemperature;};// 使用方式混乱BadTemperatureSensor sensor;sensor.currentTemperature36.5;// 直接赋值// 客户需要记住哪些是变量、哪些是函数当类中既有 public 数据成员又有 public 成员函数时客户必须记住访问数据用.不加括号访问函数用.加括号。这种不一致性增加了使用负担和出错概率。2.2 实现细节被锁定// ❌ public 成员锁定了实现classBadStudentRecords{public:Student students_[1000];// 固定数组publicsize_t count0;};// 一旦要改为 vector所有客户端代码都要修改一旦数据成员是 public任何内部实现的变更都会影响到所有客户端代码。这意味着你失去了优化的自由、重构的自由、演进的自由。三、private 带来的三大核心价值3.1 访问控制的一致性// ✅ 优秀的设计行为导向接口classGoodTemperatureSensor{public:doublereadTemperature()const{if(!isCalibrated_){throwstd::logic_error(传感器未校准);}returncurrentTemperature_;}boolisInSafeRange()const{returncurrentTemperature_minTemperature_currentTemperature_maxTemperature_;}voidcalibrate(){// 复杂的校准逻辑...isCalibrated_true;}private:doublecurrentTemperature_;doubleminTemperature_;doublemaxTemperature_;boolisCalibrated_false;};客户唯一需要记住的是所有交互都通过成员函数完成。这种一致性大大降低了心智负担。3.2 精确的读写控制通过 private 成员函数你可以实现细粒度的访问控制控制级别实现方式应用场景只读访问constgetter计算属性、状态查询只写访问setter无 getter密码、密钥等敏感数据读写验证getter setter需要校验的业务数据内部计算无直接访问缓存、延迟计算属性classBankAccount{public:// 只读访问——余额不能被直接修改doublegetBalance()const{std::lock_guardstd::mutexlock(mutex_);returnbalance_;}// 受控写入——业务规则校验voiddeposit(doubleamount){if(amount0){throwstd::invalid_argument(存款金额必须为正);}std::lock_guardstd::mutexlock(mutex_);balance_amount;logTransaction(存款,amount);}// 条件操作——封装业务逻辑boolwithdraw(doubleamount){if(amount0){throwstd::invalid_argument(取款金额必须为正);}std::lock_guardstd::mutexlock(mutex_);if(balance_amount){balance_-amount;logTransaction(取款,amount);returntrue;}returnfalse;// 余额不足}// 计算属性——不存储实时计算boolisOverdrawn()const{returngetBalance()0;}private:doublebalance_0.0;mutablestd::mutex mutex_;// mutable 允许在 const 函数中锁定voidlogTransaction(conststd::stringtype,doubleamount);};3.3 不变式的维护classRectangle{public:voidsetWidth(doublew){if(w0)throwstd::invalid_argument(宽度必须为正);width_w;updateArea();// 维护不变式area width * height}voidsetHeight(doubleh){if(h0)throwstd::invalid_argument(高度必须为正);height_h;updateArea();}doublegetArea()const{returnarea_;}private:doublewidth_;doubleheight_;doublearea_;// 缓存的面积值必须始终保持一致voidupdateArea(){area_width_*height_;}};不变式Invariant类在任何时候都必须满足的条件。通过 private 成员 受控接口你可以在每次修改时验证并维护这些不变式。四、protected 并不比 public 好多少很多开发者认为protected是一个折中方案——比 public 安全又比 private 灵活。但 Scott Meyers 明确指出protected 成员几乎和 public 一样缺乏封装性。// ❌ protected 的封装幻觉classBase{protected:intinternalData_;// 以为比 public 好std::vectorintimplementationDetails_;};classDerived:publicBase{public:voidmessUp(){internalData_-999;// 任意修改破坏不变式implementationDetails_.clear();// 破坏基类假设}};// 问题一旦修改 Base 的 protected 成员所有派生类都可能需要修改// 封装性实际上和 public 差不多差设计原则如果派生类确实需要访问基类的某些数据应该通过protected 的成员函数提供受控访问而非直接暴露数据成员。// ✅ 更好的设计classWellDesignedBase{public:virtual~WellDesignedBase()default;protected:// 为派生类提供受控的扩展点virtualvoiddoProcess(intdata){implementationDetail_data;}// 只读访问conststd::vectorintgetInternalData()const{returninternalData_;}private:intimplementationDetail_;std::vectorintinternalData_;};五、实际应用场景5.1 延迟初始化与缓存classDocumentProcessor{public:voidsetContent(conststd::stringcontent){content_content;invalidateCaches();// 状态变化时清理缓存}// 计算属性看起来像数据访问实则是计算constWordCountgetWordCount()const{if(!wordCountCache_){wordCountCache_std::make_uniqueWordCount(analyzeWords());}return*wordCountCache_;}private:std::string content_;mutablestd::unique_ptrWordCountwordCountCache_;// mutable 允许延迟初始化mutableboolisAnalyzed_false;voidinvalidateCaches(){wordCountCache_.reset();isAnalyzed_false;}WordCountanalyzeWords()const{// 昂贵的分析操作returnWordCount(/* ... */);}};5.2 Pimpl 惯用法极致封装// 头文件接口完全稳定classWellEncapsulatedClass{public:WellEncapsulatedClass(conststd::stringname);~WellEncapsulatedClass();voidperformCalculation(doubleparameter);doublegetAverage()const;boolisValid()const;private:classImpl;// 前向声明std::unique_ptrImplpImpl_;// 实现完全隐藏};// 实现文件Impl 定义在这里客户端完全不可见classWellEncapsulatedClass::Impl{public:std::string name_;std::vectordoublemeasurements_;// ... 任何实现细节变更都不影响头文件};PimplPointer to Implementation是 private 封装思想的极致体现客户端甚至看不到类的成员变量有哪些5.3 线程安全封装classThreadSafeCounter{public:voidincrement(){std::lock_guardstd::mutexlock(mutex_);count_;}intget()const{std::lock_guardstd::mutexlock(mutex_);returncount_;}// 原子性的复合操作intfetchAndAdd(intvalue){std::lock_guardstd::mutexlock(mutex_);intoldcount_;count_value;returnold;}private:intcount_0;mutablestd::mutex mutex_;// mutable 允许 const 方法加锁};六、常见误区误区真相“为每个字段写 getter/setter 就是封装”过度封装等于没有封装应该提供行为接口而非数据接口“protected 比 public 安全”protected 的封装性几乎和 public 一样差“struct 默认 public所以用 struct 更方便”struct 更适合纯数据聚合PODclass 更适合封装对象“内联 getter 有性能优势所以应该暴露数据”编译器可以内联访问函数性能与直接访问相同七、总结核心原则所有数据成员都应该是 private——无一例外提供行为接口而非数据接口——表达做什么而非是什么protected 并不比 public 好多少——需要访问时用 protected 成员函数封装的价值在于未来的自由——你可以随时改变实现而不影响客户端设计检查清单classWellDesignedClass{public:// 稳定的公有接口voidperformAction();StategetState()const;boolisValid()const;// 计算属性doublegetDerivedValue()const;protected:// 为派生类提供的受控扩展点virtualvoidonStateChanged();private:// 所有数据成员都是 privatestd::string name_;std::vectordoubledata_;std::unique_ptrImplementationpImpl_;mutablestd::mutex mutex_;};记住封装的价值不在于今天能做什么而在于明天能改变什么。将成员变量声明为 private是面向未来软件设计的第一步。参考与延伸阅读《Effective C》第三版Scott Meyers条款22《C Primer》第五版关于类访问控制的章节Sutter’s Mill: GotW #100: Compilation Firewalls如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、留言 你的支持是我持续输出的动力