IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++中的多态 -> 正文阅读

[C++知识库]C++中的多态

1、多态的定义及实现

1.1、多态概念

????多态就是不同继承关系的类对象,调用同一个函数得到不同结果的行为,就如同成人和学生去买票,成人买的是成人票,学生买的是学生票。继承要实现需要满足两个条件:(1)必须通过基类的指针或者引用调用虚函数。(2)调用的函数必须是virtual修饰的虚函数。具体可见下图:
在这里插入图片描述

1.2、虚函数重写

????虚函数重写就是派生类中有一个和基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型。函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数。具体代码如下:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
class Student :public Person
{
	virtual void BuyTicket()  //派生类的virtual关键字也可以不用写也构成重写
	{
		cout << "买票-半价" << endl;
	}
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student s;

	//传对象
	Func1(p);
	Func1(s);

	//传对象的地址
	Func2(&p); 
	Func2(&s);
	return 0;
}

1.3、虚函数重写的两个例外

????虚函数的重写有两个例外:(1)协变,就是派生类与基类的虚函数名字相同,但是返回值为类的指针或引用,也满足重写。具体代码如下:

//返回值为类指针
class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return 0;
	}
};
class Student :public Person
{
	virtual Student* BuyTicket()  //派生类的virtual关键字也可以不用写也构成重写
	{
		cout << "买票-半价" << endl;
		return 0;
	}
};
//返回值为类引用
class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "买票-全价" << endl;
		return *this;
	}
};
class Student :public Person
{
	virtual Student& BuyTicket()  //派生类的virtual关键字也可以不用写也构成重写
	{
		cout << "买票-半价" << endl;
		return *this;
	}
};

????(2)析构函数重写,由于基类和派生类的类名,所以析构函数名不同,但是这仍然构成虚函数重写,因为析构函数名编译器会同一处理成destructor。具体代码如下:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student :public Person
{
public:
	virtual ~Student()   //不加virtual也构成重写
	{
		cout << "~Student()" << endl;
	}
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;
	delete p2;
	return 0;
}

1.4、override和final

????用final在虚函数后面进行修饰虚函数,表示该虚函数不能再被重写,具体操作如下:

virtual void Drive() final
{}

????用override在派生类虚函数后面进行修饰虚函数,可以检测该虚函数是否重写,防止名字写错照成错误,具体操作如下:

virtual void Drive() override
{}

1.5、重载,重写以及重定义(隐藏)之间的区别

在这里插入图片描述

2、抽象类的认识

2.1、抽象类的概念

????抽象类就是包含纯虚函数的类,而在虚函数的后面写上=0,就是纯虚函数。基类中包含纯虚函数后,不能实例化对象,派生类继承纯虚函数后,只有重写纯虚函数才能实例化对象。纯虚函数规范了派生类必须重写,强制子类重写。具体代码如下:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒服" << endl;
	}
};
class BMW:public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操作" << endl;
	}
};
int main()
{
	Car* p = new Benz;//报错
	p->Drive();

	Car* b = new BMW;//报错
	b->Drive();

	Car c;//报错

	Benz b;

	BMW bb;
	return 0;
}

2.2、实现继承与接口继承

????实现继承:就是不加virtual的普通继承。
????接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

3、多继承原理

3.1、虚函数表

????有虚函数的类情实例化的对象中存放着虚函数指针,指向一个虚函数表,表中存放着虚函数地址,我们看下面一段技术类对象大小的代码:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:

	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

????根据上述代码得到的结果如下:
在这里插入图片描述
????由上图我们可以看出,对象b中存放着一个成员变量和一个虚函数表指针,而对象大小为8byte。接着我们看下一个代码:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
class Driver :public Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 2;
};
int main()
{
	Base b;
	Driver d;
	cout << sizeof(b) << endl;
	cout << sizeof(d) << endl;
	return 0;
}

????运行上述代码,我们可以得到如下结果:
在这里插入图片描述????根据上述结果我们可以知道,派生类继承了基类的虚函数,而派生类d的虚函数表的生成是先将基类的虚函数表复制到自己的虚函数表中,如果派生类中重写了虚函数,就用派生类自己的虚函数在虚表中覆盖基类的虚函数,最后将自己虚函数按声名顺序加入虚表中。虚函数存在于代码段中。

3.2、多态实现原理

????首先看下面测试代码:

#include <iostream>
using namespace std;
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	int _p = 1;
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	int _s = 2;
};
int main()
{
	Person Mike;
	Student Johnson;
	Johnson._p = 3; //以便观察是否完成切片
	Mike.BuyTicket();
	Johnson.BuyTicket();
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

????根据上面的测试代码可发现,对象Mike中包含一个成员变量_p和一个虚表指针,对象Johnson中包含两个成员变量_p和_s以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。
在这里插入图片描述????而多态的的原理就是在对象调用虚函数的时候会在自己的虚表指针指向的虚函数表中查找对应的虚函数进行调用,不同的对象调用不同的虚函数实现不同的虚函数功能,达到同一个函数不同状态的效果。

Person* p1 = &Mike;
Person* p2 = &Johnson;

????上述代码属于切片行为,切片是会让父类指针或引用见派生类对象的内容切出来。然后虽然指针是父类指针,但是调用的认识派生类的虚函数。
在这里插入图片描述
????下面令一种切片方法是不构成多态的,也就是派生类对象直接赋值给派生类的方法:

Person p1 = Mike;
Person p2 = Johnson;

????在派生类对象赋值给基类对象的时候,会调用父类的赋值拷贝构造生成父类对象进行赋值,,而拷贝构造出来的父类对象的虚表指针式指向父类的虚函数表,因此不构成多态。
在这里插入图片描述

3.3、静态绑定和动态绑定

????静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。
????动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

4、常见面试题

????1,下面程序输出结果是什么?(A)虚拟继承,重复的内容指保留一份,存在同一个地方,同一个成员也就构造一次

#include <iostream>
using namespace std;
class A
{
public:
	A(char* s) { cout << s << endl; }
	~A() {};
};
class B : virtual public A
{
public:
	B(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class C : virtual public A
{
public:
	C(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class D : public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4)
		:B(s1, s2)
		, C(s1, s3)
		, A(s1)
	{
		cout << s4 << endl;
	}
};
int main()
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

//A.class A?class B?class C?class D
//B.class D?class B?class C?class A
//C.class D?class C?class B?class A
//D.class A?class C?class CBemsp;class D

????2,下面说法正确的是?

class Base1
{
public:
	int _b1;
};
class Base2
{
public:
	int _b2;
};
class Derive : public Base1, public Base2
{
public:
	int _d;
};
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

//A.p1 == p2 == p3
//B.p1 < p2 < p3
//C.p1 == p3 != p2 对
//D.p1 != p2 != p3

????3,下面说法正确的是?

#include <iostream>
using namespace std;
class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	virtual void test()
	{
		func();
	}
};
class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* p = new B;
	p->test();
	return 0;
}
//A.A->0?
//B.B->1?对
//C.A->1?
//D.B->0

????4,什么是多态?
多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。多态又分为静态的多态和动态的多态,静态的多态
????2、什么是重载、重写(覆盖)、重定义(隐藏)?
????重载是指两个函数在同一作用域,这两个函数的函数名相同,参数不同。
????重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。
????重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同。若两个基类和派生类的同名函数不构成重写就是重定义。
????3、多态的实现原理?
????构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。
????4、inline函数可以是虚函数吗?
????我们知道内联函数是会在调用的地方展开的,也就是说内联函数是没有地址的,但是内联函数是可以定义成虚函数的,当我们把内联函数定义虚函数后,编译器就忽略了该函数的内联属性,这个函数就不再是内联函数了,因为需要将虚函数的地址放到虚表中去。
????5、静态成员函数可以是虚函数吗?
????静态成员函数不能是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚表,所以静态成员函数无法放进虚表。
????6、构造函数可以是虚函数吗?
????构造函数不能是虚函数,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的,
????7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
析构函数可以是虚函数,并且最后把基类的析构函数定义成虚函数。若是我们分别new一个父类对象和一个子类对象,并均用父类指针指向它们,当我们使用delete调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数分别对父类和子类对象进行析构,否则当我们使用父类指针delete对象时,只能调用到父类的析构函数。
????8、对象访问普通函数快还是虚函数更快?
????对象访问普通函数比访问虚函数更快,若我们访问的是一个普通函数,那直接访问就行了,但当我们访问的是虚函数时,我们需要先找到虚表指针,然后在虚表当中找到对应的虚函数,最后才能调用到虚函数。
????9、虚函数表是在什么阶段生成的?存在哪的?
????虚表是在构造函数初始化列表阶段进行初始化的,虚表一般情况下是存在代码段(常量区)的。
????10、C++菱形继承的问题?虚继承的原理?
????菱形虚拟继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。
????什么是抽象类?抽象类的作用?
????抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,因为子类若是不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 18:48:02  更:2022-06-29 18:51:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/19 16:08:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码