一.继承的概念和定义 1.1继承的概念 继承允许我们基于已有的类(基类)创建新类(派生类)。派生类自动获得基类的成员(属性和方法),并可以扩展新的功能。
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
class Person
{
public:
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。
1.2继承的定义 1.2.1定义格式 下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(既叫基类/派⽣类,也叫⽗类/⼦类)
继承方式和访问限定符:
三种继承方式:public、protected 与 private
C++ 提供三种继承方式,每种方式对基类成员在派生类中的访问权限有不同影响,同时也决定了派生类对象对基类成员的可访问性:
public 继承:保持接口一致性
当使用public继承时,基类的public成员在派生类中仍为public,protected成员仍为protected,private成员不可访问。这种继承方式最符合 “is-a” 关系,派生类完全继承基类的接口,外部代码可以像使用基类对象一样使用派生类对象(里氏替换原则)。
protected 继承:限制接口暴露范围
protected继承会将基类的public成员变为protected,protected成员保持不变,private成员不可访问。此时派生类的子类可以访问这些成员,但外部代码无法直接访问,常用于基类作为 “实现细节” 而非 “接口” 的场景。
private 继承:实现细节的封装
private继承会将基类的public和protected成员都变为private,仅在派生类内部可访问,外部代码和派生类的子类都无法访问。
继承基类访问方式的变化:
基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象,但是语法上限制派生类对象无论是在类里面还是在类外面都不可以访问基类的私有成员。这个基类的private成员跟派生类中原有的private成员不同,派生类原有的private成员在类里面可以访问,在类外不可访问 由于基类的private成员在派生类中是不能访问的,所以如果基类的成员不想在派生类继承后在派生类的类外面直接访问,但可以在派生类的类里面进行访问,那么就将基类的成员定义为protected成员(protected成员在基类中的里面可以访问,在基类的外面不可以访问)。由此可见protected保护成员限定符是因为继承才出现的 总结一下表格,在基类中的private私有成员无论是以什么方式被派生类继承,其在派生类中都是不可见的,基类其它成员在派生类的访问方式是取后面两个的关键字权限小的那一个(成员在基类的访问限定符,继承方式),关键字权限大小关系:public>protected>private 如果不加继承方式,那么使用关键字class时,默认继承方式是private继承。使用关键字struct时,默认继承方式是public继承。不过在实际的使用中最好显示写出继承方式 在实际的使用场景中,使用最多的是public继承,而protected/private继承的使用场景非常少。同时我们同样不建议使用protected/private继承,因protected/private继承进行继承下来的成员,只能在派生类的类里面进行使用,可维护性不强 二.基类和派⽣类间的转换 public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
基类对象不能赋值给派⽣类对象。
基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _Stuid; // 学号
};
int main()
{
//以前我们所知道的强制类型转化
double d = 1.1;
int i = d;
//子类对象可以赋值给父类对象/指针/引用
Student sobj;
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//基类对象不能赋值给派生类对象
//sobj = pobj;
}2.1切片现象的产生 切片现象通常发生在对象赋值或函数参数传递时,当派生类对象被赋值给基类对象,或者派生类对象被传递给以基类对象为参数的函数时,派生类对象的额外成员变量会被截断,仅保留基类部分的数据。这种现象被称为“切片”。
三.继承中的作⽤域 3.1 隐藏规则 在继承体系中基类和派⽣类都有独⽴的作⽤域。
派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
隐藏现象的产生 在 C++ 的继承机制中,派生类可以继承基类的成员,但当派生类中定义了与基类同名的成员时,就会发生隐藏现象。隐藏现象不仅会隐藏同名的成员变量,还会隐藏同名的成员函数,甚至会隐藏基类中重载的函数版本。
成员变量的隐藏
当派生类中定义了与基类同名的成员变量时,派生类的成员变量会隐藏基类的成员变量。 成员函数的隐藏
当派生类中定义了与基类同名的成员函数时,派生类的成员函数会隐藏基类的同名成员函数,即使它们的参数列表不同。 四.派⽣类的默认成员函数 6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?
派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造
函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派
⽣类对象先清理派⽣类成员再清理基类成员的顺序。
派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
构造与析构的顺序 重要规则:
构造顺序:基类 → 派生类的成员对象 → 派生类自身
析构顺序:完全相反(派生类自身 → 成员对象 → 基类)
基类构造函数的显式调用 如果基类没有默认构造函数(即定义了带参数的构造函数),派生类必须在初始化列表中显式调用基类的构造函数。
五.继承与友元 友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员。
友元与继承的注意事项 友元不是继承的一部分:友元函数或友元类不会随着继承关系传递到派生类中。如果需要在派生类中使用友元,必须显式声明。 友元与封装:友元机制打破了封装原则,因此应谨慎使用。只有在确实需要访问类的私有成员时,才使用友元。 友元与多态:友元函数与多态无关。友元函数不会自动参与虚函数的动态绑定。 友元与模板:友元机制也可以与模板结合使用,但需要特别注意模板的实例化和友元声明的语法。 六.继承与静态成员 6.1静态成员变量 静态成员变量是类的共享属性,它属于类本身,而不是类的某个特定对象。因此,静态成员变量在类的所有对象之间是共享的,且只有一份拷贝。
声明与定义:静态成员变量必须在类外进行定义和初始化。 访问方式:可以通过类名或对象名访问静态成员变量。 6.2 静态成员函数 静态成员函数是类的全局函数,它不依赖于类的任何对象。因此,静态成员函数不能访问非静态成员变量或非静态成员函数,但可以访问静态成员变量和其他静态成员函数。
声明:在函数声明前加上 static 关键字。 调用方式:可以通过类名或对象名调用静态成员函数。 继承与静态成员的关系 静态成员变量的继承
静态成员变量是类的属性,而不是对象的属性。因此,派生类会继承基类的静态成员变量,但不会创建自己的独立拷贝。派生类和基类共享同一个静态成员变量。 静态成员函数的继承
静态成员函数也可以被继承,但与普通成员函数不同,静态成员函数不依赖于对象的状态。因此,静态成员函数的行为不会因继承而改变。 继承中的静态成员初始化
静态成员变量的初始化必须在类外进行,且只能初始化一次。如果派生类继承了基类的静态成员变量,初始化仍然在基类中完成。 注意事项 静态成员的共享性:静态成员变量在基类和派生类之间是共享的,修改一个类中的静态成员变量会影响所有类。 静态成员函数的限制:静态成员函数不能访问非静态成员变量或非静态成员函数,因为它们不依赖于对象的状态。 隐藏与覆盖:派生类中的静态成员函数会隐藏基类中的同名静态成员函数,而不是覆盖。 初始化顺序:静态成员变量的初始化顺序与它们在代码中的定义顺序有关,而不是声明顺序。 七.多继承 7.1 继承模型 单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承:
当多个基类共享同一个祖先类时,会形成菱形继承结构,导致派生类中存在多个基类子对象,引发数据冗余和二义性问题
7.2 虚继承 有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。
八.继承和组合 1. 继承(Inheritance) 继承是一种“是一种”(is-a)关系,表示一个类(派生类)继承了另一个类(基类)的属性和行为。派生类可以扩展或修改基类的功能,同时保留基类的接口。
特点:
代码复用:派生类可以直接使用基类的成员函数和成员变量。
多态支持:通过虚函数和动态绑定,派生类可以覆盖基类的函数,实现多态行为。
层次结构:可以形成类的层次结构,便于理解和维护。
2. 组合(Composition) 组合是一种“包含”(has-a)关系,表示一个类(容器类)包含另一个类(成员类)的对象。容器类通过成员类的对象来实现功能,而不是继承其行为。
特点:
代码复用:通过成员对象调用其方法,实现功能复用。
灵活性:可以动态地组合对象,便于扩展和修改。
封装性:可以隐藏成员对象的实现细节,只暴露必要的接口。
继承与组合的比较 功能扩展
继承:通过扩展基类的功能,派生类可以添加新的成员变量和成员函数,或者覆盖基类的虚函数。
组合:通过组合成员对象,容器类可以调用成员对象的方法来实现功能。如果需要扩展功能,可以通过添加新的成员对象或修改成员对象的行为。 灵活性
继承:一旦定义了继承关系,派生类的行为和结构就与基类紧密绑定。修改基类可能会影响所有派生类。
组合:组合关系更加灵活,可以通过动态地创建和组合对象来实现不同的功能。修改成员类不会影响容器类的结构。 封装性
继承:继承可能会破坏封装性,因为派生类可以访问基类的保护成员。
组合:组合关系可以更好地封装成员对象的实现细节,只暴露必要的接口。 多态支持
继承:通过虚函数和动态绑定,继承可以实现多态行为。
组合:组合本身不支持多态,但可以通过成员对象的多态行为来实现类似的效果。 何时使用继承,何时使用组合 使用继承的场景
行为扩展:当派生类需要扩展基类的行为,并且这种扩展是合理的“是一种”关系时,使用继承。
多态需求:当需要通过基类指针或引用调用派生类的方法时,使用继承。
接口共享:当多个类需要共享相同的接口时,使用继承。 使用组合的场景
功能复用:当需要复用某个类的功能,但不需要继承其行为时,使用组合。
灵活性需求:当需要动态地组合对象,或者需要在运行时修改对象的行为时,使用组合。
封装需求:当需要隐藏成员对象的实现细节时,使用组合。 八.实现⼀个不能被继承的类 基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。 C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。