以下是对上述内容的详细解释:
一、继承的概念及定义
(一)继承的概念
- 代码复用手段:继承是面向对象程序设计中实现代码复用的重要手段。它允许在保持原有类特性的基础上进行扩展,产生新的派生类,增加方法和属性,体现了由简单到复杂的认知过程,是类设计层次的复用,区别于函数层次的复用。
- 示例说明:以学生(Student)和老师(Teacher)为例,他们都有一些共同的成员变量(姓名、地址、电话、年龄)和成员函数(身份认证函数
identity
),如果不使用继承,这些相同的部分在两个类中会重复定义,造成冗余。使用继承后,可以将这些公共部分放到一个基类(如Person
)中,学生类和老师类继承自这个基类,从而复用这些成员,避免重复定义。
(二)继承定义
- 定义格式:
Person
是基类(父类),Student
是派生类(子类)。继承方式有public
(公有继承)、protected
(保护继承)、private
(私有继承)。- 使用
class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,但最好显式写出继承方式。 - 在实际运用中一般使用
public
继承,其他两种继承方式使用较少且不提倡,因为其继承下来的成员只能在派生类内部使用,扩展性和维护性不强。
- 继承基类成员访问方式的变化:
- 基类的私有成员在派生类中无论以何种方式继承都是不可见的,虽然被继承到了派生类对象中,但语法上限制了派生类对象在类内和类外都不能访问。
- 如果基类成员不想在类外直接被访问,但需要在派生类中能访问,可以定义为
protected
。 - 基类的私有成员在派生类中不可访问,基类的其他成员在派生类的访问方式取决于“成员在基类的访问限定符”和“继承方式”中的较小者(
public > protected > private
)。
(三)继承类模板
- 作用:通过继承类模板,可以直接调用基类模板的成员函数来完成特定功能。例如,通过继承
vector<T>
模板类来实现stack
栈类,按需实例化vector
的成员函数,避免重复实现栈的基本操作。 - 注意事项:当基类是类模板时,在派生类中调用基类的成员函数需要指定类域,否则可能会编译报错。这是因为模板是按需实例化,未实例化的成员函数可能找不到标识符。
(四)基类和派生类间的转换
- 派生类对象赋值给基类指针/引用:
public
继承的派生类对象可以赋值给基类的指针或引用,这种情况被形象地称为“切片”或“切割”,即把派生类中基类那部分切出来,基类指针或引用指向的是派生出类中切出来的基类那部分。 - 基类对象不能赋值给派生类对象。
- 基类指针/引用转换为派生类指针/引用:基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须是基类的指针指向派生类对象时才是安全的。如果基类是多态类型,可以使用
dynamic_cast
进行安全转换,这个在后面的类型转换章节会专门讲解。
(五)继承中的作用域
- 隐藏规则:
- 在继承体系中,基类和派生类都有独立的作用域。
- 当派生类和基类中有同名成员时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。在派生类成员函数中,可以使用“基类::基类成员”显示访问被隐藏的基类成员。
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。在实际中,在继承体系里最好不要定义同名的成员。
- 考察继承相关选择题:
- 对于两个类中的函数构成的关系,如果只有在同一个类里函数名相同且参数不同才构成重载,在不同类中函数名相同一般构成隐藏。
- 如果程序中没有正确处理成员函数的隐藏问题,可能会导致编译报错。
(六)派生类的默认成员函数
- 生成规则:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的
operator=
必须要调用基类的operator=
完成基类的复制,且由于派生类的operator=
隐藏了基类的operator=
,所以需要显式调用基类的operator=
并指定基类作用域。 - 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员,以保证派生类对象先清理派生类成员再清理基类成员的顺序。因为多态中一些场景析构函数需要构成重写,编译器会对析构函数名进行特殊处理,处理成
destructor()
,所以基类析构函数不加virtual
的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
- 示例说明:以
Person
和Student
类为例,展示了派生类在构造、拷贝构造、赋值操作和析构过程中对基类相应函数的调用。
(七)实现一个不能被继承的类
- 方法一:将基类的构造函数设为私有,派生类必须调用基类的构造函数才能实例化对象,但基类构造函数私有化后,派生类无法调用,也就无法实例化对象。
- 方法二:在 C++11 中,可以使用
final
关键字修饰基类,使得派生类不能继承该基类。
(八)继承与友元
- 友元关系不继承:基类的友元不能访问派生类的私有和保护成员。例如,
Display
是Person
的友元,但不是Student
的友元,所以无法访问Student
的成员。 - 解决方法:如果需要让
Display
访问Student
的成员,可以将Display
也变成Student
的友元。
(九)继承与静态成员
- 特点:基类定义了
static
静态成员,则整个继承体系里只有一个这样的成员,无论派生出多少个派生类,都只有一个static
成员实例。
(十)多继承及其菱形继承问题
- 继承模型:
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类,多继承对象在内存中的模型是先继承的基类在前,后继承的基类在后,派生类成员在最后。
- 菱形继承:
- 菱形继承是多继承的一种特殊情况,存在数据冗余和二义性问题。例如,在
Assistant
类中,由于Student
和Teacher
分别继承了Person
,导致Assistant
类中有两份Person
的成员变量和成员函数。 - 解决二义性问题可以显式指定访问哪个基类的成员,但数据冗余问题无法解决。
- 菱形继承是多继承的一种特殊情况,存在数据冗余和二义性问题。例如,在
- 虚继承:
- 虚继承可以解决数据冗余和二义性问题,但底层实现复杂,性能会有一些损失。使用虚继承后,基类会单独提取出来,避免了多个派生类中重复存储基类成员。
(十一)多继承中指针偏移问题
- 问题分析:在多继承中,对象的内存布局是先声明的基类在前,后声明的基类在后,派生类成员在最后。对于
Derive
类继承自Base1
和Base2
,当分别用Base1*
、Base2*
和Derive*
指针指向Derive
对象时,Base1*
和Derive*
指针指向的地址相同,而Base2*
指针指向的地址与它们不同。 - 答案解释:所以答案是
p1 == p3!= p2
,即选项 C。
(十二)继承和组合
- 关系描述:
public
继承是一种“is - a”的关系,每个派生类对象都是一个基类对象。- 组合是一种“has - a”的关系,一个类组合了另一个类,就意味着这个类的对象中包含另一个类的对象。
- 复用方式:
- 继承是白箱复用,基类的内部细节对派生类可见,一定程度破坏了基类的封装,基类的改变对派生类影响大,派生类和基类间依赖关系强,耦合度高。
- 对象组合是黑箱复用,被组合的对象内部细节不可见,组合类之间依赖关系弱,耦合度低,优先使用对象组合有助于保持每个类的封装。
- 实践建议:实践当中建议优先使用组合,而不是继承。但如果类之间的关系适合继承(“is - a”)或者要实现多态,则需要使用继承。如果类之间的关系既适合继承又适合组合,也优先使用组合。
二、总结
继承是面向对象编程中的重要概念,它允许代码复用,但也带来了一些复杂性,如作用域隐藏、多继承的问题等。在实际编程中,需要根据具体情况合理选择继承和组合,以提高代码的可维护性和扩展性。同时,要注意避免设计出复杂的继承关系,尤其是菱形继承,以免带来数据冗余和二义性等问题。
标签:定义,继承,成员,基类,概念,对象,派生类,函数 From: https://blog.csdn.net/weixin_64368818/article/details/143804111