【C++】面试官爱的C++多态八股文,这次让你彻底搞懂!

🔥小陈又菜个人主页

📖个人专栏《MySQL:菜鸟教程》 《小陈的C++之旅》《Java基础》

✨️想多了都是问题,做多了都是答案!



目录

问题引入:

1. 多态的概念

2. 多态的定义和实现

2.1. 多态的构成条件

2.2. 虚函数

2.3. 虚函数的重写

2.4. 虚函数重写的两个意外

2.4.1. 协变(派生类返回值类型与基类不同)

2.4.2. 析构函数的重写(派生类和基类的析构函数名字不同)

2.4.3. 选择题测试

2.5. C++11 final和override

2.5.1. final

2.5.2. override

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

3. 抽象类

3.1. 什么是抽象类

3.2. 接口继承和实现继承

4. 虚函数表


问题引入:

这篇文章重点介绍C++中的多态特性。前面我们知道了,派生类中可以调用基类中的方法,对于同名的函数我们有隐藏的相关概念。但是现实可能存在一个问题,就是基类中的方法和派生类中的方法是不同的,不同的对象调用的方法我们希望是不同的行为。这种复杂的行为我们称之为多态,形象地来说,就是具有不同的形态,方法的行为会根据上下文不同。

1. 多态的概念

像刚才上面讲到的,多态形象地来说就是多种形态,具体地来说就是完成完成一个任务有不同的状态,不同的对象会产生不同的状态。

下面可以举一个例子:

举个例子:

车站售票处推出根据购票身份不同从而出售与身份相对应的车票。

推出三种方案:

1. 普通成人:排队购票,每人100¥

2. 学生       :排队购票,每人 50 ¥

3. 军人       :购买预留票,无需排队,每人100¥

上面就是一个典型的多态案例,不同的身份购买到了不同的车票。我们尝试使用代码实现。

2. 多态的定义和实现

2.1. 多态的构成条件

有两个重要的机制用于实现多态:

  • 派生类中重新定义基类中的方法,叫做方法的重写(要求返回值、函数名、参数列表相同)
  • 使用虚方法(虚函数)

我们来看一下下面这段代码:

class Person
{
public:
	Person(const char* name = "张三")
		:_name(name)
	{}
	
    //这个地方使用virtual关键字
	virtual void BuyTicket()
	{
		cout << _name << "购票,需要排队,每人 100 ¥" << endl;
	}
protected:
	string _name;
};
 
class Student : public Person
{
public:
	Student(const char* name)
		:_name(name)
	{}
 
	virtual void BuyTicket()
	{
		cout << _name << "购票,需要排队,每人 50 ¥" << endl;
	}
private:
	string _name;
};
 
class Soldier : public Person
{
public:
	Soldier(const char* name)
		:_name(name)
	{}
 
	virtual void BuyTicket()
	{
		cout << _name << "购买预留票,不需要排队,每人 100 ¥" << endl;
	}
private:
	string _name;
};
 
void Buy(Person* p)
{
	p->BuyTicket();
}
 
int main()
{
	Person p("张三");
	Buy(&p);
 
	Student st("张同学");
	Buy(&st);
 
	Soldier so("报国");
	Buy(&so);
 
	return 0;
}

运行结果:

2.2. 虚函数

虚函数是指被virtual关键字修饰的函数:

    virtual void BuyTicket()
	{
		cout << _name << "购票,需要排队,每人 100 ¥" << endl;
	}

2.3. 虚函数的重写

虚函数的重写也叫覆盖,派生类(子类)有一个和基类完全相同的虚函数(也就是派生类的函数名、返回参数列表都完全相同),称此为子类的虚函数重写了基类的虚函数:

class Person
{
public:
	virtual void BuyTicket(){}
};
 
class Student : public Person
{
public:
    //派生类也可以不写virtual 但是不推荐
	virtual void BuyTicket(){}
};
 
class Soldier : public Person
{
public:
    //派生类也可以不写virtual 但是不推荐
	virtual void BuyTicket(){}
};

注意:子类的虚函数就算没有添加virtual也同样构成重写(基类的虚函数在子类继承之后依然保持虚函数属性),但是这种写法并不规范。

2.4. 虚函数重写的两个意外

2.4.1. 协变(派生类返回值类型与基类不同)

派生类重写基类虚函数时,返回值类型两者并不相同(指基类虚函数返回基类对象的指针或引用,派生类虚函数返回子类对象的指针或引用),称之为协变:

class A 
{};
 
class B : public A 
{};
 
class Person {
public:
	virtual A* f() 
	{ 
		cout << "A* f() " << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B* f() 
	{ 
		cout << "B* f() " << endl;
		return nullptr;
	}
	
};
 
int main()
{
	Person p;
	Student s;
	
	Person* ptr = &p;
	ptr->f();
 
	ptr = &s;
	ptr->f();
 
	return 0;
}

这种情况同样是构成重写的:

2.4.2. 析构函数的重写(派生类和基类的析构函数名字不同)

如果基类的析构函数已经是一个虚函数了,那么派生类的析构函数有没有virtual关键字,都与基类的构造函数构成重写。这里虽然名字并不相同,似乎违背了重写的一个定义,但是本质上是编译器知道这是一个重写的情况下,对于函数的名字统一为destructor:

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
 
class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};
 
int main()
{
	Person* p = new Person;
	Person* st = new Student;
 
	delete p;
	delete st;
 
	return 0;
}

注意:只有派生类的析构函数重写了基类的析构函数,下面的delete调用析构函数才能构成多态,换句话说,就是基类析构函数一定要使用virtual关键字。

2.4.3. 选择题测试

下面给出一段程序,回答它的输出是什么?

A: A->0      B:  B->1     C:A->1      D:B->0   E:编译出错    F:以上都不正确

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()
{
	B* p = new B;
	p->test();
	return 0;
}

我们分析一下这道题:

  • 首先进入main函数,创建了一个B类型的指针,指向一个派生类
  • 此时p - > test() 调用test,因为B是公有继承A的,所以p确实可以调用基类的公有方法test()
  • 此时进入基类的test(),注意此时的this指针类型是A*,且因为基类A中的func()函数是一个虚函数,所以派生类B中的func(),构成重写,形成多态
  • 所以调用的是派生类的test()
  • 最后一步,有关缺省值的问题,缺省值会使用基类的缺省值

2.5. C++11 final和override

2.5.1. final

final用于修饰虚函数,表示它不能再被重写。

class Car
{
public:
	virtual void Drive() final 
	{}
};
class Benz :public Car
{
public:
	virtual void Drive() 
	{ cout << "Benz-舒适" << endl; }
};

这里大家可能会有一个疑问,既然都是用了virtual关键字,那我的目的不就是希望能够重写然后形成多态吗?为什么还需要有一个final来限制虚函数的重写?其实我在学习的时候也有这个疑问,后来发现,其实这两者并不矛盾。

这里其实更准确地说,final使用了限制函数的进一步重写,也就是说virtual + final 的组合意思是:这个函数曾经参与多态机制(所以是virtual),但现在我们锁定了它的实现,不允许在进一步的派生类中修改(所以是final)

2.5.2. override

检查派生类时候重写了基类的虚函数,如果没有重写需要报错。

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

加入我们一旦去掉基类的virtual,override就会提示不能进行重写:

总的来说,override 是C++11引入的关键字,它的作用正是为了确保能够正确重写基类成员。

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

3. 抽象类

3.1. 什么是抽象类

在虚函数后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类

抽象类不能被实例化,抽象类的派生类同样不能被实例化,只有重写纯虚函数,派生类才能被实例化。也就是说纯虚函数规范了派生类必须会被重写,这一点在接口继承有很重要的体现

//抽象类 -- 不能实例化出对象
//在现实生活一般没有具体对应的实体
//间接功能:子类必须进行重写才能实例化出对象
class Car
{
public:
	//纯虚函数  --  抽象  
	//没有对象可以调用 子类必须重写才能调用
	virtual void Drive() = 0;
 
	//实现没有价值,因为没有对象会调用他
	virtual void Drive() = 0
	{
		cout << " Drive()" << endl;
	}
};

很明显证实了我们上面说到的,抽象类是不能被实例化的。

我们接下来,在派生类中重写基类中的纯虚函数:

class Car
{
public:
	void f()
	{
		cout << "f()" << endl;
	}
	virtual void Drive() = 0;
 
};
class BMW : public Car
{
public:
	//进行了重写
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
 
int main()
{
	BMW b;
	b.Drive();
	b.f();
	return 0;
}

3.2. 接口继承和实现继承

普通的函数继承是一种实现继承,目的是为了继承基类的函数,然后拥有该函数,并且使用这个函数;而虚函数的继承体现的是一种接口继承,目的是为了派生类重写,构成多态,继承的是接口。所以如果不使用多态,就不要把函数定义成虚函数

4. 虚函数表

这里给大家看一道很经典的笔试题:

//32位下 sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	return 0;
}

首先我们打印出的结果应该8(64位操作系统打印出来的是16我们稍后解释),但这显然不符合我们我们在类和对象中学到的,sizeof()只会计算类中的成员变量,而没有成员函数。

通过调试我们发现对象b中不仅有_b,还多了一个_vfptr,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

然后我们试着多添加几个虚函数,看一看它的储存机制:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};

不难发现一个虚函数对应一个指针,这些指针都存储子啊_vfptr中。

我在对比着来看普通函数:

不难发现,普通函数并没有存储在虚表中。

总结:

  • 类中有虚函数的话,就会存在虚表,虚表中存放的虚函数指针,并不是虚函数本身(虚函数和普通函数一样,本身是储存在代码段的)
  • 对象中存储的的是虚函数指针,并不是虚表!!!通过调试可以知道vs环境下,虚表储存在与代码段
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

针对上面的代码我们继续深挖一点,如果我使用一个派生类去继承包含虚函数的基类,这个派生类的实例化对象又会是怎么样的呢?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Func3()" << endl;
	}
	void Func4()
	{
		cout << "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;
	return 0;
}

通过调试我们发现:

  • 派生类的实例化对象分为两部分(基类部分、派生类部分)
  • 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表 中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 
  • 另外Func2和Func3 继承下来后是虚函数,所以放进了虚表, Func4 也继承下来了,但是不是虚函数,所以不会放进虚表

总结:

  • 先将基类中的虚表内容拷贝一份到派生类虚表
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后


(本篇完)

评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值