C++的继承是其面向对象编程(OOP)的核心特性之一,它允许我们创建基于现有类(基类)的新类(派生类)。这种机制促进了代码重用和层次化设计。然而,在定义继承关系时,我们使用的访问模式(
public
、protected
、private
)对派生类的行为,特别是与基类之间的类型转换,有着至关重要的影响。理解这些细微差别是编写健壮、可维护C++代码的关键。继承语法中的访问模式
接触学习C++面向对象编程的同学一定见到这样的语法
class Derived: public Base
,其中位于基类名之前的关键字public
就是显示指定的继承访问模式。这个模式决定了基类中的成员(public
、protected
和private
)在派生类中将拥有怎样的访问权限,同时也影响着派生类与基类关系的语义。public
继承:- 访问规则:
- 基类的
public
成员在派生类中仍然是public
。 - 基类的
protected
成员在派生类中仍然是protected
。 - 基类的
private
成员在派生类中不可直接访问(但它们仍然是派生类对象的一部分)。 - 含义:这表示一个 "is-a" (是一个)的关系。派生类对象 是 一个基类对象,并且可以被用在任何期望使用基类对象的地方。这是最常用的继承方式。
protected
继承:- 访问规则:
- 基类的
public
成员在派生类中变为protected
。 - 基类的
protected
成员在派生类中仍然是protected
。 - 基类的
private
成员在派生类中不可直接访问。 - 含义:这表示一种更受限制的 "is-implemented-in-terms-of" (依据...来实现)主要关注于实现细节的复用,而不是公共接口的继承。
private
继承:- 访问规则:
- 基类的
public
成员在派生类中变为private
。 - 基类的
protected
成员在派生类中变为private
。 - 基类的
private
成员在派生类中不可直接访问。 - 含义:这也表示 "is-implemented-in-terms-of"。派生类纯粹是为了重用基类的实现,而不希望基类的接口成为派生类公共接口的一部分。基类接口对派生类的用户是隐藏的。
值得注意的是,如果在继承时没有显式指定访问模式(例如,
class Derived : Base;
),C++会根据派生类的定义方式提供一个默认模式:- 如果派生类是用
class
关键字定义的 (例如class Derived : Base
), 则默认为private
继承。
- 如果派生类是用
struct
关键字定义的 (例如struct Derived : Base
), 则默认为public
继承。
了解这一点很重要,因为它可能导致意外的行为,特别是当期望
public
继承(这是 "is-a" 关系最常见的情况)时,却不小心得到了 private
继承,从而限制了基类接口的继承和向上转型的能力。总结表:
基类成员访问权限 | public 继承后 | protected 继承后 | private 继承后 |
public | public | protected | private |
protected | protected | protected | private |
private | 不可访问 | 不可访问 | 不可访问 |
代码示例:访问模式的影响
让我们通过一个例子来看看这些模式是如何工作的。
#include <iostream> #include <string> class Base { public: std::string public_member = "Base_Public"; void public_method() { std::cout << "Base::public_method()" << std::endl; } protected: std::string protected_member = "Base_Protected"; void protected_method() { std::cout << "Base::protected_method()" << std::endl; } private: std::string private_member = "Base_Private"; void private_method() { std::cout << "Base::private_method()" << std::endl; } }; // Public Inheritance class DerivedPublic : public Base { public: void access_base_members() { std::cout << "DerivedPublic accessing:" << std::endl; std::cout << " " << public_member << std::endl; // 正确 public_method(); // 正确 std::cout << " " << protected_member << std::endl; // 正确 protected_method(); // 正确 // std::cout << private_member << std::endl; // 错误:在 Base 中是 private 成员 // private_method(); // 错误:在 Base 中是 private 成员 } }; // Protected Inheritance class DerivedProtected : protected Base { public: void access_base_members() { std::cout << "DerivedProtected accessing:" << std::endl; std::cout << " " << public_member << std::endl; // 正确 (变为 protected) public_method(); // 正确 (变为 protected) std::cout << " " << protected_member << std::endl; // 正确 (保持 protected) protected_method(); // 正确 (保持 protected) } }; // Private Inheritance class DerivedPrivate : private Base { public: void access_base_members() { std::cout << "DerivedPrivate accessing:" << std::endl; std::cout << " " << public_member << std::endl; // 正确 (变为 private) public_method(); // 正确 (变为 private) std::cout << " " << protected_member << std::endl; // 正确 (变为 private) protected_method(); // 正确 (变为 private) } }; class GrandChildOfProtected : public DerivedProtected { public: void access_base_via_derived_protected() { std::cout << "GrandChildOfProtected accessing:" << std::endl; // public_member 在 Base 中是 public,在 DerivedProtected 中变为 protected std::cout << " " << public_member << std::endl; // 正确 public_method(); // 正确 // protected_member 在 Base 中是 protected,在 DerivedProtected 中保持 protected std::cout << " " << protected_member << std::endl; // 正确 protected_method(); // 正确 } }; int main() { std::cout << "--- DerivedPublic Demo ---" << std::endl; DerivedPublic d_pub; d_pub.access_base_members(); std::cout << "Accessing from main: " << d_pub.public_member << std::endl; // 正确 d_pub.public_method(); // 正确 // std::cout << d_pub.protected_member << std::endl; // 错误:protected 成员 // d_pub.protected_method(); // 错误:protected 成员 std::cout << "\\n--- DerivedProtected Demo ---" << std::endl; DerivedProtected d_prot; d_prot.access_base_members(); // std::cout << d_prot.public_member << std::endl; // 错误:public_member 在 DerivedProtected 中现在是 protected 的 // d_prot.public_method(); // 错误:public_method 在 DerivedProtected 中现在是 protected 的 GrandChildOfProtected gc_prot; gc_prot.access_base_via_derived_protected(); std::cout << "\\n--- DerivedPrivate Demo ---" << std::endl; DerivedPrivate d_priv; d_priv.access_base_members(); // std::cout << d_priv.public_member << std::endl; // 错误:public_member 在 DerivedPrivate 中现在是 private 的 // d_priv.public_method(); // 错误:public_method 在 DerivedPrivate 中现在是 private 的 return 0; }
解释:
DerivedPublic
: 基类的public
成员对外部仍然可见。
DerivedProtected
: 基类的public
和protected
成员在DerivedProtected
内部及其派生类(如GrandChildOfProtected
)中是protected
的,但对外部不可见。
DerivedPrivate
: 基类的public
和protected
成员在DerivedPrivate
内部是private
的,对外部和其派生类都不可见。
访问模式与类型转换 (向上转型 Upcasting)
向上转型是指将派生类对象的指针或引用转换为基类类型的指针或引用。这是C++多态性的基础。访问模式对这种转换的可行性有直接影响。
public
继承:- 规则:始终允许从派生类到基类的隐式向上转型。
- 原因:因为派生类 "is-a" 基类,并且公开继承了基类的接口。
- 示例:
DerivedPublic dp; Base* b_ptr_pub = &dp; // 正确 Base& b_ref_pub = dp; // 正确 b_ptr_pub->public_method();
protected
继承:- 规则:向上转型仅在派生类自身、其友元以及从它派生的类的成员函数中被允许。在这些上下文之外,通常不允许隐式向上转型。
- 原因:基类的公共接口在派生类中变成了
protected
,不应被外部代码视为一个完整的基类。
- 示例:
class DerivedProtectedUser { public: void use_derived_protected(DerivedProtected& d_prot) { // 位于可能为友元或相关上下文的类内部(用于说明) // 然而,在派生类层次结构之外进行标准向上转型是比较复杂的。 // Base* b_ptr = &d_prot; // 通常在 DerivedProtected 或其子类之外会是错误 // 或友元。 } }; class DerivedProtected : protected Base { public: // ... (前面定义的 access_base_members) void convert_to_base_internally() { Base* b_ptr = this; // 正确:在 DerivedProtected 内部 b_ptr->public_method(); // 这个调用比较微妙:public_method 现在是 protected 的。 // 如果对象 *是* Base 类型,则可以通过 Base 指针访问。 // 这里,b_ptr 指向 DerivedProtected,其中 Base::public_method 是 protected 的。 // 此处的访问权限取决于 *此代码块* 的上下文。 // 由于 public_method 在 DerivedProtected 中现在是 protected 的,这应该是可以的 // 因为我们位于 DerivedProtected 内部。 std::cout << "Converted to Base* internally from DerivedProtected." << std::endl; // 已在 DerivedProtected 内部转换为 Base*。 } void call_base_public_method_via_base_ptr() { Base* basePtr = this; // 正确,在 DerivedProtected 内部允许转换 // basePtr->public_method(); // 如果通过 Base* 从 main 调用,这就不可以, // 因为 public_method 在 DerivedProtected 的接口中是 protected 的。 // 然而,通过一个指向 Base 对象的真正的 Base 指针,它是 public 的。 // 关键在于名称查找和访问控制是如何解析的。 // 通过 Base* 访问一个 *变为* protected 的方法 // 通常在派生类层次结构之外是受限的。 // 由于 Base 的所有 public 方法在这里都变成了 protected, // 我们不能通过 Base* 将它们作为 'public' 直接调用 // 这从 DerivedProtected 的 *外部* 角度来看是这样。 // 但在内部,这就像调用一个 protected 成员。 protected_method(); // 作为 protected 成员可直接访问。 // public_method(); // 同样可访问,因为它变成了 protected。 } }; class GrandChildOfProtected : public DerivedProtected { public: void convert_to_base_from_grandchild() { Base* b_ptr = this; // 正确:在 DerivedProtected 的子类内部 b_ptr->public_method(); // 与上面的访问考量相同。 std::cout << "Converted to Base* internally from GrandChildOfProtected." << std::endl; // 已在 GrandChildOfProtected 内部转换为 Base*。 } }; // In main: // DerivedProtected d_prot_main; // Base* b_ptr_prot = &d_prot_main; // 编译错误!无法将 DerivedProtected 转换为 Base (在上下文之外) // Base& b_ref_prot = d_prot_main; // 编译错误!
问题:如果你期望在通用代码中使用指向基类的指针来处理
DerivedProtected
对象,你会遇到编译错误,因为这种转换不是普遍允许的。这限制了多态性的应用。private
继承:- 规则:向上转型仅在派生类自身及其友元的成员函数中被允许。在这些上下文之外,不允许隐式向上转型。
- 原因:基类的接口完全对派生类的用户隐藏。派生类不是一个(公开的)基类。
- 示例:
class DerivedPrivate : private Base { public: // ... (前面定义的 access_base_members) void convert_to_base_internally() { Base* b_ptr = this; // 正确:在 DerivedPrivate 内部 b_ptr->public_method(); // 访问考量:public_method 现在是 private 的 // 对于 DerivedPrivate 而言。通过 Base* 调用它 // 取决于上下文以及名称查找如何发生。 // 本质上,你是在调用自己类的一个 private 成员。 std::cout << "Converted to Base* internally from DerivedPrivate." << std::endl; // 已在 DerivedPrivate 内部转换为 Base*。 } }; // In main: // DerivedPrivate d_priv_main; // Base* b_ptr_priv = &d_priv_main; // 编译错误!无法将 DerivedPrivate 转换为 Base // Base& b_ref_priv = d_priv_main; // 编译错误!
问题:与
protected
继承类似,但限制更强。这几乎完全阻止了将派生类对象用作基类对象的多态行为。何时使用哪种访问模式?
public
继承:- 用例:当你能明确地说 "派生类 是一个 基类" 时 (Liskov替换原则)。这是实现多态和建立清晰类型层次结构的标准方式。
- 例子:
Dog
is anAnimal
,Square
is aShape
.
protected
继承:- 用例:不常见。当你希望派生类及其后续派生类能够重用基类的实现,并且能够访问基类的
public
和protected
成员作为其自身的protected
成员,但你不希望派生类的外部用户将派生类视为基类。 - 思考:这通常意味着基类的接口对于派生类来说是实现细节的一部分,而不是其公共契约的一部分,但这个实现细节也想暴露给下一层派生。
private
继承:- 用例:"is-implemented-in-terms-of"。当你纯粹想重用基类的代码,而不希望派生类继承基类的接口时。基类对派生类的用户是完全不可见的。
- 替代方案:通常,组合优于私有继承。如果你仅仅想重用功能,将基类的一个实例作为派生类的成员变量通常是更清晰、更灵活的选择。
class MyClassWithReusableFeature { private: Base feature_provider; // 组合 public: void do_something() { feature_provider.public_method(); // 使用该特性 } };
不合适的访问模式带来的问题
选择不合适的继承访问模式,尤其是
protected
或 private
继承,当你的意图是建立一个 "is-a" 关系时,会导致以下问题:破坏多态性:如上所述,
protected
和 private
继承限制或阻止了从派生类到基类的向上转型。这意味着你不能轻易地将派生类对象传递给期望基类指针或引用的函数,从而失去了多态调用的能力。在这里说一个本人实际工作中踩过的坑:
class MyException : std::exception {//省略...}
即自定义了一个异常类,该类继承于
std::exception
,但是在定义继承时没有显示指定访问模式为public
,抛出该类型异常后,在捕获异常时的catch
语句中使用std::exception&
类型参数没有捕获到该异常。原因即是对于class
关键字定义的派生类,在继承时没有显示指定访问模式为public
,其默认模式就是private
,结果该派生类MyException
在这里不被认为是一个std::exception
基类,无法被捕获。违反 Liskov 替换原则 (LSP):LSP指出,如果S是T的一个子类型,那么程序中类型T的对象应该可以用类型S的对象替换,而不会改变程序的任何期望属性。
public
继承旨在支持LSP。protected
和 private
继承通常违反了这一点,因为派生类不能无缝替换基类。接口不明确:使用
private
或 protected
继承,派生类的公共接口不再是基类公共接口的超集(或相同集合)。这给开发者带来使用时的心智负担。最佳实践
- 默认为
public
继承:当你想要一个 "is-a" 关系并利用多态性时,始终使用public
继承。这是最直观和最常用的方式。
- 优先考虑组合:对于 "has-a" 或 "is-implemented-in-terms-of" 的关系,如果不需要访问基类的
protected
成员,或者不希望建立类型层次结构,优先使用对象组合而不是private
(甚至protected
) 继承。组合通常更灵活,耦合更松。
- 谨慎使用
protected
继承。
private
继承用于实现细节:如果确实需要访问基类的protected
成员来实现派生类,并且不希望暴露基类接口,private
继承是一个选项。但最好还是先考虑使用组合。
总结
C++中的继承访问模式是控制类之间关系的强大工具。
public
继承支持 "is-a" 关系和多态性,是构建类型层次结构的首选。protected
和 private
继承则服务于实现重用,但它们限制了派生类到基类的类型转换,从而影响了多态行为。理解这些差异并根据设计的意图选择正确的访问模式,对于编写清晰、可维护且符合面向对象原则的C++代码至关重要。在大多数情况下,当我们想继承时,可能真正需要的是 public
继承。对于其他情况,需权衡并考虑组合作为替代方案。参考链接
版权声明:本文为作者beaclnd的原创文章,遵循版权协议署名-非商业性使用-禁止演绎 4.0 (CC BY-NC-ND 4.0) ,若转载请附上原文出处链接和本声明。