C++继承与组合深度解析:从三大特性到设计模式实战 📅 2026/7/15 6:39:46 1. 项目概述从“是什么”到“为什么”在C的面向对象编程世界里封装、继承和多态这三大特性就像盖房子的三块基石。封装让你把数据和操作打包成一个“黑盒”多态让你能用统一的接口处理不同的对象而继承则是我们今天要深挖的核心。它允许你基于一个已有的类基类或父类来创建一个新的类派生类或子类从而复用代码、建立类之间的层次关系。但继承远不止是“复制粘贴”父类的代码那么简单。它背后牵涉到访问权限控制、内存布局、接口与实现的分离、以及至关重要的“is-a”关系设计。更复杂的是当你发现单纯用继承来描述“有一个”的关系显得笨拙甚至错误时组合Composition这个强大的工具就登场了。很多初学者甚至一些有经验的开发者常常混淆或误用这两者导致代码结构僵化、难以维护。这篇文章我将结合十多年的C项目踩坑经验带你彻底搞懂继承的三种方式public, protected, private、它们的内存与行为影响并深入剖析组合作为继承的替代或补充方案在何时、为何以及如何被使用。我们不止步于语法更要探究设计思想让你在下一个项目中能自信地做出最合适的设计选择。2. 继承的三种方式不仅仅是访问权限C提供了三种继承方式public、protected和private。很多人只记住了它们改变了成员在派生类中的访问权限但这只是冰山一角。更深层的区别在于派生类与基类之间的语义关系以及这种关系对代码设计和使用的约束。2.1 Public继承建立“是一个is-a”关系public继承是C中最常用也最需要谨慎使用的继承方式。它的核心语义是派生类对象“是一个”基类对象。这意味着在任何期望使用基类对象的地方你都可以安全地使用派生类对象来替代。class Vehicle { public: virtual void move() 0; // 纯虚函数Vehicle是抽象类 virtual ~Vehicle() {} // 虚析构函数确保正确释放资源 }; class Car : public Vehicle { // public继承 public: void move() override { std::cout Car is driving on the road.\n; } }; void transport(Vehicle v) { v.move(); // 可以接受任何Vehicle的派生类 } int main() { Car myCar; transport(myCar); // 正确Car is-a Vehicle }关键点解析接口继承通过public继承派生类Car继承了Vehicle的接口move。Car对象可以被当作Vehicle对象来使用如传递给transport函数。访问权限映射基类的public成员 - 在派生类中仍为public。基类的protected成员 - 在派生类中为protected。基类的private成员 -对派生类不可见。注意是“不可见”而非“不存在”。派生类对象的内存布局中仍然包含基类的private成员派生类的构造函数、析构函数、拷贝控制成员拷贝构造、拷贝赋值等也会隐式地操作它们但派生类的成员函数无法直接访问它们。“is-a”的严格性这是设计的关键。Car继承Vehicle是合理的因为所有Car应该具备Vehicle的所有行为比如都能move。但经典的“正方形继承自矩形”就是一个反例因为正方形改变了长宽必须相等的约束违反了矩形长宽可独立变化的基类契约。实操心得在设计public继承层次时务必用“里氏替换原则”Liskov Substitution Principle, LSP来检验。问问自己所有对基类对象的假设前置条件、后置条件、不变量是否都适用于派生类对象如果答案是否定的那么public继承很可能是不合适的。2.2 Protected与Private继承实现继承的两种形态当你不希望建立“is-a”关系而只是想复用基类的实现代码时protected和private继承就派上用场了。它们统称为实现继承。class Utility { public: void helperFunc1() { /* ... */ } protected: void helperFunc2() { /* ... */ } private: int secretData; }; class MyClassProtected : protected Utility { // helperFunc1 在这里变成 protected // helperFunc2 在这里仍然是 protected // secretData 不可见 void foo() { helperFunc1(); // OK可在派生类内部使用 helperFunc2(); // OK } }; class MyClassPrivate : private Utility { // helperFunc1 在这里变成 private // helperFunc2 在这里变成 private // secretData 不可见 void bar() { helperFunc1(); // OK可在派生类内部使用 helperFunc2(); // OK } }; void test() { MyClassProtected objP; // objP.helperFunc1(); // 错误helperFunc1在MyClassProtected中是protected MyClassPrivate objV; // objV.helperFunc1(); // 错误helperFunc1在MyClassPrivate中是private Utility* ptr objP; // 错误protected/private继承下不能将派生类指针/引用转为基类指针/引用除了在派生类成员函数内部。 }核心区别与设计考量特性Protected继承Private继承访问权限变化基类public/protected成员 - 派生类protected成员基类public/protected成员 - 派生类private成员后续继承影响可以被进一步继承。MyClassProtected的派生类还能访问到Utility的public/protected成员尽管在MyClassProtected中它们已是protected。继承链终止。MyClassPrivate的派生类无法再访问Utility的任何成员因为它们在MyClassPrivate中已是private。语义“在派生类及其后续派生类中实现复用”。“仅在当前派生类中实现复用且不希望被进一步暴露”。与组合的对比很多时候可以用“包含一个protected成员对象”来替代。很多时候可以用“包含一个private成员对象”来替代。但private继承有一个独特优势空基类优化EBO。空基类优化Empty Base Optimization, EBO实战当一个类没有任何非静态成员变量、虚函数且其基类和所有非静态成员也都是空类型时它的大小理论上可以为0。但C规定独立对象必须有唯一地址因此sizeof(空类)通常为1。然而如果这个空类作为基类被继承编译器可以将其优化到不占派生类的任何空间。class Empty {}; // 空类sizeof(Empty) 1 (通常) // 使用组合包含一个Empty对象成员 class HolderComposition { Empty e; // 至少占1字节可能因内存对齐占更多 int data; }; // sizeof(HolderComposition) 很可能 sizeof(int) 1并向上对齐。 // 使用private继承利用EBO class HolderPrivateInherit : private Empty { int data; }; // sizeof(HolderPrivateInherit) 很可能 sizeof(int)。Empty部分被优化掉了。标准库中的std::unique_ptr、std::shared_ptr的deleter以及boost::noncopyable等都广泛利用了EBO来避免不必要的空间开销。注意事项优先考虑使用组合对象成员而不是protected/private继承来实现代码复用除非你需要重写基类的虚函数组合无法直接做到需要额外设计。利用空基类优化EBO来节省空间。访问基类的protected成员组合时成员对象如果是其他类的实例则无法访问其protected成员。如果以上三点都不满足那么组合通常是更清晰、耦合度更低的选择。3. 虚函数、纯虚函数与接口设计继承机制与虚函数密不可分。虚函数实现了运行时多态是C动态绑定的核心。3.1 虚函数与纯虚函数接口与实现的博弈class Shape { public: // 纯虚函数只继承接口必须被重写 virtual double area() const 0; // 虚函数继承接口和一份默认实现 virtual void draw() const { std::cout Drawing a generic shape.\n; } // 非虚函数继承接口和一份强制实现不应被重写否则会隐藏而非覆盖 int getId() const { return id_; } virtual ~Shape() default; // 基类析构函数必须是虚的 private: int id_; }; class Circle : public Shape { public: Circle(double r) : radius_(r) {} // 必须实现纯虚函数area double area() const override { return 3.14159 * radius_ * radius_; } // 可以选择性重写虚函数draw void draw() const override { std::cout Drawing a circle with radius radius_ .\n; } // 不能重写但可以隐藏非虚函数getId private: double radius_; };设计决策何时用纯虚何时用虚函数纯虚函数 ( 0)当你定义一个接口并且强制所有具体的派生类都必须提供自己的实现时使用。这使基类成为抽象类无法实例化。它明确了“做什么”但把“怎么做”完全交给派生类。虚函数当你提供一个默认实现并且允许但不强制派生类根据需要进行特化或扩展时使用。危险在于派生类可能无意中继承了不合适的默认行为。一个经典的“默认实现”陷阱假设有一个Bird基类它有一个虚函数fly()提供默认实现比如拍打翅膀。然后你创建了Penguin企鹅类继承Bird。企鹅不会飞但因为它继承了fly()的默认实现编译器不会报错只有在逻辑上或运行时才会出现问题。这就是糟糕的默认实现。安全提供默认实现的模式为了避免上述陷阱同时避免在每个派生类中重复编写相似的代码可以采用“纯虚函数默认实现函数”的模式。class Bird { public: virtual void fly() 0; // 纯虚强制派生类声明 protected: void defaultFly() { // 保护的默认实现不是接口的一部分 std::cout Flapping wings to fly.\n; } }; class Sparrow : public Bird { public: void fly() override { defaultFly(); // 显式调用默认实现 } }; class Penguin : public Bird { public: void fly() override { // 企鹅不会飞必须提供不同的实现比如游泳或行走 // 如果忘记实现链接器会报错“未定义的引用”因为fly()是纯虚的。 std::cout Sorry, I cant fly. I swim.\n; } };这样需要默认行为的派生类Sparrow可以显式调用而不需要或不能使用默认行为的派生类Penguin则必须自己实现如果忘记会在编译期或链接期发现错误而不是在运行时产生逻辑错误。3.2 接口类与实现完全分离在严格的设计中特别是大型项目或库的开发中我们追求接口与实现的完全分离。这催生了纯接口类的概念。一个纯接口类通常具有以下特征所有成员函数都是纯虚函数 0。没有非静态成员变量。通常有一个虚析构函数且是唯一的非纯虚函数可能提供默认实现。所有成员都是public的。class ISerializable { // 通常以大写I开头表示接口 public: virtual ~ISerializable() default; // 虚析构函数保证通过接口指针删除对象正确 virtual void serialize(std::ostream out) const 0; virtual void deserialize(std::istream in) 0; // 没有数据成员 }; class MyData : public ISerializable { private: int value_; std::string name_; public: void serialize(std::ostream out) const override { out value_ name_; } void deserialize(std::istream in) override { in value_ name_; } };使用纯接口类的好处是依赖倒置和最小化编译依赖。客户端代码只依赖于稳定的接口ISerializable而不依赖于易变的实现MyData。当MyData的实现改变时只需要重新编译MyData.cpp和直接链接它的模块而所有只包含ISerializable头文件的模块都无需重新编译。4. 组合Composition更灵活的“有一个”关系当类之间的关系不是“is-a”而是“has-a”或“uses-a”时组合是比继承更自然、更灵活的选择。组合意味着将一个类的对象作为另一个类的成员变量。4.1 组合 vs 继承如何选择让我们通过一个具体案例来分析。假设我们有一个Engine引擎类和一个Car汽车类。使用继承不推荐class Engine { public: void start() { /* ... */ } void stop() { /* ... */ } }; class Car : public Engine { // Car is-an Engine? 这很奇怪 public: void drive() { start(); // 可以直接调用语法上没问题 // ... 驾驶逻辑 } };问题很明显汽车“是一个”引擎吗不汽车“有一个”引擎。用继承来表达“has-a”关系是语义错误它错误地让Car继承了Engine的所有接口比如Car对象可以直接调用stop()这不合逻辑也限制了Car的设计比如一辆车不能有多个引擎。使用组合推荐class Engine { /* 同上 */ }; class Car { private: Engine engine_; // 组合Car has-an Engine // std::vectorEngine engines_; // 甚至可以拥有多个引擎 public: void drive() { engine_.start(); // 通过成员对象调用其功能 // ... 驾驶逻辑 } // Car 不会暴露 Engine 的 stop 等无关接口封装性更好。 };组合的优势语义正确清晰表达了“有一个”的关系。封装性好Car可以控制对外暴露哪些Engine的功能甚至可以完全不暴露。灵活性高可以在运行时更换Engine通过指针或引用成员可以拥有多个Engine可以延迟初始化Engine。降低耦合Car和Engine是独立的类修改Engine的内部实现不太会影响Car。4.2 组合的实践委托与策略模式组合不仅仅是包含一个对象更是一种强大的设计工具可以实现委托Delegation和策略模式Strategy Pattern。委托示例class Printer { // 被委托者 public: void printDocument(const std::string doc) { std::cout Printing: doc std::endl; // 实际的打印逻辑可能很复杂 } }; class Computer { // 委托者 private: Printer printer_; // 组合 public: void print(const std::string doc) { // 计算机将打印任务委托给打印机对象 printer_.printDocument(doc); } // Computer 可能还有其他与打印无关的功能 };策略模式示例运行时多态// 策略接口 class CompressionStrategy { public: virtual ~CompressionStrategy() default; virtual std::vectorchar compress(const std::vectorchar data) 0; }; // 具体策略 class ZipCompression : public CompressionStrategy { std::vectorchar compress(const std::vectorchar data) override { /* ZIP算法 */ } }; class RarCompression : public CompressionStrategy { std::vectorchar compress(const std::vectorchar data) override { /* RAR算法 */ } }; // 上下文使用策略 class FileArchiver { private: std::unique_ptrCompressionStrategy strategy_; // 组合一个策略指针 public: void setStrategy(std::unique_ptrCompressionStrategy strat) { strategy_ std::move(strat); } void archive(const std::string filename) { auto data readFile(filename); auto compressed strategy_-compress(data); // 委托给策略对象 writeFile(filename .arc, compressed); } };FileArchiver通过组合一个CompressionStrategy指针可以在运行时动态切换压缩算法而不需要修改FileArchiver本身的代码。这是组合与接口继承结合的强大威力。5. 多重继承与菱形继承难题C允许一个类从多个基类继承即多重继承。这带来了强大的表达能力但也引入了著名的“菱形继承”问题。5.1 多重继承的基本使用与名字冲突class Scanner { public: void scan() { std::cout Scanning...\n; } }; class Printer { public: void print() { std::cout Printing...\n; } }; class AllInOneMachine : public Scanner, public Printer { public: void copy() { scan(); print(); // 可以调用两个基类的方法 } };这看起来很直观。但当两个基类有同名的成员时就会产生二义性。class A { public: void func() { std::cout A::func\n; } }; class B { private: // 注意这里是private void func() { std::cout B::func\n; } }; class C : public A, public B { }; int main() { C c; // c.func(); // 错误对‘func’的请求有歧义 c.A::func(); // 正确使用作用域解析运算符指定 // c.B::func(); // 错误B::func()是private的 }编译器在查找c.func()时会先在C的作用域内找没找到然后会去所有基类A和B的作用域中查找。它同时找到了A::func和B::func此时就产生了歧义。访问控制public/private是在名字查找和重载决议之后才检查的所以即使B::func是private的也依然参与了名字查找并导致了歧义。解决方法是使用作用域解析运算符::显式指定。5.2 菱形继承与虚继承菱形继承是多重继承中的一个棘手问题。class File { public: std::string name; void open() { /* ... */ } }; class InputFile : public File { /* 增加读功能 */ }; class OutputFile : public File { /* 增加写功能 */ }; class IOFile : public InputFile, public OutputFile { /* 既能读又能写 */ };现在IOFile对象中有两个File子对象一个来自InputFile路径一个来自OutputFile路径。这会导致数据冗余IOFile对象中有两份name成员。二义性调用ioFile.open()或访问ioFile.name时编译器不知道你指的是哪个File子对象中的成员。虚继承Virtual Inheritance就是为了解决这个问题而引入的。它保证在继承体系中虚基类被virtual关键字继承的基类无论在整个继承层次中出现多少次在最终的派生类对象中都只存在一个共享的实例。class File { /* 同上 */ }; class InputFile : virtual public File { /* ... */ }; // 虚继承 class OutputFile : virtual public File { /* ... */ }; // 虚继承 class IOFile : public InputFile, public OutputFile { /* ... */ };现在IOFile对象中只有一个File子对象。InputFile和OutputFile共享这个子对象。访问ioFile.name或ioFile.open()不再有二义性。虚继承的代价与注意事项性能开销虚继承通常通过指针虚基类指针来实现共享这增加了间接寻址的开销可能影响性能。初始化责任在非虚继承中每个派生类负责初始化其直接基类。在虚继承中最底层的派生类最终派生类负责初始化虚基类。中间层的派生类如InputFile对虚基类File的初始化会被忽略。class File { public: File(const std::string n) : name(n) {} std::string name; }; class InputFile : virtual public File { public: InputFile(const std::string n) : File(n .in) {} // 这个初始化可能被忽略 }; class OutputFile : virtual public File { public: OutputFile(const std::string n) : File(n .out) {} // 这个初始化可能被忽略 }; class IOFile : public InputFile, public OutputFile { public: // 最终派生类必须负责初始化虚基类File IOFile(const std::string n) : File(n), // 必须显式初始化虚基类 InputFile(), // 传递给InputFile的File初始化被忽略 OutputFile() // 传递给OutputFile的File初始化被忽略 {} };设计复杂虚继承破坏了继承的局部性使得类的初始化顺序和内存布局变得反直觉。许多编码规范如Google C Style Guide明确禁止使用多重继承或者禁止使用虚继承。最佳实践建议优先使用组合绝大多数“多重继承”的场景都可以通过组合多个成员对象来更好地实现。如果必须用多重继承尝试模仿Java或C#的接口继承。即至多有一个基类是包含实现和数据成员的“主基类”其他基类都应该是纯接口类只有纯虚函数没有数据成员。这样可以有效避免菱形继承和数据冗余问题。避免虚继承除非你完全理解其所有含义和开销并且有非常充分的理由否则不要使用虚继承。6. 高级模式Mixin与CRTP编译期多态当我们讨论组合多个正交功能时继承和组合的简单使用有时会显得力不从心。这时我们可以借助模板和一种称为Mixin或CRTP的惯用法。6.1 问题组合多个正交功能假设我们有一个基础任务接口ITask现在我们想为任务动态地添加一些横切关注点功能比如日志Logging和计时Timing。我们希望这些功能模块是独立的、可插拔的。传统继承的局限如果通过继承链ITask-LoggingTask-TimingTask功能之间紧密耦合TimingTask无法单独使用且无法灵活组合。组合的局限使用代理模式LoggingTask持有一个ITask*可以解耦但引入了运行时多态和堆分配的开销并且LoggingTask需要实现所有ITask接口即使只是转发代码臃肿。6.2 Mixin解决方案使用模板进行编译期组合Mixin的核心思想是通过模板参数将功能“混合”进一个类中。每个Mixin都是一个小的、独立的类模板它通过继承其模板参数来增强功能。// 基础任务类假设不是接口有具体实现 class MyTask { public: void execute() { std::cout Executing MyTask...\n; // 实际任务逻辑 } std::string getName() const { return MyTask; } }; // Mixin: 计时功能 template typename Base class TimingMixin : public Base { public: void execute() { auto start std::chrono::high_resolution_clock::now(); Base::execute(); // 调用基类即被增强的类的execute auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::milliseconds(end - start); std::cout Base::getName() took duration.count() ms.\n; } // 继承了Base的getName等方法 }; // Mixin: 日志功能 template typename Base class LoggingMixin : public Base { public: void execute() { std::cout [LOG] Starting execution of Base::getName() std::endl; Base::execute(); std::cout [LOG] Finished execution of Base::getName() std::endl; } }; // 使用Mixin进行灵活组合 using MyTaskWithLogging LoggingMixinMyTask; using MyTaskWithTiming TimingMixinMyTask; using MyTaskWithLoggingAndTiming LoggingMixinTimingMixinMyTask; // 顺序可换using MyTaskWithTimingAndLogging TimingMixinLoggingMixinMyTask; int main() { MyTaskWithLoggingAndTiming task; task.execute(); // 输出 // [LOG] Starting execution of MyTask // Executing MyTask... // MyTask took X ms. // [LOG] Finished execution of MyTask }Mixin的优势零开销抽象所有调用在编译期确定没有虚函数开销编译器可以进行充分的内联优化。功能正交与可组合TimingMixin和LoggingMixin彼此完全独立可以以任意顺序组合也可以单独使用。类型安全生成的是具体类型不是通过指针的运行时多态。代码复用Mixin模板可以应用于任何具有execute()和getName()方法的类复用性极高。6.3 CRTP奇特的递归模板模式CRTP是Mixin的一种特殊形式其中派生类将自己作为模板参数传递给基类。这允许基类在编译时使用派生类的类型信息。// 基类模板CRTP template typename Derived class Comparable { public: // 基类可以提供基于派生类operator的默认operator! bool operator!(const Derived other) const { return !(static_castconst Derived(*this) other); } }; // 派生类 class MyValue : public ComparableMyValue { // 将自己作为模板参数传入 private: int value; public: MyValue(int v) : value(v) {} // 只需要实现operator bool operator(const MyValue other) const { return value other.value; } // operator! 自动从ComparableMyValue继承而来 }; int main() { MyValue a(5), b(5), c(10); std::cout (a b) std::endl; // true, 调用MyValue::operator std::cout (a ! c) std::endl; // true, 调用ComparableMyValue::operator! // 注意a ! b 调用的是从Comparable继承来的operator! }在CRTP中基类Comparable通过static_castconst Derived(*this)将自己转换到派生类类型从而可以调用派生类的方法这里是operator。这实现了编译期的“静态多态”也是一种强大的代码复用技术常用于实现各种编译期策略、扩展接口等。注意事项Mixin和CRTP都是高级模板技术它们提高了代码的灵活性和性能但也增加了编译器的负担和代码的复杂度可能会使错误信息难以阅读。建议在性能关键、且需要高度灵活组合功能的场景下使用并在团队中确保大家对这种模式有共识。7. 常见问题与排查技巧实录在实际项目中应用继承和组合时会遇到各种各样的问题。这里记录一些典型坑点和排查思路。7.1 切片问题Object Slicing这是值语义和继承结合时的一个经典错误。class Base { public: virtual void print() const { std::cout Base\n; } int data 10; }; class Derived : public Base { public: void print() const override { std::cout Derived\n; } int extra 20; }; void funcByValue(Base b) { // 按值传递 b.print(); // 总是调用Base::print()即使传入的是Derived对象 } int main() { Derived d; funcByValue(d); // 发生切片d的Derived部分extra和重写的虚表被“切掉”了。 Base b d; // 同样发生切片 }现象派生类对象被赋值给基类对象或按值传递给基类参数时派生类特有的部分会丢失虚函数表指针也会被重置为基类的。排查当多态行为失效或者派生类成员数据丢失时检查是否无意中进行了按值传递或赋值。解决总是通过指针或引用来传递多态对象。使用Base或Base*最好是智能指针std::unique_ptrBase或std::shared_ptrBase。7.2 继承与默认成员函数如果你没有显式定义编译器会为类生成默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。在继承体系中这些函数的生成和调用有特定规则。问题示例忘记虚析构函数class Base { public: ~Base() { std::cout Base dtor\n; } // 非虚析构函数 }; class Derived : public Base { public: ~Derived() { std::cout Derived dtor\n; } int* ptr new int(100); }; int main() { Base* p new Derived(); delete p; // 未定义行为只调用了~Base()没有调用~Derived()导致内存泄漏ptr没被delete。 }解决基类的析构函数必须声明为虚函数virtual ~Base() default;。这样通过基类指针删除派生类对象时会正确调用整个继承链上的析构函数。问题示例拷贝控制与继承class Base { public: Base() default; Base(const Base) { std::cout Base copy ctor\n; } Base operator(const Base) { std::cout Base copy assign\n; return *this; } }; class Derived : public Base { public: // 编译器会为Derived生成默认的拷贝构造函数和拷贝赋值运算符。 // 它们会自动调用基类对应的拷贝控制成员。 Derived() default; // 隐式生成的Derived拷贝构造函数会先调用Base的拷贝构造函数。 // 隐式生成的Derived拷贝赋值运算符会先调用Base的拷贝赋值运算符。 }; int main() { Derived d1; Derived d2 d1; // 输出Base copy ctor (来自隐式生成的Derived拷贝构造函数) d1 d2; // 输出Base copy assign (来自隐式生成的Derived拷贝赋值运算符) }关键点派生类的拷贝/移动构造函数会先调用基类的对应构造函数。派生类的拷贝/移动赋值运算符会先调用基类的对应赋值运算符。如果你在派生类中自己定义了这些函数必须显式调用基类的版本否则基类部分会被默认初始化或保持不变。class Derived : public Base { public: Derived(const Derived other) : Base(other) { /* 派生类特有成员的拷贝 */ } // 正确 Derived operator(const Derived other) { if (this ! other) { Base::operator(other); // 显式调用基类赋值运算符 // 派生类特有成员的赋值 } return *this; } };7.3 访问控制与using声明有时你希望改变继承来的成员在派生类中的访问权限。class Base { protected: void internalHelper() { /* ... */ } public: void publicMethod() { /* ... */ } }; class Derived : private Base { // private继承所有Base的public/protected成员在Derived中都变成private public: // 使用using声明将Base::publicMethod在Derived中恢复为public访问权限 using Base::publicMethod; // 错误using声明不能提升访问权限从protected到public在这里是允许的因为Derived是Base的派生类 // using Base::internalHelper; // 如果放在public区域则internalHelper在Derived中变为public protected: using Base::internalHelper; // 正确将internalHelper在Derived中声明为protected };using声明在这里非常有用特别是在private或protected继承中可以有选择地将基类的部分接口暴露给派生类的用户。7.4 设计决策速查表当你面临“用继承还是组合”的选择时可以问自己以下问题问题倾向于继承倾向于组合新类与旧类的关系是“是一个is-a”吗是(Public继承)否你需要重写基类的虚函数吗是否组合无法直接重写你需要基类的protected成员吗是通常否组合无法访问成员对象的protected成员你需要空基类优化EBO吗是(Private继承)否你需要在运行时动态切换或拥有多个“部分”吗否是你希望隐藏基类的部分接口吗否Public继承会暴露所有public接口是你的类会是多重继承体系的一部分吗小心使用更安全记住一个经典的设计原则“优先使用对象组合而不是类继承”Favor composition over inheritance。组合通常能提供更好的封装性、更低的耦合度和更高的灵活性。继承尤其是public继承应该被用于严格建模“is-a”关系并且要确保符合里氏替换原则。