C++的多态以及常见面试题

373 阅读2分钟

多态的概念

概念

就是完成某种行为,不同的对象去完成就有不同的结果,举个通俗的例子,一个人的自行车丢了,普通人去派出所报案,警察对你爱答不理,县委书记去了,把整个县翻个底朝天也要帮你找到自行车。
  再比如你不小心得了病毒,你就要被关进小黑屋隔离,你的狗染上了病毒,狗就会被铁锹拍死。这就是多态的通俗解释。
  其中静态的多态就是函数重载
  

多态的定义及其实现

构成条件

不同继承关系的类对象,去调用同一个函数,会产生不同的结果,县委书记继承了普通人,县委书记可以找到自行车,普通人就找不到。

在继承中构成多态还要满足两个条件

1- 必须通过基类的引用或者指针来调用虚函数
#include<iostream>
using namespace std;
class OrdinaryPerson
{
public:
	virtual void FindBike()
	{
		cout << "警察不理你" << endl;
	}
};
class officials :public OrdinaryPerson
{
	virtual void FindBike()//构成重写
	{
		cout << "警察帮你找到自行车" << endl;
	}
};
void Func(OrdinaryPerson* people)//必须通过基类的引用或者指针来调用虚函数
{
	people->FindBike();
}
void Func(OrdinaryPerson& people)//必须通过基类的引用或者指针来调用虚函数
{
	people.FindBike();
}
int main()
{
	OrdinaryPerson Tom;
	Func(&Tom);
	officials clerk;
	Func(&clerk);
}

image.png image.png

image.png

image.png 这里就只是调用基类的成员函数了

2- 被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写。当两个虚函数的函数名,参数返回值都相同,派生的函数就对基类函数进行重写(覆盖)

image.png

什么是虚函数

virtual修饰的类成员函数就是虚函数

virtual void FindBike() { cout << "警察不理你" << endl; }

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类

型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。 还有一种特殊情况,派生类成员函数不加virtual时也是可以构成虚函数的,原因是,派生类继承了基类的虚函数属性,但是不建议这样去用

class OrdinaryPerson
{
public:
	virtual void FindBike()
	{
		cout << "警察不理你" << endl;
	}
};
class officials :public OrdinaryPerson
{
	 void FindBike()//构成重写,不加virtual也可以构成虚函数
	{
		cout << "警察帮你找到自行车" << endl;
	}
};

虚函数重写的两个意外

  • 派生类重写基类虚函数时,与基类虚函数返回值类型不同但是,也就是说基类成员函数返回基类对象的指针或者引用,派生类返回派生类对象的指针或者引用时,成为协变
    
class A{};
class B :public A
{};
class person {
public:virtual A* f()

{
	return new A;
}
};
class student :public person
{
public:virtual B* f()
{
	return new B;
	}
};
  • 析构函数的重写(基类与派生析构函数的名字不同) 如果基类的析构函数是虚函数,派生类的析构函数一旦定义,那么就构成重写,因为,子类的析构函数继承了虚属性,而且都没有返回值,对于函数名,编译器对于析构函数的名称做了特殊处理,编译后的析构函数名称统一处理为destructor
class OrdinaryPerson
{
public:
	virtual void FindBike()
	{
		cout << "警察不理你" << endl;
		
	}
	virtual ~OrdinaryPerson()
	{
		cout << "~OrdinaryPerson" << endl;
	}
};
class officials :public OrdinaryPerson
{
public:
	 virtual void FindBike()//
	{
		cout << "警察帮你找到自行车" << endl;
	}
	 ~officials()
	 {
		 cout << "~offocials" << endl;
	 }
};

还有一个问题,我们如果只是通过创建对象来调用析构函数的时候,无论析构函数是不是虚函数,都可以正确调用,是因为这是并没有通过指针或者引用去调用析构函数,但是如果我们通过指针或者引用去调用析构函数了,那么析构函数一定要构成多态才能被正确调用,例如这里的delete函数,如果析构函数不是多态,那么就会错误调用

image.png image.png

image.png 动态申请的对象如果给了父类指针管理,那么你在进行析构的时候通过的是原本的对象的指针或者引用,这时,析构函数要是虚函数才可以正确进行调用,否则全部调用了父类的析构函数了

当然这里的析构函数只写父类的就可以,因为派生类会进程继承虚函数属性,如果派生类的虚构函数没加公有,也没关系,因为它继承了父类的公有属性。

多态的原理

对于一个类,一个类的虚函数是被记录在虚函数表里面的,当然虚函数表里面是函数指针指向虚函数,这里的虚函数和普通函数一样,都是存在代码区里面的,这个类里边有一个指针的指针,指向虚函数表,我们成为虚表指针。

虚表指针:

image.png

虚表:

image.png

讲完这个,我们说一下为什么要使用指针或者引用传递才能实现多态。我们看下面的例子

#include<iostream>
using namespace std;
//构造函数私有就无法继承
class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
	void func3()
	{
		cout << "Base::func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
private:
	int _d=2;
	
};
int main()
{
	Base B;
	
	Derive d;
	B = d;
	Base& b=d;
	return 0;
	//为啥是引用或者指针传参呢,我们可以看到对象传参的时候,这里的子类对象的虚函数表指针传递给父类时这里的父类的虚函数表指针并没有变化
	//但是实际上子类和父类的虚函数表指针的值是不同的,如果使用对象传入时传入的虚函数表指针是穿不过去的,所以就调用父类的虚函数了
	// 
	//子类虚函数表指针传给父类时,这里的父类的虚函数表指针就变成子类的了,所以调用了子类的虚函数,实现了多态
}

image.png

在这里,使用引用传递时,d的虚表指针是等于b的虚表指针的,使用对象传递时,这里的B的虚表指针不是b的虚表指针。也就是说只有引用或者指针传递时,才能正确传递虚表指针,从而找到虚表。

动态绑定和静态绑定

静态绑定就是指在程序运行之前就已经知道方法是属于哪一个类的了,也就是是编译的时候就可以连接到类中,定位到这个方法。 动态绑定的话,是指在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。也就是链接的时候去找方法。

override final

C++对于函数重写的要求是比较严格的,有些情况下可能会由于疏忽,反而无法构成重载,这种错误在编译期间是不会报出,所以我们引入的关键字,override final

1.override 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

`class Car{

public:

virtual void Drive(){}

};

class Benz :public Car {

public:

virtual void Drive() override {cout << "Benz-舒适" << endl;}

};`

2.final 修饰虚函数,表示该函数不可以被重写

class Car

{

public:

virtual void Drive() final {}

};

class Benz :public Car

{

public:

virtual void Drive() {cout << "Benz-舒适" << endl;}

};

重载、覆盖(重写)、隐藏(重定义)的对比

image.png

多态常见的面试题

  1. 什么是多态 多态是C++的一种基本性质,对于不同的对象可以调用不同的方法去实现。也就是一个接口多个实现。

  2. 什么是重载、重写(覆盖)、重定义(隐藏)? 重载是指函数的名字相同,但是返回值,参数不同引起的调用不同,这些并列的重载函数在的是同一个作用域。

对于重写,是父类的虚函数被子类继承,子类也是虚函数,两个函数可以说完全相同,但是对于子类和父类的调用是调用不同的虚函数

对于重定义,两个函数在子类和父类里边,函数完全相同,但是不构成重写,就是重定义

  1. 多态的原理

派生类的指针可以赋值给基类的指针,对于虚函数的调用,取决于指针对象指向哪种类型的对象,或者对于引用派生类的对象可以赋值给基类的引用,通过基类引用调用基类,和派生类中的同名函数时,若该引用调用的是基类的对象,那么就调用基类的虚函数。否则就是派生类的虚函数。

4.inline函数是虚函数吗? 虚函数可以是内联函数,内联是可以修饰虚函数的,但是这个虚函数是不可以表现多态的性质的。因为内联函数是编译的时候就执行的,但是对于多态的时候是在运行期间,编译器是无法知道运行期间调用哪一个代码,因此虚函数在表现为多态时不可以内联

5.静态成员可以是虚函数吗? 不可以,对于虚函数而言,它有一个vptr指针,在类的构造函数中创建生成,并且只能通过this指针来访问它,它是类的一个成员,并且vptr指向保存虚函数地址的vtable,this->vptr->vtable->virtual funtion,所以静态成员连this指针都没有就不可以是虚函数了。

6.构造函数可以是虚函数吗? 不可以,对象的虚函数表指针是构造函数的初始化列表阶段生成的,如果构造函数是虚函数了,那么是谁生成的虚函数的指针呢。难道是构造函数的构造函数吗?这显然是不合逻辑的。

7.虚构函数可以是虚函数吗?什么场景下析构函数是虚函数? 可以,一般在存在继承场景的对象层次中,将基类的虚构函数声明为虚函数。

8.对象访问普通函数快,还是虚函数更快? 如果使用普通对象是一样快的,如果是指针对象或者是引用对象,则调用的普通函数快,因为构成了多态,通过虚函数指针来寻找虚函数,多了一层

  1. 虚函数表是在什么阶段生成的,存在哪的? 虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。