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++菱形继承的问题?虚继承的原理?
    菱形虚拟继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。
    什么是抽象类?抽象类的作用?
    抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,因为子类若是不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

滋巴糯米团

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

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

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

打赏作者

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

抵扣说明:

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

余额充值