多态
接口继承和实现继承:
普通函数:子类继承了父类的函数,继承的是函数的实现
接口函数:子类继承了父类的函数声明,目的是为了重写来实现具体方法具体是吸纳
多态定义和条件
概念
:同一件事情,不同类型的对象来做就会产生不同的效果(前提是构成继承关系)
普通人买票就是全价购买
学生买票就是免费
作用的对象
:成员函数
构成多态的条件
继承、重写、指针或引用、虚函数
1、调用这个函数必须用父类的指针或者引用来调用
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价购买" << endl;
}
};
---------------------------------------------------
class Person
{
public:
virtual void BuyTicket()
{
cout << "免费购买" << endl;
}
};
--------------------------------------------------------
调用时必须先通过父类的指针或者引用来调用虚函数
void Func(Person & people)
{
people.BuyTicket();
}
------------------------------------------------
int main()
{
Person p;
Func(p);
Student s;
Func(s);//以切片的形式,将父类的部分传过去
return 0;
}
2、多态的实现要通过父类和子类的虚函数重写
虚函数重写:
1、用virtual修饰
2、要保证三同:函数名相同
参数类型相同(和两个函数给的缺省值无关,只看类型)
返回值相同
重写的概念:
假如说:我作为子类,继承了父类的函数,但是这个函数对我不满足
所以我就把函数的内容重新写了一份写了一份
在这个过程中,
方法还是那个方法(所以才三同),
但内容变了(重写的概念)
例子:
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
};
---------------------------------virtual修饰,函数名和参数类型和返回值相同
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "免费买票" << endl;
}
};
虚函数和普通的类成员函数一样存储在常量区(代码段)
P.S. 判断某个东西在什么地方存储
条件以外但仍然满足多态的情况
一般情况下,要求三同和virtual才可以满足重写的要求
但有三个例外:
1.协变(子类和父类的虚函数返回值类型不同 )
但并不是所有的返回值都可以;
必须要是:
父类虚函数返回父类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用
例子:
class Person
{
public:
virtual Person& BuyTicket()
{
cout << "全价买票" << endl;
return *this;
}
};
---------------------------------virtual修饰,函数名和参数类型相同
------------------------*********返回值类型为本类的指针或者引用
class Student :public Person
{
public:
virtual Student& BuyTicket()
{
cout << "免费买票" << endl;
return *this;
}
};
2.析构函数的重写(名字可以不同)
为了后续多态能够正常使用,编译器会将所有的析构函数的函数名都改为destructor
为什么要修改析构函数的函数名?:
正常情况下是没问题的
Person p;
Student s;
生命周期结束后自动调用析构函数
1、s:(先调用s的析构函数,然后s自动调用父类的析构函数)
2、p:(p调用析构函数)
但如果遇到下面的问题
Person* p2 = new Person;
Person* p3 = new Student;
delete p2;
delete p3;
我new了Person和Student的空间出来
按道理来说:
我的预期应该和第一种情况那样(~Student ~Person ~Person)
但实际上:
原因:Student的空间被Person的指针指向着,delete p3的时候,会根据p3的类型来调用析构函数
危害:如果在Student的成员变量中,有成员申请开了空间,但并没有~Student,就会导致内存泄漏
为了满足我的期望(开了谁的对象就调用谁的析构函数),就必须要重写Student的析构函数
所以析构函数的函数名才统一被编译器改为:destructor
3.子类可以省略写virtual
只要父类写了virtual就可以省略子类的virtual(不过这边建议还是加上virtual,代码更好读)
多态理解加深训练题
理解加强点:
- 子类对象调用父类函数(切片的理解)
- 对象的指针
- 重写的内容(重写的仅为方法体)
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
---------------------------------------------------------------
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
---------------------------------------------------------------
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
--------------------------------------------------------------------
A: A->0
B : B->1
C : A->1
D : B->0
E : 编译出错
F : 以上都不正确
答案解析:
选B
原因:
- B类作为子类,继承了A类的内容,当test的参数为A类对象时,我们可以将B类对象的关于A类的切片传过去(所以编译不会出错)
- test()接收到了A类的切片,但这实际上是指向了B类成员的A类切片,所以当function获得传参是,获得的就是这个B类成员的A类切片
- A和B是继承关系,判断是否符合多态调用(三同,virtual,利用父类的指针进行调用)
- 判断符合多态的调用,于是根据指针,指向谁,就调用谁的函数(指向B类成员,就调用B类成员的函数)
- 重写的概念:方法还是那个方法(所以才三同),但内容变了(重写的概念)
即:函数的重写是重写函数体(内容),函数的声明还是父类的
实际上调用的部分是父类的声明+子类的函数体
多态的底层原理
virtual
概念:virtual 是一个关键字,用于创建虚函数。
virtual和对象模型
如果类中没有虚函数:
内存中存成员变量,将成员函数存到公共代码段中
如果类中又虚函数:
要多存一个虚函数表指针(4byte 或者 8byte)
虚函数表指针(虚表指针):该指针指向一块空间,这块空间存储着该类的所有虚函数的地址
例子:
class Yes
{
public:
virtual void func() { ; }
private:
int _yes;
};
----------------------------------------------
class No
{
public:
void func() { ; }
private:
int _no;
};
----------------------------------------------------
int main()
{
Yes yes;
No no;
cout << sizeof(yes) << endl;//内存大小为8
cout << sizeof(no) << endl;//内存大小为4
return 0;
}
在以上的代码中,验证得知:有虚函数的类确实会要多存一个虚函数表指针
实际内存结构:
根据前面所说的,满足多态的条件,其一是:必须要是父类的指针或者引用
原因:
多态的概念就是:做同一件事情,不同类型的对象来做就会产生不同的效果(前提是构成继承关系)
所以当父类传整个父类对象给函数的时候,编译器会从父类的虚函数表指针中寻找需要调用的函数
同理:当子类传父类对象的切片给函数的时候,编译器会从子类的虚函数表指针中寻找需要调用的函数
整个底层流程
编译器全程判断:
如果不满足多态:
在编译链接期间就会将需要调用的函数地址给记住
根据对象的类型,来确认调用的函数,确定函数的地址
如果满足多态
在运行期间,会去找对象的虚表指针指向的函数地址
根据这个地址来调用指定的函数
多继承的多态
菱形继承和菱形虚拟继承的多态
C++为此特地增加的关键字(仅供了解)
override
作用:明确指示该函数需要重写,快速检查某函数是否完成了重写
如果在派生类中使用 override 关键字来重写基类的虚函数,而基类中没有对应的虚函数,则会导致编译错误。
override 关键字用于在派生类中明确指示对基类虚函数的重写。它有助于提高代码的可读性和可维护性,并防止由于拼写错误或参数列表不匹配等原因导致的意外行为。
语法:
class Base {
public:
virtual void foo() const {
// 基类虚函数
}
};
------------------------------------如果foo不满足重写,就会报编译错误
class Derived : public Base {
public:
void foo() const override {
// 派生类重写基类虚函数
}
};
final
作用:final 关键字用于在类的定义中阻止继承或在虚函数声明中禁止重写。
一个类被声明为 final 时,它不能被其他类继承。同样地,当一个虚函数被声明为 final 时,它不能被派生类重写。
语法:
class Base final {
// 该类不能被继承
};
class Base {
public:
virtual void foo() const final {
// 该虚函数不能被重写
}
};
抽象类
- 概念:有纯虚函数的类
- 使用方法:在虚函数后面赋值0
- 特点:若该类中存在纯虚函数,则该类无法实例化出对象(和抽象的本意一样,无实体)
- 意义:该类的子类也无法实例化处对象(因为子类继承了父类的纯虚函数)
- 作用:强制了子类必须重写这个纯虚函数
抽象类和override的使用区别:
如果只是想要检查是否重写了,就用override
如果不希望父类实例化,就使用抽象类
二者都是强制重写,只不过抽象类能做更多事情