【C++】简单学——多态

接口继承和实现继承:
普通函数:子类继承了父类的函数,继承的是函数的实现
接口函数:子类继承了父类的函数声明,目的是为了重写来实现具体方法具体是吸纳

多态定义和条件

概念

:同一件事情,不同类型的对象来做就会产生不同的效果(前提是构成继承关系)

普通人买票就是全价购买
学生买票就是免费

作用的对象

:成员函数

构成多态的条件

继承、重写、指针或引用、虚函数

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,代码更好读)

多态理解加深训练题

理解加强点:

  1. 子类对象调用父类函数(切片的理解)
  2. 对象的指针
  3. 重写的内容(重写的仅为方法体)
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

原因:

  1. B类作为子类,继承了A类的内容,当test的参数为A类对象时,我们可以将B类对象的关于A类的切片传过去(所以编译不会出错)
  2. test()接收到了A类的切片,但这实际上是指向了B类成员的A类切片,所以当function获得传参是,获得的就是这个B类成员的A类切片在这里插入图片描述
  3. A和B是继承关系,判断是否符合多态调用(三同,virtual,利用父类的指针进行调用)
  4. 判断符合多态的调用,于是根据指针,指向谁,就调用谁的函数(指向B类成员,就调用B类成员的函数)
  5. 重写的概念:方法还是那个方法(所以才三同),但内容变了(重写的概念)

即:函数的重写是重写函数体(内容),函数的声明还是父类的

在这里插入图片描述
实际上调用的部分是父类的声明+子类的函数体

多态的底层原理

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 {
        // 该虚函数不能被重写
    }
};
抽象类
  1. 概念:有纯虚函数的类
  2. 使用方法:在虚函数后面赋值0
  3. 特点:若该类中存在纯虚函数,则该类无法实例化出对象(和抽象的本意一样,无实体)
  4. 意义:该类的子类也无法实例化处对象(因为子类继承了父类的纯虚函数)
  5. 作用:强制了子类必须重写这个纯虚函数

抽象类和override的使用区别:
如果只是想要检查是否重写了,就用override
如果不希望父类实例化,就使用抽象类
二者都是强制重写,只不过抽象类能做更多事情

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CtrlZ大牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值