【C++进阶】继承
🥕个人主页:开敲🍉
🔥所属专栏:C++🥭
🌼文章目录🌼
1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称子类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有⼀些不同的成员变量和函数,比如老师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,老师的独有成员函数是授课。
class Teacher
{
public:
void identity()
{
//身份认证
}
private:
size_t _age;//年龄
string _name;//姓名
string _address;//地址
string _tel;//电话string _title;//学号
};
class Student
{
private:
size_t _age;//年龄
string _name;//姓名
string _address;//地址
string _tel;//电话string _stuid;//职称
};
下面我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。
class Person
{
public:
public:
void identity()
{
//身份认证
}size_t _age;//年龄
string _name;//姓名
string _address;//地址
string _tel;//电话
};
class Teacher:public Person
{
private:
string _title;//学号
};
class Student:public Person
{
private:
string _stuid;//职称
};
1.2 继承定义
1.2.1 定义格式
从上面我们看到,Person是父类,也可以称为基类,Student和Teacher是子类,也可以称为派生类。继承方式如下图:
可以看到,这里的继承方式所用的关键字和我们访问限定符的关键字是一样的:
1.2.2 继承父类成员访问方式的变化
总结如下:
子类中的父类成员的访问方式取决于 父类对这些成员的访问限定符与继承方式 中权限较小的。
比如,父类的成员用private限定,继承方式用public,那么实际上父类中的成员继承到子类中后,也是private,因此子类虽然继承了父类的成员,但是不可访问。
1.3 继承类模板
这里我们实现一个栈的类,继承的是库中的vector,因此我们在实现栈的功能:尾插、尾删、获取栈顶元素等操作时,就可以调用vector中的函数。
2. 父类和子类对象赋值兼容转换
① public继承的子类对象可以赋值给父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去。
② 父类对象不能赋值给子类对象。
3. 继承中的作用域
3.1 隐藏规则:
① 在继承体系中父类和子类都有独立的作用域。
② 子类和父类中有同名成员,子类中将会屏蔽对父类中的同名成员的直接访问,这种情况叫隐藏。如果想要访问父类中的同名成员,使用 父类 :: 同名成员 即可访问。
③ 子类和父类中的成员函数只要函数名相同就构成隐藏,不受其他因素限制。
④ 因此在继承体系中不建议定义同名函数。
4. 子类的默认成员函数
4.1 4个常见默认成员函数
默认成员函数是指我们不写,编译器会帮我们自动生成,常见的4个默认成员函数有:构造函数、析构函数、拷贝构造函数、operator=。那么在子类中,这4个默认成员函数是如何生成的呢?
① 子类的构造函数必须调用父类的构造函数初始化父类的那⼀部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
② 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。
③ 子类对象初始化时先调用父类对象的构造函数再调用子类对象的构造函数。
④ 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
⑤ 子类的operator=必须要调用父类的operator=完成父类的复制。需要注意的是子类的operator=隐藏了父类的operator=,所以显示调用父类的operator=,需要指定父类作用域。
4.2 实现一个无法被继承的类
方法1:将父类的构造函数设为private,因为在构造子类对象时必须要调用父类对象的构造函数,而父类对象的构造函数设为私有无法访问,因此也就无法被继承。
方法2:使用final修饰父类,就不能够被继承了。finale有最后的意思,这里可以理解为最终类。
5. 继承与友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
6. 继承与静态成员
父类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个子类,都只有⼀个static成员实例。
7. 多继承及灵性继承问题
7.1 继承模型
单继承:一个子类只有一个父类的情况我们称为单继承。
多继承:一个子类有两个或者两个以上的父类时称为多继承,多继承对象在内存中存储的方式为:先继承的父类在最前面,后面继承的父类按照顺序排列,子类的成员排在最后。
菱形继承:菱形继承是多继承的一种特殊情况。
从上面可以看出,fun2和fun3都继承了fun1,都存有了一份size_t _fun1,而fun0继承了fun2和fun3,因此不可避免地fun0中存有了两份fun1中的size_t _fun1,这就会导致数据冗余和⼆义性。支持多继承就一定会存在菱形继承的问题,实践中使用多继承是务必要小心谨慎。
这里在访问_fun1时系统报错说fun0::_fun1不明确,也就是这里的两份_fun1,系统也不知道你要访问的是哪一份。这就是二义性问题。
7.2 虚继承
那我们在实践中不可避免地要写出菱形继承,但是又想避免数据冗余和⼆义性的问题该如何做呢?这里就需要使用到virtual关键字:
7.3 多继承中的指针偏移问题
来看一道例题:
关于多继承中指针偏移问题,下面选项正确的是()
A. f1==f2==f3 B. f1<f2<f3 C. f1==f3!=f2 D. f1!=f2!=f3
代码:
class fun1
{
public:
size_t _fun1;
};class fun2
{
public:
size_t _fun2;
};
class fun3 :public fun1, public fun2
{
private:
size_t _fun3;
};
int main()
{
fun3 f3;fun1* f1 = &f3;
fun2* f2 = &f3;
return 0;
}
正确答案:C
为什么呢?一张图弄明白:
8. 继承和组合
8.1 继承和组合
① 继承是一种is-a的关系。意思就是每个子类对象都是一个父类对象,就好比是有血缘关系的父子,儿子有自己不同于父亲之处,同时也一定有与父亲的相同之处,相同之处便是子类继承父类的那一部分。
② 继承允许你根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见
。继承⼀定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系
很强,耦合度高。
③ 组合是一种has-a的关系。可以理解为数学中的集合,{1,2,3}和{1,2,3,4,5,6}的关系,前者是后者的子集后者包含前者。
④ 对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
⑤ 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),那就用组合。
⑥ 很多人说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,并不像我们想的那么简单,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的⼀些编程语⾔都没有多继承,如Java。
创作不易,点个赞呗,蟹蟹啦~