在前面的课程中,你已初步了解了基类继承的工作机制。迄今为止,我们所有示例都采用公有继承(public inheritance),即派生类以公有方式继承基类。
本文将深入探讨公有继承,并介绍另外两种继承方式——私有继承(private inheritance)和受保护继承(protected inheritance)。同时,我们也将研究不同类型的继承与各类访问说明符之间的相互作用,从而决定成员的可访问性。
截至目前,你已见过 private 与 public 两种访问说明符,它们决定了类成员可被谁访问。简要回顾:public 成员可被任何代码访问;private 成员只能被本类成员函数或友元访问。这也意味着派生类无法直接访问基类的 private 成员!
class Base
{
private:
int m_private {}; // 仅可由 Base 的成员及友元访问(派生类不可)
public:
int m_public {}; // 可被任何代码访问
};
上述规则简洁明了,应当已为你所熟悉。
受保护访问说明符(protected)
当涉及继承层次时,情况会稍显复杂。
C++ 还提供了第三种访问说明符 protected,此前未予讨论,因为它只在继承语境下才有意义。protected 成员允许所属类自身、其友元以及派生类访问,但禁止类外代码直接访问。
class Base
{
public:
int m_public {}; // 可被任何代码访问
protected:
int m_protected {}; // 可被 Base 成员、友元及派生类访问
private:
int m_private {}; // 仅可被 Base 成员及友元访问(派生类不可)
};
class Derived: public Base
{
public:
Derived()
{
m_public = 1; // 合法:在派生类中可访问 public 基类成员
m_protected = 2; // 合法:在派生类中可访问 protected 基类成员
m_private = 3; // 非法:无法直接访问基类 private 成员
}
};
int main()
{
Base base;
base.m_public = 1; // 合法:类外可访问 public 成员
base.m_protected = 2; // 非法:类外不可访问 protected 成员
base.m_private = 3; // 非法:类外不可访问 private 成员
return 0;
}
以上示例可见,基类的 protected 成员 m_protected
可被派生类直接访问,但类外代码则不可。
何时应使用 protected 访问说明符?
若基类的某属性声明为 protected,则派生类可直接访问该属性。一旦将来你对该 protected 属性做任何修改(如类型、语义等),很可能需要同时修改基类及所有派生类。
因此,protected 访问说明符最适用于以下场景:
- 你(或你的团队)计划从自身编写的类派生新类;
- 派生类数量有限。
这样,当基类实现发生变更、需要同步更新派生类时,你可以亲自完成这些调整,且工作量可控(因派生类数量少)。
若将成员设为 private,则类外及派生类均无法直接修改基类实现,这有利于隔离实现细节、保证不变式成立。但代价是,你可能需要为 public 或 protected 接口提供更多函数,从而增加构建、测试与维护成本。
一般而言,能设为 private 就优先设为 private;仅当确实计划派生,且为 private 成员构建接口成本过高时,才考虑使用 protected。
最佳实践:优先使用 private 成员,而非 protected 成员。
不同的继承方式及其影响
类继承有三种方式:public、protected 与 private。只需在继承列表中显式指明即可:
// 公有继承 Base
class Pub: public Base
{
};
// 受保护继承 Base
class Pro: protected Base
{
};
// 私有继承 Base
class Pri: private Base
{
};
class Def: Base // 未指明时默认为私有继承
{
};
若未显式指定继承方式,C++ 默认为 private 继承(与成员默认 private 访问一致)。
由此共产生 9 种组合:3 种成员访问说明符(public、private、protected)× 3 种继承方式(public、private、protected)。
区别何在?简言之,成员被继承后,其在派生类中的访问级别可能因继承方式而异;换言之,基类成员在派生类中的可见性会发生变化。
继续阅读时请牢记以下规则:
- 类总能访问自身的(非继承)成员;
- 类外代码依据所访问类的访问说明符决定能否访问;
- 派生类访问继承而来的成员时,取决于继承方式与基类成员的访问说明符。
公有继承(public inheritance)
公有继承是使用最广泛的继承方式,几乎在所有场景下都应首选。规则最简单:
- 基类的 public 成员在派生类中仍为 public;
- 基类的 protected 成员在派生类中仍为 protected;
- 基类的 private 成员在派生类中仍不可访问。
基类访问说明符 | 公有继承后的访问级别 |
---|---|
public | public |
protected | protected |
private | 不可访问 |
示例:
class Base
{
public:
int m_public {};
protected:
int m_protected {};
private:
int m_private {};
};
class Pub: public Base // 注意:公有继承
{
// 公有继承意味着:
// 继承的 public 成员仍为 public(m_public 视为 public)
// 继承的 protected 成员仍为 protected(m_protected 视为 protected)
// 继承的 private 成员仍不可访问(m_private 不可访问)
public:
Pub()
{
m_public = 1; // 合法:m_public 在 Pub 中为 public
m_protected = 2; // 合法:m_protected 在 Pub 中为 protected
m_private = 3; // 非法:派生类无法访问基类 private 成员
}
};
int main()
{
// 类外访问依据被访问类的访问说明符
Base base;
base.m_public = 1; // 合法:m_public 在 Base 中为 public
base.m_protected = 2; // 非法:m_protected 在 Base 中为 protected
base.m_private = 3; // 非法:m_private 在 Base 中为 private
Pub pub;
pub.m_public = 1; // 合法:m_public 在 Pub 中为 public
pub.m_protected = 2; // 非法:m_protected 在 Pub 中为 protected
pub.m_private = 3; // 非法:m_private 在 Pub 中不可访问
return 0;
}
该示例与之前引入 protected 的示例一致,只是额外实例化了派生类,以证明公有继承时派生类与基类表现一致。
除非有明确理由,否则应始终使用公有继承。
最佳实践:如无特殊需求,请使用公有继承。
受保护继承(protected inheritance)
受保护继承是最罕见的继承方式,几乎不使用。规则如下:
- 基类的 public 与 protected 成员在派生类中均变为 protected;
- 基类的 private 成员仍不可访问。
基类访问说明符 | 受保护继承后的访问级别 |
---|---|
public | protected |
protected | protected |
private | 不可访问 |
鉴于其罕见性,此处略去示例,仅以上表总结。
私有继承(private inheritance)
采用私有继承时,基类所有成员在派生类中均变为 private:
- public 与 protected 成员变为 private;
- private 成员仍不可访问。
注意:这并不影响派生类自身访问继承成员,而是影响通过派生类对象在外部访问这些成员。
class Base
{
public:
int m_public {};
protected:
int m_protected {};
private:
int m_private {};
};
class Pri: private Base // 注意:私有继承
{
// 私有继承意味着:
// 继承的 public 成员变为 private(m_public 视为 private)
// 继承的 protected 成员变为 private(m_protected 视为 private)
// 继承的 private 成员仍不可访问(m_private 不可访问)
public:
Pri()
{
m_public = 1; // 合法:m_public 在 Pri 中为 private
m_protected = 2; // 合法:m_protected 在 Pri 中为 private
m_private = 3; // 非法:派生类无法访问基类 private 成员
}
};
int main()
{
// 类外访问依据被访问类的访问说明符
Base base;
base.m_public = 1; // 合法:m_public 在 Base 中为 public
base.m_protected = 2; // 非法:m_protected 在 Base 中为 protected
base.m_private = 3; // 非法:m_private 在 Base 中为 private
Pri pri;
pri.m_public = 1; // 非法:m_public 在 Pri 中为 private
pri.m_protected = 2; // 非法:m_protected 在 Pri 中为 private
pri.m_private = 3; // 非法:m_private 在 Pri 中不可访问
return 0;
}
总结如下表:
基类访问说明符 | 私有继承后的访问级别 |
---|---|
public | private |
protected | private |
private | 不可访问 |
当派生类与基类之间没有明显的"is-a"关系,但派生类内部需要复用基类实现时,私有继承可能有用。此时,我们不希望基类的 public 接口通过派生类对象暴露给外部。
实际开发中,私有继承亦很少使用。
综合示例
class Base
{
public:
int m_public {};
protected:
int m_protected {};
private:
int m_private {};
};
- Base 自身可无限制访问所有成员。
- 类外代码仅能访问
m_public
。 - 派生类可访问
m_public
与m_protected
。
class D2 : private Base // 注意:私有继承
{
// 私有继承意味着:
// 继承的 public 成员变为 private
// 继承的 protected 成员变为 private
// 继承的 private 成员仍不可访问
public:
int m_public2 {};
protected:
int m_protected2 {};
private:
int m_private2 {};
};
- D2 自身可无限制访问所有成员。
- D2 可访问 Base 的
m_public
与m_protected
,不能访问m_private
。 - 因 D2 私有继承 Base,
m_public
与m_protected
在 D2 中视为 private,故类外代码或 D2 的派生类均无法通过 D2 对象访问这些变量。
class D3 : public D2
{
// 公有继承意味着:
// 继承的 public 成员仍为 public
// 继承的 protected 成员仍为 protected
// 继承的 private 成员仍不可访问
public:
int m_public3 {};
protected:
int m_protected3 {};
private:
int m_private3 {};
};
- D3 自身可无限制访问所有成员。
- D3 可访问 D2 的
m_public2
与m_protected2
,不能访问m_private2
。 - 因 D3 公有继承 D2,
m_public2
与m_protected2
保持原访问级别。 - D3 无法访问 Base 的
m_private
(已在 Base 中设为 private),也无法访问 Base 的m_protected
与m_public
,因为它们在 D2 私有继承时已变为 private。
总结
访问说明符、继承方式与派生类之间的交互常令人困惑。为尽量厘清:
- 类(及其友元)总能访问自身非继承成员;访问说明符仅影响外部代码及派生类。
- 派生类继承成员后,这些成员在派生类中的访问级别可能变化;这并不影响派生类自身非继承成员,而仅影响外部代码以及"派生类的派生类"对这些继承成员的访问。
全部组合一览表:
基类访问说明符 | 公有继承后的访问级别 | 私有继承后的访问级别 | 受保护继承后的访问级别 |
---|---|---|---|
public | public | private | protected |
protected | protected | private | protected |
private | 不可访问 | 不可访问 | 不可访问 |
最后,尽管上例仅展示了成员变量,上述访问规则对所有成员(成员函数、类内声明的类型等)均适用。