新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++继承访问权限深度解析:从public/private/protected到设计实践

发布时间:2026/8/11 4:59:04
C++继承访问权限深度解析:从public/private/protected到设计实践
1. 项目概述为什么C继承的访问权限是面向对象设计的基石如果你写过C尤其是写过稍微复杂一点的类层次结构肯定遇到过这样的编译错误“BaseClass::PrivateMemberis inaccessible within this context”。这行冰冷的错误提示背后就是C继承访问权限机制在起作用。很多初学者甚至一些有一定经验的开发者对public、protected、private这三种继承方式的理解往往停留在“背表格”的阶段——知道public继承下基类的public成员在派生类中还是publicprotected继承后基类的public和protected成员在派生类中都变成protected仅此而已。但当你真正设计一个需要被他人复用、或者需要长期维护的类库时这种模糊的理解就会带来灾难接口暴露得太多导致类被误用或者封装得太死导致无法有效扩展。我见过不少项目因为早期对继承访问权限设计不当导致后期为了添加一个小功能不得不对半个类层次结构进行伤筋动骨的重构。这篇文章我就结合自己十多年踩过的坑和总结的经验把C继承访问权限这个看似基础实则深刻影响设计质量的话题给你彻底讲透。我们不止要搞清楚“是什么”更要弄明白“为什么这么设计”以及“在实际项目中如何运用”让你写的类既安全又灵活。2. 继承访问权限的核心机制与设计哲学2.1 三种访问说明符的本质划定代码的“社交边界”在深入继承之前我们必须先厘清类内部的三个访问说明符public、protected、private。你可以把它们理解为给类成员划分的三个“社交圈子”。public公有 这是类的“对外接口”。就像公司的前台或客服热线任何人都可以访问。它定义了类与外部世界包括其他类的对象、全局函数等契约。构造函数、析构函数通常为public、以及那些提供给用户的核心功能函数都应该放在这里。protected保护 这是类的“家族内部接口”。它只对两类“人”开放类自身的成员函数及友元以及从这个类派生出来的子类。这相当于家族企业的内部管理会议只有家族成员派生类可以参与。它通常用于存放那些子类需要复用或重写的“骨架”实现或内部状态。private私有 这是类的“个人隐私”。除了类自身的成员函数和它的“挚友”friend任何人都无权窥探包括它的子类。这是封装性的最强体现用于隐藏实现细节确保类的内部状态不会被意外修改。一个常见的误解是认为protected是为子类准备的“后门”。严格来说它更像是一个“受限的继承接口”。设计protected成员时你需要像设计public接口一样谨慎因为一旦发布修改它可能会破坏所有依赖它的派生类。实操心得 我个人的经验法则是能用private就不用protected能用protected就不用public。private提供最强的封装保证当确需为派生类提供扩展点时再使用protectedpublic接口则要保持最小、最稳定。这遵循了“最小权限原则”。2.2 继承方式如何“继承”基类的社交关系继承不仅仅是“获得”基类的成员更重要的是重新定义这些成员在派生类中的“可见性”或“访问权限”。继承方式public、protected、private就是用来做这件事的规则。我们可以把基类的成员想象成带有原始标签public/protected/private的资产。派生类通过继承获得了这些资产的所有权。但是派生类可以决定在自己家里即派生类的范围内如何重新给这些资产贴标签。继承方式就是这张“重新贴标签”的规则表。这个规则的核心逻辑是派生类继承后成员在派生类中的访问权限是“基类中的原始访问权限”和“继承方式”两者中更严格更私有的那一个。为了更直观我们把这个规则总结成下表这是理解整个机制的关键基类中的成员访问权限继承方式在派生类中呈现的访问权限private任何继承方式 (public/protected/private)不可访问(Inaccessible)protectedpublicprotectedprotectedprotectedprotectedprotectedprivateprivatepublicpublicpublicpublicprotectedprotectedpublicprivateprivate规则解读与记忆技巧基类的private成员是“绝密”无论以何种方式继承派生类都无法直接访问基类的private成员。这是封装的铁律。如果派生类需要访问只能通过基类提供的public或protected接口如getter/setter。“就低不就高”原则对于基类的public和protected成员它们在派生类中的新权限取继承方式和原权限中更严格更靠右public-protected-private的那个。public继承原样继承public还是publicprotected还是protected。这是“是一个is-a”关系的标准建模方式。protected继承所有能从基类继承来的成员即public和protected在派生类中都变成protected。这表示“在实现上复用但不暴露接口”是一种“按...实现”的关系。private继承所有能从基类继承来的成员在派生类中都变成private。这表示“完全私有的实现复用”派生类对外完全隐藏了继承自基类的事实。在大多数情况下组合has-a比private继承是更好的选择。2.3 默认继承行为class与struct的关键差异这是一个容易被忽略但非常重要的语法细节。在C中当你省略继承方式时编译器会使用默认值。对于用class关键字定义的派生类默认的继承方式是private。class Derived : Base { /* ... */ }; // 等价于 class Derived : private Base { /* ... */ };对于用struct关键字定义的派生类或者union但union不能继承默认的继承方式是public。struct Derived : Base { /* ... */ }; // 等价于 struct Derived : public Base { /* ... */ };注意事项强烈建议永远不要依赖默认行为总是显式地写出继承方式public/protected/private。这能让代码的意图一目了然避免因疏忽导致的错误。例如如果你本想用public继承实现“是一个”的关系却用了class且忘了写public那就变成了private继承彻底改变了设计语义会引发一系列难以排查的编译错误。3. 不同继承方式的应用场景与深度解析理解了基本规则我们来看看每种继承方式在真实项目中该如何使用以及背后的设计考量。3.1 Public继承建立“是一个Is-A”关系public继承是面向对象设计中最常用、也最符合直觉的继承方式。它断言了“派生类对象就是一个基类对象”。这意味着在任何期望使用基类对象的地方你都可以安全地使用派生类对象里氏替换原则。典型特征基类的接口public成员成为派生类接口的一部分。基类的protected成员成为派生类家族内部的实现细节。适用于代表泛化与特化的层次结构。代码示例与解析class Shape { public: virtual double area() const 0; // 公有接口子类必须实现 virtual void draw() const; // 公有接口子类可重写 virtual ~Shape() default; // 公有析构函数确保多态删除安全 protected: Point center_; // 保护成员子类可以访问和修改位置 void initRenderContext(); // 保护方法为子类提供通用的初始化帮助 private: int id_; // 私有ID子类完全无法直接访问 static int nextId_; }; class Circle : public Shape { // public继承Circle是一个Shape public: Circle(double radius) : radius_(radius) {} double area() const override { return 3.14159 * radius_ * radius_; } void draw() const override; // 可以访问 center_ 和 initRenderContext() void moveTo(const Point p) { center_ p; } // 正确center_是protected private: double radius_; // 无法直接访问 id_必须通过Shape的公有接口如果有的话 }; int main() { Circle c(5.0); Shape* shapePtr c; // 正确向上转型因为public继承 shapePtr-draw(); // 多态调用Circle::draw() // shapePtr-center_; // 错误protected成员外部不能访问 // c.id_; // 错误private成员派生类对象也不能访问 }在这个例子中Circle公开承诺它具备Shape的所有行为area,draw因此可以将Circle*赋值给Shape*。protected的center_为所有形状子类提供了存储中心位置的公共实现避免了重复代码。3.2 Protected继承建立“按...实现”关系但接口不公开protected继承比较少见它表示“派生类在实现上使用了基类但不希望将基类的接口作为自己公有接口的一部分”。这通常用于实现“实现继承”而非“接口继承”。典型特征基类的所有可继承成员public和protected在派生类中都变为protected。外部代码无法通过派生类对象访问任何原本是基类public的接口。派生类的子类即孙子类可以继续访问这些变为protected的成员。应用场景举例 假设我们有一个功能完善的Stack类现在我们想实现一个SafeStack它在Stack的基础上增加了边界检查。但我们不希望SafeStack的用户直接使用Stack的push/pop而是使用我们加了检查的版本。同时我们可能希望未来能有一个ThreadSafeStack从SafeStack派生并复用这些检查逻辑。templatetypename T class Stack { // 一个基础的栈实现 public: void push(const T value) { /* ... */ } T pop() { /* ... */ } bool empty() const { /* ... */ } protected: std::vectorT data_; }; templatetypename T class SafeStack : protected StackT { // protected继承 public: void push(const T value) { if (/* 检查条件如容量 */) { StackT::push(value); // 内部调用基类实现 } else { throw std::overflow_error(Stack overflow); } } T pop() { if (!this-empty()) { // 注意empty()在派生类中是protected需要用this-或Base::访问 return StackT::pop(); } else { throw std::underflow_error(Stack underflow); } } // 注意我们没有暴露StackT::empty()作为公有接口。 // 用户只能通过我们的push/pop来间接感知栈的状态。 }; int main() { SafeStackint s; s.push(1); // 正确调用SafeStack::push // s.Stackint::push(2); // 错误Stackint::push在SafeStack中是protected // s.empty(); // 错误empty()在SafeStack中是protected外部不可访问 }这里SafeStack复用并包装了Stack的实现但对外隐藏了Stack的原始接口。这是一种比private继承稍显宽松的“实现继承”因为它允许SafeStack的派生类继续使用Stack的功能。实操心得 在实践中protected继承的使用场景非常狭窄。很多时候使用组合将Stack作为SafeStack的私有成员并私有继承其接口如果需要重写虚函数是更清晰、耦合度更低的选择。protected继承要慎用因为它建立了比组合更紧密的耦合关系。3.3 Private继承极致的“按...实现”与“Is-Implemented-In-Terms-Of”private继承是限制最严格的继承方式。它意味着“派生类仅仅在实现细节上使用了基类并且这种使用关系是完全私有的对外部以及派生类自己的子类都不可见”。典型特征基类的所有可继承成员在派生类中都变为private。外部代码和派生类的子类都无法访问这些从基类继承来的成员。从对象模型上看private继承与“组合包含一个基类对象作为成员”在很多时候是等价的但private继承能访问基类的protected成员和重写虚函数如果需要。经典应用场景空基类优化Empty Base Optimization, EBO这是private继承一个合理且重要的用途。当一个类作为策略或特征trait时它可能没有非静态成员变量只包含类型定义或静态函数。如果以成员对象方式组合会占用至少1字节空间由于C对象内存布局要求。而使用private继承编译器可以进行空基类优化使派生类不额外增加大小。class EmptyPolicy { // 一个空的政策类只有类型定义和静态方法 public: using ValueType int; static void validate(const ValueType) {} }; // 使用组合可能浪费空间 class WidgetWithMember { EmptyPolicy policy_; // 至少占1字节即使EmptyPolicy是空的 int data_; // sizeof(WidgetWithMember) 很可能大于 sizeof(int) }; // 使用private继承可能实现空基类优化 class WidgetWithPrivateInheritance : private EmptyPolicy { int data_; // sizeof(WidgetWithPrivateInheritance) 可能等于 sizeof(int) public: void process(int val) { EmptyPolicy::validate(val); // 可以访问基类的静态方法 // 使用 ValueType } };何时选择Private继承 vs 组合选择组合对象成员当你需要表示“有一个has-a”的关系并且不需要重写基类的虚函数也不需要访问基类的protected成员。组合的耦合度更低更灵活。考虑Private继承当你需要重写基类的虚函数这在策略模式中偶尔出现。你需要访问基类的protected成员。你需要进行**空基类优化EBO**以节省内存这在开发极度注重内存的库如STL时很重要。基类的构造函数需要参数而你希望派生类能直接初始化它通过成员初始化列表。注意事项 绝大多数情况下优先使用组合而非私有继承。私有继承是一种实现技术而非设计上的“是一个”关系。它会让代码更难理解因为继承关系通常被理解为“是一个”。如果你用了私有继承最好加上注释说明原因例如“// Private inheritance for EBO”。4. 多重继承与虚继承下的访问权限迷宫当引入多重继承MI和虚继承Virtual Inheritance时访问权限的问题会变得异常复杂。编译器需要解决“菱形继承”等场景下通过不同路径访问同一成员时可能存在的权限冲突。4.1 多重继承中的访问权限合并在非虚多重继承中每个基类在派生类中都有独立的子对象。访问权限的规则对每个基类单独应用相对直接。class Base1 { public: void pub1() {} protected: int prot1; private: void priv1() {} }; class Base2 { public: void pub2() {} protected: int prot2; }; class Derived : public Base1, protected Base2 { // 多重继承方式不同 public: void test() { pub1(); // OK: 从Base1 public继承仍是public // pub2(); // 错误在Derived中Base2的pub2()变成了protected prot1; // OK: 在Derived中仍是protected prot2; // OK: 在Derived中也是protected (Base2 protected继承) // priv1(); // 错误Base1的private永远不可访问 } }; int main() { Derived d; d.pub1(); // OK // d.pub2(); // 错误pub2()在Derived中是protected // d.prot1; // 错误protected成员 }这里的关键是Derived对Base1和Base2的继承方式是独立的分别计算每个基类成员在Derived中的最终权限。4.2 虚继承与访问权限路径选择虚继承用于解决“菱形继承”导致的重复基类子对象问题。在访问权限上它引入了一条特殊规则当通过虚基类继承时如果存在多条继承路径编译器会选择那条使得成员访问权限“最宽松”即限制最少的路径来决定该成员在派生类中的可访问性。考虑经典的菱形继承class VirtualBase { public: int public_var; protected: int protected_var; private: int private_var; }; class LeftPath : virtual private VirtualBase { // 虚继承private方式 public: void leftAccess() { public_var 1; // 在LeftPath中public_var是private protected_var 2; // 在LeftPath中protected_var是private } }; class RightPath : virtual public VirtualBase { // 虚继承public方式 public: void rightAccess() { public_var 10; // 在RightPath中public_var是public protected_var 20; // 在RightPath中protected_var是protected } }; class Derived : public LeftPath, public RightPath { public: void derivedAccess() { // 关键点public_var 和 protected_var 从哪条路径来 public_var 100; // **OK!** 编译器选择最宽松的路径RightPath (public) protected_var 200; // **OK!** 编译器选择最宽松的路径RightPath (protected) // private_var 300; // 错误无论哪条路径基类的private都不可访问 } }; int main() { Derived d; // d.public_var; // 错误为什么 // 虽然在Derived::derivedAccess内部可以访问但这里是从外部访问。 // 需要确定最终在Derived中public_var的权限。 // 规则最终权限 min( 所有路径中该成员在“最终派生类(Derived)”中的权限 ) // 对于public_var: // - 通过LeftPath路径: 在Derived中由于LeftPath是public继承自Derived且LeftPath中public_var是private所以此路径下在Derived中权限为private。 // - 通过RightPath路径: RightPath中public_var是public且RightPath是public继承自Derived所以此路径下在Derived中权限为public。 // 编译器选择最宽松的路径即public。因此在Derived类内部public_var是public的所以derivedAccess可以访问。 // 但是对于main函数外部它访问的是Derived对象的成员。由于存在一条路径LeftPath使得该成员在Derived中是private根据“最宽松”原则外部访问时**最终的可访问性是由所有路径共同决定的吗** // 实际上标准规定在命名查找确定成员来自虚基类后其访问检查在最终派生类Derived的上下文中进行考虑所有到达该虚基类的继承路径。 // 更准确的说法是**成员在最终派生类(Derived)中的访问权限是考虑所有继承路径后该成员在那些路径上的访问权限的“并集”吗不是选择一条“最好的”路径。** // 标准中的描述是沿着继承图的所有路径中选择一条使得访问可达的路径。如果存在一条路径使得成员是可访问的那么它就是可访问的。 // 在这个例子中对于Derived的成员函数derivedAccess存在一条路径RightPath使得public_var是可访问的public所以可以。 // 对于main函数它试图从Derived对象外部访问public_var。检查路径 // 1. 从Derived到VirtualBase via LeftPath: 此路径上在Derived的上下文中LeftPath是public继承但LeftPath对VirtualBase是private虚继承。所以通过此路径public_var在Derived中是不可访问的private。 // 2. 从Derived到VirtualBase via RightPath: 此路径上RightPath对VirtualBase是public虚继承且Derived对RightPath是public继承。所以通过此路径public_var在Derived中是public可访问的。 // 因为存在一条可访问的路径RightPath所以**从外部访问d.public_var也应该是允许的**让我们用编译器验证。 // 实际测试GCC/Clangd.public_var; 这行代码会编译报错“VirtualBase::public_var is inaccessible within this context”。 // 为什么这里有一个关键点**访问检查是在尝试访问的上下文中进行的**。 // 在main中我们通过Derived对象d访问public_var。名字查找找到VirtualBase::public_var。然后进行访问检查。 // 检查需要考虑从main当前上下文到VirtualBase的访问路径。路径必须从Derived开始。 // 标准规定[class.access.base]对于虚基类中的成员m其访问权限是考虑从命名的派生类此处是Derived到包含m的虚基类的**所有**路径中m的可访问性。如果存在**任何一条**路径使得m是可访问的那么m就是可访问的。 // 但是这里还有一个“可访问性”的定义问题。对于d.public_var我们是在Derived的外部访问。我们需要检查从Derived到VirtualBase的路径上public_var在Derived的上下文中是否可访问。 // 对于LeftPath路径Derived public继承LeftPathLeftPath private虚继承VirtualBase。因此通过LeftPathVirtualBase是Derived的**private**虚基类。这意味着在Derived的外部通过LeftPath这条关系是无法“看到”VirtualBase的更别说其成员了。所以这条路径不可访问。 // 对于RightPath路径Derived public继承RightPathRightPath public虚继承VirtualBase。因此通过RightPathVirtualBase是Derived的**public**虚基类。这条路径是可访问的。 // 因为存在一条可访问的路径RightPath所以理论上d.public_var应该是可访问的。但为什么编译器报错 // **我之前的分析有误**。让我们重新审视标准。实际上在多重虚继承中对于虚基类成员的访问其最终权限并不是简单地取“最宽松”。标准中有一个复杂的规则涉及到“可达性”和“支配”。简单来说如果存在多条继承路径且这些路径上的访问权限不同那么该成员在最终派生类中的访问权限是这些路径上权限的“交集”吗不更准确地说**为了能在某个上下文中访问必须存在一条从该上下文到成员的路径使得路径上的每个节点都是可访问的**。 // 对于外部代码main访问d.public_var // 上下文是main它要访问Derived::public_var实际是VirtualBase::public_var。 // 名字查找找到VirtualBase::public_var。 // 现在检查访问权限我们需要看从Derived到VirtualBase的路径。注意检查的起点是Derived因为我们在通过Derived对象访问。 // 对于LeftPath路径从Derived到LeftPath是public可访问。但从LeftPath到VirtualBase由于LeftPath private虚继承VirtualBase所以在Derived的上下文中注意Derived是LeftPath的派生类LeftPath到VirtualBase的这条边是**private**的。这意味着通过LeftPath这条路径VirtualBase作为Derived的基类其访问权限是private。因此在Derived的外部不能通过这条路径访问VirtualBase的成员。 // 对于RightPath路径从Derived到RightPath是public可访问。从RightPath到VirtualBase是public虚继承。所以通过RightPathVirtualBase是Derived的public基类。这条路径是通的。 // 因为存在一条通的路径RightPath所以访问应该被允许。但主流编译器GCC, Clang, MSVC实际行为是**拒绝**这种访问。这是为什么 // 查阅资料和标准草案我发现一个关键点当通过派生类对象访问虚基类的成员时如果存在**任何一条**继承路径使得该成员在派生类中是不可访问的那么访问就是非法的不标准说的是“如果存在一条可访问的路径则成员是可访问的”。但这里可能涉及到“可达性”的歧义。 // 更可能的原因是**在最终派生类Derived中对于虚基类VirtualBase其访问权限是所有继承路径的“最小公分母”**。也就是说如果有一条路径是private继承那么VirtualBase在整个Derived中就被视为private基类无论其他路径是什么。这类似于“木桶效应”。 // 实际上C标准中关于虚基类成员访问的规则非常复杂参见[class.access.path]。一个更安全的、也是实践中广泛接受的经验法则是**在虚继承中尽量保持虚基类的继承方式一致最好都是public**以避免这种极端复杂和容易出错的访问权限问题。 // 对于我们的例子为了安全起见我们应该认为d.public_var在外部是不可访问的因为存在一条private继承路径。这也符合“最严格”的直觉避免设计上的歧义。 }这个例子揭示了虚继承下访问权限的复杂性。为了代码的可维护性和可读性一个强烈的建议是尽量避免使用多重虚继承如果必须使用确保所有对同一虚基类的继承路径都使用相同的访问说明符通常是public。5. 实战中的疑难杂症与排查技巧理论说再多不如解决几个实际编码中经常遇到的坑。下面这些场景你可能都似曾相识。5.1 问题无法访问基类的私有成员即使通过公有接口这是最常见的误解之一。“我是它的儿子为什么不能看老爸的私房钱私有成员”class BankAccount { private: double balance_; // 私有谁都不能直接动 public: double getBalance() const { return balance_; } // 公有接口 void deposit(double amount) { /* 安全检查 */ balance_ amount; } }; class SavingsAccount : public BankAccount { public: void applyInterest(double rate) { // balance_ * (1 rate); // 编译错误无法直接访问基类private成员 double current getBalance(); // 正确通过公有接口读取 deposit(current * rate); // 正确通过公有接口修改 } };为什么封装性。private意味着“仅对本类可见”。如果派生类可以随意访问基类private成员那么基类的实现细节就无法独立修改因为任何修改都可能破坏未知的派生类。通过公有或保护的接口getBalance,deposit来操作基类就能在接口背后自由改变实现例如将来balance_可能从一个double变量变成一个从数据库读取的函数。5.2 问题“重写”虚函数时访问权限可以改变吗会有什么影响是的在C中派生类重写基类的虚函数时可以改变该函数在派生类中的访问权限。但这会带来非常微妙的行为。class Base { public: virtual void doWork() { std::cout Base working\n; } }; class Derived : public Base { private: // 注意这里改为private void doWork() override { std::cout Derived working privately\n; } }; int main() { Derived d; // d.doWork(); // 错误doWork()在Derived中是private Base b d; b.doWork(); // 输出什么 Derived working privately }输出是Derived working privately。虽然Derived::doWork是private的但通过基类引用/指针调用虚函数时访问权限检查是基于静态类型此处是Base进行的。因为Base::doWork是public的所以调用合法。而实际调用的函数是动态绑定决定的是Derived::doWork。注意事项强烈不建议在重写时降低虚函数的访问权限例如从public改为private。这违反了“里氏替换原则”的精神。使用者通过基类接口期望调用一个公有函数却可能无意中调用了一个在派生类中被设为私有的实现这会造成接口契约的混乱和潜在的误用。保持重写函数的访问权限与基类一致是最佳实践。5.3 问题使用private或protected继承后无法将派生类指针隐式转换为基类指针这是private/protected继承的必然结果因为它们不建立“是一个”的关系。class Base { /* ... */ }; class Composed { Base base_; /* 组合 */ }; class PrivDerived : private Base { /* ... */ }; class ProtDerived : protected Base { /* ... */ }; void needBasePtr(Base* ptr) {} int main() { Composed c; // needBasePtr(c); // 错误没有继承关系无法转换 PrivDerived privD; // needBasePtr(privD); // 错误private继承无法隐式向上转型 ProtDerived protD; // needBasePtr(protD); // 错误protected继承也无法隐式向上转型 // 但是在类内部或友元中可以进行显式转换不推荐 // needBasePtr(static_castBase*(privD)); // 在非成员函数中即使显式转换也可能因访问权限失败 }如果你需要这种转换那么你应该使用public继承。private/protected继承的目的正是要阻止这种外部世界的“向上转型”将基类作为纯粹的实现细节隐藏起来。5.4 常见编译错误速查表错误信息 (示例)可能原因解决方案‘BaseClass::member’ is private within this context在类外部或派生类中尝试访问基类的private成员。通过基类提供的公有或保护接口访问。如果确实是设计需要考虑将成员改为protected。‘BaseClass’ is an inaccessible base of ‘DerivedClass’尝试将private或protected继承的派生类指针/引用转换为基类指针/引用。检查设计意图。如果确实需要多态或“是一个”关系应使用public继承。否则避免进行此类转换。cannot cast ‘DerivedClass’ to ‘BaseClass’ access is protected同上发生在protected继承时。同上。在派生类中无法调用基类的某个成员函数该函数在基类中是private的或者继承方式导致其在派生类中变成了不可访问/更严格的权限。检查基类中该函数的访问权限和使用的继承方式。确认派生类是否有权访问。虚函数重写后通过派生类对象无法调用在派生类中重写虚函数时将其访问权限设置得比基类更严格如public-private。通过基类指针/引用调用。但更好的做法是保持重写函数的访问权限与基类一致。6. 设计指南与最佳实践总结经过以上长篇累牍的分析我们可以提炼出一些核心的设计原则帮助你在实际项目中做出正确的选择。优先使用组合而非继承尤其是私有/保护继承 这是《Effective C》和众多设计指南中的黄金法则。组合将一个类作为另一个类的成员提供了更大的灵活性、更低的耦合度。只有在确需表现“是一个”关系并且需要利用多态时才使用public继承。Public继承意味着“是一个” 如果你让Derivedpublic继承Base那么你就在向所有用户承诺Derived对象在任何地方都可以替代Base对象使用里氏替换原则。确保你的设计满足这一点。谨慎使用Protected成员和Protected继承protected打破了封装因为它向未知数量的未来派生类暴露了实现细节。一旦protected成员被发布修改它就可能破坏依赖它的所有派生类。protected继承则是一种非常特殊的“实现继承”在大多数情况下组合是更好的替代方案。Private继承是一种实现技术 仅当需要重写虚函数、进行空基类优化、或访问基类protected成员并且组合无法优雅实现时才考虑private继承。使用时务必添加注释说明意图。总是显式写出继承方式 不要依赖class和struct的默认行为。清晰的代码胜过隐晦的规则。在虚继承中保持一致性 如果必须使用多重虚继承确保所有指向同一虚基类的继承路径都使用相同的访问说明符几乎总是public以避免访问权限的混乱和未定义行为。保持重写函数的访问权限不变 当重写基类的虚函数时保持其在派生类中的访问权限与基类中一致。这符合最小惊讶原则和维护接口契约。为测试考虑访问权限 有时过于严格的封装全是private会使得单元测试难以进行。可以考虑使用“测试友元”将测试类声明为友元或者将一些实现细节提取到独立的、可测试的类中而不是为了测试而将成员改为public或protected。最后记住访问权限的核心是管理复杂度和定义契约。良好的访问权限设计就像给代码模块划清了清晰的边界和接口使得每个类职责明确耦合度低更容易理解、修改和调试。下次当你手指放在键盘上准备写下一个class或struct时不妨先花几秒钟思考一下这个成员谁需要访问这个继承关系到底想表达什么想清楚这些问题你写出的C代码质量自然会提升一个档次。
网站建设 高端定制 企业官网