继承和多态
继承
继承(Inheritance)是面向对象编程(OOP)中的一个核心概念,它允许一个类(派生类或子类)基于另一个类(基类或父类)来创建,并继承其属性和行为。通过继承,可以实现代码的重用和层次化设计。
继承的定义
- 继承是指一个类从另一个类获取属性和方法的过程。
- 派生类继承基类的成员(数据成员和成员函数),并可以添加新的成员或重写基类的成员。
- 继承体现了 “is-a” 关系,即派生类是基类的一种特殊类型。
继承的方式
public private protected
派生类中的类型取基类和继承中的较为低的权限
如果没有继承就没有protected这个类型,一般情况下public的继承的方式已经可以解决绝大多数的情况。
赋值兼容转换这里是c++中的一个单独的语法(重点)
子类的对象,指针,引用可以赋值给父类,这种形象的说法叫做切割
C语言中存在的类型的转换叫做截断和提升( 强制类型转换 ),类型转换的过程中会产生临时的变量,因此char& i = a(a是一个int类型的值),这里是不合法的
但是派生类不能表示基类,但是这里c++中的赋值兼容是一种单独的的规则,中间没有产生临时变量,如果是对象就是将子类对象进行切割进行copy,如果是指针(reference),就是减少指针所能表示的空间的大小,其中只能包含基类的内容这里通过基类ptr(ref)进行的操作会作用于派生类中
父类和子类中存在同名的成员。在父类中,我们查找的是父类中成员(现在父类本类域中进行查找)。
赋值兼容转换保证了基类可以对派生类进行调用,为多态性埋下了伏笔
函数隐藏和函数重载的区别
在同一个作用域下面的同名函数才能称作函数重载,如果继承的类和基类拥有同名的函数,这里构成的叫做函数的隐藏,假设我们创建一个派生类的对象,如果想要调用这个同名函数,只能调用派生类的这个函数。
class A{
public:
void fun(){
cout<<"hehe";
}
}
class B:public A{
void func(int i){
cout<<"e";
}
}
//函数重载的前提是在同一个作用域的函数叫重载,这里由于函数不在一个作用域,这里是函数的隐藏
//所以这里函数名相同叫做隐藏
这里我们的理解方式是编译器只会寻找一次,并且先从子类中寻找,“子类中没有”才从父类中找
类域的先后性很重要
实践中尽量不要写同名成员
派生类的构造和析构函数
- 派生类:继承的父类中的成员 + 自己新添加的成员
- 对于构造函数:我们可以将基类理解成一个自定义类型,他会自己去调用自己的构造函数
- 对于拷贝构造:和构造函数的理解方式是一致的(这里其实就是赋值兼容转换的一个应用,读者可以想想为什么)
对于这几个函数就是只有当有深浅拷贝的问题存在是才需要我们自己去写拷贝构造和复制重载函数
对于析构函数而言就是区别于构造函数,析构函数是不需要我们显示的调用基类的析构函数,编译器一定会自己去调用,因此,我们在对派生类写析构函数的过程中就不能去显示的去调用基类的构造函数,不然就会出现析构多次的问题出现。
c++搞这一套理论的原因是:对于构造函数,是应该先进行对基类的构造在进行派生类中成员的构造,
但是析构的时候应该先进行派生类的析构在进行基类的析构(这里的原理类似于一种stack的结构)。
原因:由于派生类是建立在基类的基础上进行的创建,如果我们在析构的时候先析构基类,析构后的派生类会显得没有意义。
c++一定是保证的是先父后字,在初始化表的时候,我们知道初始化列表的顺序和我们在其中的显示类型写出的顺序不同,而是而是和private or protected中保护的成员的声明顺序相关,在c++中,我们默认派生类中基类是最先进行声明,这也就意味着他是最先通过初始化列表进行初始化的
总结:在手搓构造和析构的时候一定得注意初始化和析构的顺序,这种时候对于析构函数我们尽量不要显示的调用好一点,他会在派生类的析构结束后自动调用。派生类中的拷贝构造函数这个时候为了实现对基类的初始化,我们前面学的赋值兼容转换就在这里排上了用场。
友元关系不能继承
友元的函数可以访问基类的全部的元素,但是派生类没有这个权限,这种时候,只有在派生类中重新的声明友元函数,同样的派生类的友元的关系不会关系到基类的友元的关系
总结:友元的关系对于每一层都需要单独去声明
static修饰的静态变量的继承
static修饰的静态的变量的继承
static修饰的静态的变量没有存储在静态中,而是单独的存储在静态区,因此,结论是,静态变量会被继承,所有的基类和派生类共用一个静态变量
c++中的菱形二义性的问题(难点)
我们实际开发的时候其实很少用到多层继承
c++中增加了关键字virtual,通过虚拟继承的方式,可以保证基类中的内容纸杯继承一次,这样就不会因为菱形继承继承两次出现错误。
c++中继承和组合的运用
- 组合顾名思义,就是将原本的base类作为派生类的成员变量
- 继承时白箱复用,组合是黑盒复用,黑盒测试比白盒测试的要求更高,白盒要求里面的底层实现
这种时候,组合是一种低耦合的方式,所以实践中能用组合尽量不用继承
多态
多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,指的是同一个接口或方法在不同情况下表现出不同的行为。多态性允许不同类的对象对同一消息做出响应,具体行为取决于对象的实际类型。
虚函数
虚函数(Virtual Function)是C++中实现运行时多态(动态多态)的关键机制。它允许在基类中声明一个函数,并在派生类中重写该函数,使得通过基类指针或引用调用该函数时,实际执行的是派生类中的版本。
虚函数的定义
- 虚函数是在基类中使用 virtual 关键字声明的成员函数。
- 派生类可以重写(Override)虚函数,提供自己的实现。
*通过基类指针或引用调用虚函数时,会根据实际对象的类型决定调用哪个版本的函数(动态绑定)。
虚函数重写
虚函数重写要求函数名参数和返回值相同(本质原因是虚函数重写的函数和基类函数用的是同一个头部,这也就是说派生类中重写的虚函数调用的默认参数永远都只会使基类的默认参数)
特例
- .协变(基类和派生类虚函数的返回值不同)
普通情况下,虚函数的返回值不同在编译的阶段就会报错
只有函数的返回值分别对应的基类和派生类的指针和引用可以使得编译通过
总结:我个人认为这里协变的作用就是为了如果赋值重载函数作为虚函数进行重写的时候返回值不同所造成的问题 - .析构函数实现函数的多态(对于析构函数来说,我们在派生类中可以不用显示的调用基类的析构函数,在派生类析构完成后,系统会默认自己去调用我们基类中的析构函数,虽然其实显示的调用析构函数对于c++来说是合法的)
为什么我们要重写析构函数呢?
当我们用基类指针去指向派生类的对象,当我们用delete baseptr的时候,他只会析构基类的部分,这个时候就会出现内存泄露
class Person
class Student : public Person
Person* ptr = new Student;
delete ptr;//如果这里的析构函数不用多态性实现,那么就会出现内存泄漏
那么有没有想过,纯虚函数需要同名,但是析构函数明显是不同名的
为什么并不同名也能构成多态呢,原因就是c++经过特殊处理,所有的析构函数都会实现为destructor(),以此构成多态性
final and override关键字
final修饰类的话,类不能被继承,同样我们也可以通过private构造函数的方式间接实现
override的作用是保证函数的重写
抽象类
包含纯虚函数的类叫做抽象类
纯虚函数的事项情况:
c++中虚函数重写的坑(经典)
由于虚函数保证了函数的名字参数以及返回值全部相同,因此,c++在实现派生虚函数的实现时,他的参数接口调用的基类的参数接口,因此,这个缺省值默认的是基类的默认值
而派生类只负责虚函数的实现,这也是为什么派生类中虚函数的实现的时候可以不用加上 virtual
class A{
public:
virtual func(int i = 0){ std::cout << "A->" << i << endl; }
void test() { func(); }
}
class B : public A{
public:
virtual func(int i = 1){ std::cout << "B->" << i << std::endl; }
}
int main()
{
B* b = new B;
b->test();//答案是B->0 //这里是c++的一个大坑
}
c++中的虚函数的重写,动态绑定(运行时调用)(总结)
c++为什么基类指针可以指向派生类的函数,我们知道如果只是普通的函数重写,这种情况下同名的函数构成的覆盖,基类指针通过赋值兼容转换只能调用基类的那个函数不能调用派生类的函数
但是这里出现了虚函数这一个概念,虚函数不同于其他的函数虚函数通过虚基表(虚基表其实本质上是函数指针数组),由于虚基表是存放在基类中的_vfptr数组中方便对虚函数进行统一的管理,基类指针可以调用,当重写了虚函数之后,创建对应对象的同时实现了虚函数的更新,这种情况下,就实现了基类指针可以调用派生类的函数,当然,由于派生类的函数和基类的函数还是存在着函数覆盖的关系,我们也是可以显示调用基类的函数(当然这不是我们用虚函数的初衷)