C++访问控制与继承:面向对象封装的三大防线与三种继承方式详解

📅 2026/8/26 12:08:16
C++访问控制与继承:面向对象封装的三大防线与三种继承方式详解
1. 项目概述为什么C的访问控制是面向对象的基石刚接触C面向对象编程时很多人对public、protected、private这三个访问权限以及public、protected、private这三种继承方式感到困惑。它们看起来像是一套复杂的规则记起来也容易混淆。但在我十多年的C开发经验里我深刻体会到这套看似繁琐的机制恰恰是C构建健壮、安全、易于维护的大型软件系统的核心保障。它不仅仅是语法更是一种设计哲学决定了你的类如何与外界交互如何保护内部状态以及如何构建清晰的类层次结构。简单来说访问权限定义了类成员的“可见性边界”而继承方式则决定了这个边界在派生类中如何被“重塑”。理解它们你就能从“写代码”进阶到“设计类”真正掌握面向对象封装和继承的精髓。无论是开发高性能游戏引擎、编写底层系统库还是构建复杂的业务应用清晰、恰当地使用访问控制和继承都是避免代码腐化、提升代码质量的关键。接下来我将结合大量实际案例和踩坑经验为你彻底讲透这六个概念让你不仅能记住规则更能理解其背后的设计意图和应用场景。2. 类的三种访问权限构筑封装的三道防线访问权限是类封装特性的直接体现。它规定了类的成员包括数据成员和成员函数在何处可以被访问。理解它们是理解C如何保护数据、隐藏实现细节的第一步。2.1 public对外的服务窗口public成员构成了类的接口Interface。它们是类对外公开的契约任何外部代码包括全局函数、其他类的成员函数、main函数等都可以直接访问。核心作用提供功能服务类的公有成员函数是外部代码与对象交互的唯一标准方式。例如一个File类的Open()、Read()、Close()方法。定义常量接口公有静态常量用于描述类的某些特性。例如std::vector::npos。极少数情况下的公有数据通常不推荐但在某些简单数据结构如struct Point {int x; int y;}或为了兼容C语言结构时使用。实操示例与心得class BankAccount { public: // 公有构造函数允许外部创建对象 BankAccount(const std::string owner, double initialBalance 0.0) : owner_(owner), balance_(initialBalance) { if (initialBalance 0) { throw std::invalid_argument(Initial balance cannot be negative.); } } // 公有成员函数提供存款服务 void deposit(double amount) { if (amount 0) { throw std::invalid_argument(Deposit amount must be positive.); } balance_ amount; logTransaction(Deposit, amount); } // 公有成员函数提供查询余额服务 double getBalance() const { return balance_; } // 公有成员函数提供查询户主服务 std::string getOwner() const { return owner_; } private: std::string owner_; double balance_; void logTransaction(const std::string type, double amount) { // 内部实现细节对外隐藏 std::cout [ owner_ ] type : amount std::endl; } }; int main() { BankAccount myAccount(Alice, 1000.0); // 调用公有构造函数 myAccount.deposit(500.0); // 调用公有成员函数 std::cout Balance: myAccount.getBalance() std::endl; // 访问公有接口 // myAccount.balance_ 1000000.0; // 错误balance_是私有成员无法直接访问 }注意设计公有接口时要遵循“最小权限原则”。只暴露必要的方法。像logTransaction这样的辅助函数应该设为private因为它属于内部实现逻辑外部调用它没有意义反而可能破坏对象的状态一致性。2.2 private内部的保险箱private成员是类的绝对隐私。只有该类自己的成员函数以及后面会提到的“友元”可以访问。派生类也无法直接访问基类的私有成员。核心作用隐藏实现细节将数据的存储方式、复杂的算法、辅助函数等封装起来。外部只需关心接口做了什么而不用关心怎么做。这降低了模块间的耦合度。保护数据完整性通过私有化数据成员并仅提供公有的getter获取和setter设置函数可以在函数中加入有效性检查、日志记录等逻辑确保对象始终处于有效状态。实现不变式类可以保证其私有数据成员之间始终满足某种关系不变式因为所有修改都必须通过受控的成员函数进行。实操示例与心得class TemperatureSensor { public: TemperatureSensor() : temperatureC_(0.0), isCalibrated_(false) {} // 通过公有接口读取温度可以加入校准状态检查 double getTemperature() const { if (!isCalibrated_) { throw std::runtime_error(Sensor not calibrated!); } return temperatureC_; } // 校准操作也通过接口进行更新内部状态 void calibrate() { // 模拟复杂的校准过程... performHardwareCalibration(); isCalibrated_ true; std::cout Sensor calibrated. std::endl; } private: double temperatureC_; // 原始温度数据私有 bool isCalibrated_; // 校准状态私有 // 一个模拟的底层硬件操作外部无需知晓 void performHardwareCalibration() { // 访问特定硬件寄存器等操作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); temperatureC_ readHardwareValue() * 0.01; // 假设的转换公式 } int readHardwareValue() { /* ... */ return 250; } // 模拟读取 };踩坑记录我曾在一个项目里图省事把一个本应私有的配置参数设成了public。结果在项目的另一个模块中同事直接修改了这个参数导致整个模块的行为出现难以追踪的异常。调试了大半天才发现是数据被意外篡改。自此之后我养成了习惯所有数据成员默认设为private除非有非常充分且明确的理由才考虑protected几乎从不使用public数据成员。这能极大减少因数据被意外修改而引发的Bug。2.3 protected家族的内部传承protected成员的访问权限介于public和private之间。它允许类自己的成员函数以及它的**派生类子类**的成员函数直接访问但对类的外部世界仍然是隐藏的。核心作用为继承设计当基类期望派生类能够直接使用或扩展其某些实现细节时将这些成员设为protected。这为派生类提供了比public更受控、比private更灵活的扩展能力。提供可覆盖的钩子方法模板方法设计模式中基类定义一个算法的骨架公有非虚函数其中某些步骤声明为protected虚函数由派生类去实现或覆盖。实操示例与心得class Shape { public: virtual ~Shape() default; // 公有接口计算面积。外部只关心这个。 double area() const { // 模板方法先验证再计算 validate(); return calculateArea(); // 调用受保护的纯虚函数 } virtual void draw() const 0; // 公有纯虚函数定义行为接口 protected: // 受保护的钩子方法供派生类实现具体的面积计算逻辑 virtual double calculateArea() const 0; // 受保护的辅助方法派生类可以复用 virtual void validate() const { // 基础验证例如检查尺寸是否为正数 // 派生类可以扩展此方法 } // 受保护的数据派生类可以直接访问用于面积计算 // 这里以“宽”“高”为例实际可能更复杂 double width_ 0.0; double height_ 0.0; }; class Rectangle : public Shape { public: Rectangle(double w, double h) { width_ w; // 可以直接访问基类的protected成员width_和height_ height_ h; } void draw() const override { /* 绘制矩形 */ } protected: // 实现基类定义的钩子方法 double calculateArea() const override { return width_ * height_; } // 可以覆盖基类的验证逻辑添加矩形特有的验证 void validate() const override { Shape::validate(); // 先调用基类的验证 if (width_ 0 || height_ 0) { throw std::invalid_argument(Rectangle width and height must be positive.); } } }; class Circle : public Shape { // Circle 可能用 radius_而不是 width_/height_所以它可能选择不直接使用基类的protected成员 // 而是通过其他方式实现calculateArea。这展示了protected提供的灵活性。 };重要辨析很多初学者容易滥用protected。记住protected破坏了封装性因为它向派生类暴露了实现细节。一旦基类的protected成员发生改变所有依赖它的派生类都可能需要修改。因此优先考虑使用private成员加上公有或受保护的访问函数getter/setter。仅当派生类确实需要以“实现继承”而非“接口继承”的方式工作且你明确希望耦合基类与派生类的实现时才使用protected数据成员。protected成员函数尤其是虚函数的使用场景则广泛得多常用于设计可扩展的框架。3. 三种继承方式重塑类的访问边界继承方式决定了基类父类的成员在派生类子类中的“默认访问权限”。它不改变基类成员在基类内部的访问属性而是改变这些成员在派生类作用域内的访问属性。我们可以把基类想象成一个有不同区域public区、protected区、private区的房子。继承方式就像是一副“眼镜”派生类通过这副眼镜去看基类的房子眼镜的颜色继承方式决定了看到的区域是明亮的可访问的还是昏暗的不可访问的。3.1 public继承建立“是一个is-a”关系public继承是最常用、最能体现面向对象“是一个”关系的继承方式。它表示派生类是基类的一种特化。规则基类的public成员在派生类中仍然是public。基类的protected成员在派生类中仍然是protected。基类的private成员在派生类中不可直接访问无论以何种方式继承private都不可直接访问只能通过基类的公有或受保护接口。设计意图派生类对象应该可以在任何需要基类对象的地方使用里氏替换原则。例如Rectanglepublic继承Shape那么任何期望Shape或Shape*的函数都可以安全地传入一个Rectangle对象。实操示例与心得class Vehicle { public: virtual void startEngine() { std::cout Vehicle engine starting... std::endl; engineRunning_ true; } virtual void stopEngine() { std::cout Vehicle engine stopping... std::endl; engineRunning_ false; } bool isEngineRunning() const { return engineRunning_; } protected: int fuelLevel_ 100; private: bool engineRunning_ false; }; class Car : public Vehicle { // public继承 public: // Car 继承了 Vehicle 的 startEngine, stopEngine, isEngineRunning // 这些成员在 Car 中依然是 public void drive() { if (isEngineRunning()) { // 可以访问基类的public成员 std::cout Car is driving. Fuel: fuelLevel_ % std::endl; // 可以访问基类的protected成员 fuelLevel_ - 10; } else { std::cout Start the engine first! std::endl; } } // 可以覆盖基类的虚函数 void startEngine() override { std::cout Car engine with keyless start... std::endl; Vehicle::startEngine(); // 调用基类实现 checkTirePressure(); // 启动前额外检查 } private: void checkTirePressure() { /* ... */ } }; void operateVehicle(Vehicle v) { // 接受基类引用 v.startEngine(); // v.drive(); // 错误Vehicle接口没有drive方法 } int main() { Car myCar; operateVehicle(myCar); // 正确Car is-a Vehicle可以向上转型 myCar.drive(); // 正确Car自己的public方法 Vehicle* vPtr myCar; // 多态指针 vPtr-startEngine(); // 动态绑定到Car::startEngine }核心原则public继承必须确保派生类对象“是一个”基类对象即派生类必须完全支持基类的接口契约。如果派生类需要废弃基类的某个接口比如Bird类继承Animal但Animal有个fly()方法不是所有鸟都能飞这就违反了“is-a”关系此时使用public继承是不合适的应考虑组合或其他设计模式。3.2 protected继承实现“以...实现”关系protected继承是一种不常见的继承方式它表示派生类“以...实现”但不是“是一个”的关系。它通常用于实现细节的复用而不是接口的继承。规则基类的public和protected成员在派生类中都变为protected。基类的private成员不可直接访问。设计意图将基类的公有和受保护接口“降级”为派生类的内部实现细节。外部代码无法通过派生类对象访问基类的任何接口因为它们都成了protected或private。这被称为“实现继承”而非“接口继承”。实操示例与心得// 一个通用的、功能丰富的堆栈实现 class StackImpl { public: void push(int value) { /* 复杂高效的入栈实现 */ } int pop() { /* 复杂高效的出栈实现 */ } bool isEmpty() const { /* ... */ } size_t size() const { /* ... */ } void clear() { /* ... */ } // ... 其他高级功能如迭代器 protected: // 一些内部状态和辅助函数 std::vectorint data_; }; // 我们只想暴露一个非常简单的、受限的堆栈接口 class RestrictedStack : protected StackImpl { // protected继承 public: // 只暴露两个最基本的方法 void push(int value) { // 可以在调用前加入限制逻辑 if (size() maxSize_) { // size()来自基类现在是protected派生类内部可用 throw std::overflow_error(Stack is full!); } StackImpl::push(value); // 复用基类实现 } int pop() { if (isEmpty()) { // isEmpty()来自基类现在是protected throw std::underflow_error(Stack is empty!); } return StackImpl::pop(); } private: static const size_t maxSize_ 100; }; int main() { RestrictedStack rs; rs.push(1); // rs.isEmpty(); // 错误isEmpty()在RestrictedStack中是protected外部无法访问 // rs.clear(); // 错误clear()在RestrictedStack中是protected外部无法访问 // StackImpl* siPtr rs; // 错误无法将RestrictedStack*隐式转换为StackImpl* // 因为继承关系是protected对外部不可见。 }使用场景与警告protected继承非常罕见。在绝大多数情况下使用组合在类中包含一个StackImpl类型的私有成员是比protected继承更好的选择。组合能提供更清晰的“has-a”关系更低的耦合度。只有在需要覆盖基类的虚函数同时又不想暴露基类接口时才可能考虑protected继承。新手应尽量避免使用。3.3 private继承另一种形式的“以...实现”private继承比protected继承更严格它也表示“以...实现”并且基类的所有非私有成员在派生类中都变成了private。规则基类的public和protected成员在派生类中都变为private。基类的private成员不可直接访问。设计意图与protected继承类似但封装得更彻底。基类的所有功能都完全变成了派生类的私有实现细节。从外部看派生类与基类没有任何关系。实操示例与心得class Timer { public: Timer() { reset(); } void reset() { start_ std::chrono::high_resolution_clock::now(); } double elapsed() const { auto end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble diff end - start_; return diff.count(); } }; // 一个需要计时的算法类它“使用”一个Timer但不想暴露这个事实 class ExpensiveAlgorithm : private Timer { // private继承 public: Result compute(const Input input) { reset(); // 调用基类的public方法现在在ExpensiveAlgorithm中是private Result r doCompute(input); // 实际计算 computationTime_ elapsed(); // 记录耗时elapsed()现在也是private logPerformance(); return r; } double getLastComputationTime() const { return computationTime_; } private: double computationTime_ 0.0; Result doCompute(const Input input) { /* 复杂算法 */ } void logPerformance() { std::cout Computation took computationTime_ seconds. std::endl; } // Timer的所有接口在这里都是private是纯粹的实现工具 }; int main() { ExpensiveAlgorithm algo; auto result algo.compute(someInput); std::cout Time: algo.getLastComputationTime() std::endl; // algo.reset(); // 错误reset()在ExpensiveAlgorithm中是private // Timer* t algo; // 错误无法进行private基类的向上转型 }private继承 vs 组合上面的ExpensiveAlgorithm同样可以用组合实现class ExpensiveAlgorithm { private: Timer timer_; };。那么如何选择选择private继承的情况需要重写基类的虚函数。需要访问基类的protected成员。基类没有数据成员只有方法空基类优化EBO使用私有继承可以节省空间这是高级优化技巧。默认选择组合在以上情况都不满足时优先使用组合。组合的语义“有一个”比私有继承“以...实现”更清晰耦合度更低代码也更容易理解。Scott Meyers在《Effective C》中明确指出尽可能使用组合必要时才使用私有继承。4. 访问权限与继承方式的组合影响深度解析理解了单独的规则后最关键的是理清它们组合起来的效果。这决定了派生类对象内部以及外部代码能访问到什么。4.1 访问检查的两重关卡当一个成员被访问时例如derivedObj.member编译器会进行两次检查在派生类内部检查该成员在派生类作用域内的访问权限这已经受继承方式影响。最终访问如果访问来自派生类外部如main函数则检查该成员在派生类中的访问权限。如果访问来自派生类内部则检查该成员在派生类中的访问权限对于继承来的成员这等价于检查其在基类中的原始权限但受到派生类内部访问规则的限制吗这里有个关键点。核心规则总结表基类中的访问权限public继承后protected继承后private继承后外部代码通过派生类对象访问派生类内部访问publicpublicprotectedprivate仅当继承后为public时可访问总是可访问因为它在派生类作用域内存在protectedprotectedprotectedprivate永远不可访问总是可访问private不可直接访问不可直接访问不可直接访问永远不可访问永远不可直接访问需通过基类接口重点解析“不可直接访问”指的是派生类的代码成员函数不能直接使用这个名字。例如基类有一个private的secretData_派生类函数里写secretData_ 10;是编译错误。但派生类可以通过调用基类的公有或受保护函数来间接操作它。派生类内部访问只要成员在派生类作用域内即不是基类的private派生类就可以访问它无论它现在是public、protected还是private在派生类内部自己的private成员也是可访问的。继承方式影响的是这个成员在派生类中的访问说明符而不是派生类内部对它的可见性除了基类private。4.2 经典误区与排查技巧误区一认为private继承后派生类内部也不能访问基类public成员。class Base { public: int x; }; class Derived : private Base { public: void foo() { x 5; // 正确x在Derived内部是private成员但类内部可以访问自己的private成员。 } }; int main() { Derived d; // d.x 10; // 错误x在Derived中是private外部不能访问。 }排查记住继承方式改变的是成员在派生类作用域的访问级别不影响派生类内部对其的访问只要它不是基类的private。误区二试图在派生类构造函数初始化列表中初始化基类private成员。class Base { private: int a; }; class Derived : public Base { public: Derived(int val) : a(val) {} // 编译错误a是Base的private成员。 };正确做法如果基类private成员需要初始化必须通过基类的构造函数。class Base { private: int a_; public: Base(int a) : a_(a) {} // 提供初始化接口 }; class Derived : public Base { public: Derived(int val) : Base(val) {} // 正确通过基类构造函数初始化 };误区三混淆“访问”和“拥有”。派生类对象在内存中包含基类子对象因此它“拥有”基类的所有数据成员包括private。但是“拥有”不意味着“可以直接访问”。访问受语言规则的严格限制。常见问题速查表问题现象可能原因解决方案error: ‘xxx’ is a private member of ‘BaseClass’在派生类中直接访问了基类的private成员。改为通过基类的公有或受保护接口访问或考虑将该成员改为protected需权衡设计。error: ‘xxx’ is inaccessible within this context外部代码试图访问一个通过protected或private继承后在派生类中变为非公有的成员。检查继承方式。如果派生类需要对外提供该功能应在派生类中提供一个公有函数进行包装。无法将派生类指针/引用赋值给基类指针/引用使用了protected或private继承。只有public继承才表示“is-a”关系支持隐式向上转型。如需转型考虑改用public继承或使用组合。派生类无法覆盖基类虚函数该虚函数在基类中是private罕见但存在见下文“非虚接口(NVI)模式”。private虚函数只能被基类的友元或成员函数调用派生类可以覆盖它但无法直接调用。通常这是设计意图。5. 高级应用场景与设计模式实战掌握了基础规则后我们来看看这些特性在高级C编程和设计模式中的巧妙应用。5.1 非虚接口NVI模式与protected虚函数这是一种强大的设计模式它使用公有非虚函数作为“接口”而将实际的可定制行为委托给protected或private虚函数。class GameCharacter { public: // 公有非虚接口这是外部调用的稳定接口 int healthValue() const { // 前置逻辑如锁定、日志 std::cout Calculating health... std::endl; // 调用真正的实现细节可被派生类定制 int retVal doHealthValue(); // 后置逻辑如解锁、通知 std::cout Health calculated: retVal std::endl; return retVal; } virtual ~GameCharacter() default; private: // 私有虚函数派生类可以覆盖此实现但无法直接调用或绕过接口逻辑 virtual int doHealthValue() const { // 默认实现基于某些属性计算 return 100; } }; class EvilWizard : public GameCharacter { private: // 覆盖基类的私有虚函数实现 int doHealthValue() const override { // 巫师的健康值计算方式不同 return 50 getMagicPower() * 2; } int getMagicPower() const { return 30; } }; int main() { EvilWizard wizard; int health wizard.healthValue(); // 输出日志并调用EvilWizard::doHealthValue // wizard.doHealthValue(); // 错误doHealthValue是private。 }设计优势接口与实现分离公有接口healthValue()稳定封装了所有通用逻辑日志、验证、锁等。强制约束派生类无法绕过接口的前置/后置逻辑保证了行为的一致性。灵活定制派生类通过覆盖doHealthValue()来定制核心行为。5.2 使用private继承实现“is-implemented-in-terms-of”当派生类需要利用基类的某些特性但又不是“is-a”关系且需要重写虚函数或利用空基类优化时private继承是合适的。// 一个线程安全的计数器基类空基类主要提供接口和类型 class Lockable { public: virtual void lock() 0; virtual void unlock() 0; virtual ~Lockable() default; }; // 一个具体的互斥锁实现 class Mutex : public Lockable { public: void lock() override { /* 系统互斥锁上锁 */ } void unlock() override { /* 系统互斥锁解锁 */ } }; // 我们需要一个线程安全的堆栈它“用到了”锁的概念但不是一种锁。 // 同时我们希望堆栈能适配任何符合Lockable接口的锁。 templatetypename T, typename LockPolicy class ThreadSafeStack : private LockPolicy { // 私有继承锁策略 public: void push(const T value) { lock(); // 调用继承来的lock()现在是private但内部可用 data_.push_back(value); unlock(); } T pop() { lock(); if (data_.empty()) { unlock(); throw std::underflow_error(Stack empty); } T value data_.back(); data_.pop_back(); unlock(); return value; } // ... 其他方法 private: std::vectorT data_; // 注意没有LockPolicy类型的成员变量因为通过私有继承LockPolicy成了基类子对象。 }; int main() { ThreadSafeStackint, Mutex stack; // 使用Mutex作为锁策略 stack.push(42); // Lockable* lockPtr stack; // 错误私有继承不能向上转型为Lockable。 // 这符合设计ThreadSafeStack不是一个Lockable它只是用Lockable来实现线程安全。 }这里private继承实现了“策略模式”或“混入”mixin同时得益于空基类优化如果LockPolicy是无状态的如Mutex可能只是一个封装系统句柄的类ThreadSafeStack对象不会因为包含一个LockPolicy成员而额外增加大小在大多数编译器上。5.3 理解protected析构函数的作用将析构函数声明为protected是一种特殊技巧用于控制对象的生命周期。class NonStackObject { public: void doSomething() { std::cout Working... std::endl; } protected: ~NonStackObject() { std::cout NonStackObject destroyed. std::endl; } // 构造函数通常也设为protected配合静态工厂方法。 }; class ConcreteObject : public NonStackObject { public: static ConcreteObject* create() { return new ConcreteObject(); // 在堆上创建 } void destroy() { delete this; // 允许派生类删除自己 } private: ConcreteObject() default; // 构造函数私有或受保护 // 析构函数隐含为public不它继承了基类的protected访问权限。 // 但派生类成员函数可以调用基类的protected析构函数。 }; int main() { // NonStackObject obj; // 错误析构函数protected无法在栈上创建自动析构。 // delete ptr; // 如果有一个NonStackObject*也无法直接delete因为析构函数不可访问。 ConcreteObject* obj ConcreteObject::create(); obj-doSomething(); obj-destroy(); // 必须通过特定的接口销毁 // delete obj; // 错误~NonStackObject() is protected. }设计意图强制对象只能在堆上创建并且只能通过特定的管理接口如destroy()来销毁。常用于对象池、单例模式或需要精确控制析构时机的场景。protected析构函数阻止了外部直接delete基类指针也阻止了栈上对象的自动析构将生命周期管理权交给了类的设计者。6. 综合案例设计一个简单的图形框架让我们用一个综合案例来串联所有概念设计一个用于渲染简单图形的框架。// 基础图形类定义通用接口和部分实现 class Drawable { public: virtual ~Drawable() default; // 公有非虚接口绘制对象。所有绘制前的准备工作如变换矩阵压栈在这里完成。 void draw() const { beginDraw(); // 前置工作 doDraw(); // 实际绘制由派生类实现 endDraw(); // 后置工作 } // 公有接口获取位置 std::pairfloat, float getPosition() const { return position_; } void setPosition(float x, float y) { position_ {x, y}; markDirty(); } // 公有接口可见性 bool isVisible() const { return visible_; } void setVisible(bool v) { visible_ v; markDirty(); } protected: // 受保护的构造函数防止直接实例化抽象基类 Drawable(float x, float y) : position_({x, y}) {} // 受保护的虚函数派生类实现具体的绘制逻辑 virtual void doDraw() const 0; // 受保护的辅助方法派生类可以调用 void markDirty() { needsUpdate_ true; } bool isDirty() const { return needsUpdate_; } // 受保护的数据成员派生类可以直接访问以优化性能但需谨慎 std::pairfloat, float position_; bool visible_ true; private: // 私有实现细节派生类无需关心 void beginDraw() const { // 模拟OpenGL/DirectX的状态设置 std::cout [Render] Setting up state for drawing at ( position_.first , position_.second ) std::endl; } void endDraw() const { std::cout [Render] Cleaning up state. std::endl; if (needsUpdate_) { std::cout [Render] Object marked as dirty, update queued. std::endl; } } bool needsUpdate_ false; }; // 具体图形圆形 class Circle : public Drawable { // public继承Circle is-a Drawable public: Circle(float x, float y, float r) : Drawable(x, y), radius_(r) { if (r 0) throw std::invalid_argument(Radius must be positive.); } float getRadius() const { return radius_; } void setRadius(float r) { if (r 0) throw std::invalid_argument(Radius must be positive.); radius_ r; markDirty(); // 调用基类protected方法 } protected: // 实现基类定义的受保护纯虚函数 void doDraw() const override { std::cout [Circle] Drawing at ( position_.first , position_.second ) with radius radius_ std::endl; // 这里会调用具体的图形API使用position_和radius_ } private: float radius_; }; // 具体图形矩形 class Rectangle : public Drawable { public: Rectangle(float x, float y, float w, float h) : Drawable(x, y), width_(w), height_(h) {} // ... 类似的getter/setter protected: void doDraw() const override { std::cout [Rectangle] Drawing at ( position_.first , position_.second ) size width_ x height_ std::endl; } private: float width_, height_; }; // 一个复合图形包含多个子图形。它“有一个”Drawable列表而不是“是一个”Drawable。 // 但它也想被绘制所以它实现Drawable接口通过组合转发而不是继承。 class CompositeShape /* : public Drawable */ { // 不继承Drawable public: void addShape(std::unique_ptrDrawable shape) { shapes_.push_back(std::move(shape)); } // 提供与Drawable兼容的接口但不是继承关系 void draw() const { for (const auto shape : shapes_) { shape-draw(); // 委托给子对象 } } void setPosition(float x, float y) { // 复合图形的位置可能影响所有子图形这里简化处理 for (auto shape : shapes_) { // 不能直接访问shape-position_因为它是protected。 // 必须通过公有接口。 auto [sx, sy] shape-getPosition(); // 计算相对位置偏移...此处省略 } } private: std::vectorstd::unique_ptrDrawable shapes_; }; int main() { Circle c(10.0f, 20.0f, 5.0f); Rectangle r(30.0f, 40.0f, 15.0f, 25.0f); c.setVisible(true); r.setPosition(35.0f, 45.0f); // 会调用markDirty() std::vectorDrawable* scene; // 利用多态 scene.push_back(c); scene.push_back(r); for (auto* obj : scene) { obj-draw(); // 统一接口调用触发NVI模式 } // c.markDirty(); // 错误markDirty()是Drawable的protected成员外部不能访问。 // float x c.position_.first; // 错误position_是protected即使通过Circle对象外部也不能访问。 }这个案例展示了public继承Circle和Rectangle与Drawable是“is-a”关系。protected成员doDraw(),markDirty(),position_等为派生类提供扩展点和内部数据访问同时对外隐藏。private成员beginDraw(),endDraw(),needsUpdate_是完全封装的实现细节。NVI模式公有的draw()方法是非虚接口封装固定逻辑doDraw()是受保护的虚函数用于定制。访问控制外部代码只能通过公有接口getPosition,setPosition,draw与对象交互。派生类可以访问protected成员。CompositeShape演示了当不适合用“is-a”关系时如何使用组合而非继承。理解并熟练运用访问权限和继承方式能让你设计的C类层次结构更加清晰、健壮和灵活。记住一个简单的决策流数据成员默认private为继承设计的接口或工具函数考虑protected对外契约定义为public。继承首选public以建立“is-a”关系除非有明确需求如实现继承、EBO否则使用组合代替protected/private继承。在实践中多思考“这个成员/类的关系应该对谁可见”你的代码设计自然会走向成熟。