C++多态

C++教学总目录

1、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
大家应该都在12306上面买过票,或者去旅游景区线上买票,不同人群买票有不同的动作,成人买票默认就是全价票,大学生买票是半价票,军人买票是优先买票。不同人群买票有不同的状态,这就是多态。


2、多态的定义和实现

2.1、多态的构成条件

多态的条件
1、必须通过基类的指针或引用调用虚函数
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

下面给出虚函数和重写的概念:
虚函数:被virtual修饰的类成员函数称为虚函数
重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

下面给出代码:

class Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票-半价" << endl;
	}
};

void func(Person& p)
{
	p.BuyTecket();
}

int main() 
{
	Person p;
	Student s;
	func(p);
	func(s);
	return 0;
}

在这里插入图片描述
可以看到,当传过去的是Person对象时,调用的是Person对象的买票函数。当传过去的是Student对象,发生了赋值兼容转换,但是调用的是Student的买票函数。
通过多态,实现了指向父类调父类,指向子类调子类。记住多态的调用看的是指向的对象。如果不满足多态,看当前调用者类型。


下面把func函数中的参数换成指针,由于重写了虚函数,并且是基类的指针,所以也是满足多态条件的(基类的指针/引用都满足)。

class Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTecket()
	{
		cout << "买票-半价" << endl;
	}
};

void func(Person* p)
{
	p->BuyTecket();
}

int main() 
{
	Person p;
	Student s;
	func(&p);
	func(&s);
	return 0;
}

在这里插入图片描述

但是如果是基类对象就不是多态了,因为只有是基类的指针/引用才满足多态,如果是基类对象调用的都是Person类中的买票函数,因此输出都是买票-全价。
在这里插入图片描述


但是虚函数重写还有两个例外:
1、派生类的重写虚函数可以不加virtual
在这里插入图片描述
如图:派生类重写的虚函数没有加virtual,但是还是构成多态。

2、协变,返回值可以不同,但是要求必须是父子类的指针或引用。
在这里插入图片描述
如图,基类虚函数返回值为Person的指针,派生类虚函数返回值是Student的指针。

必须是Person和Student类的指针吗?不一定,下面使用A类和B类指针做测试,A类为基类,B类为派生类。
在这里插入图片描述

返回父子类的引用也是一样的,这里就不再演示了,大家可以自行测试。

总结:
多态的条件:
1、基类的指针或引用调用重写的虚函数。
2、调用函数必须是重写的虚函数。

重写虚函数的条件:virtual + 三同(返回值相同、函数名相同、函数参数列表相同)但是有两个例外:
1、派生类重写的虚函数可以不加virtual
2、协变,重写虚函数的返回值可以不同,但是必须是父子类的指针/引用


2.2、面试题:析构函数可以是虚函数吗?为什么需要是虚函数?

先给出代码:

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main() 
{
	Person p;
	Student s;
	return 0;
}

1、析构函数加virtual,是不是虚函数重写?
你可能认为不是,因为他们函数名不一样。但是实际上是虚函数重写,因为析构函数都被处理成destructor这个统一的名字。

2、为什么要这么处理呢?
因为要让它们构成重写。

3、为什么要让它们构成重写?
上面的代码,你不加virtual构成虚函数重写也是可以的。
但是下面的场景就会出问题了:

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[20];
};

int main() 
{
	Person* p = new Person();
	delete p;
	p = new Student();
	delete p;
	return 0;
}

如果不实现虚函数重写,那么就无法构成多态。这里我们期望p第二次调用析构函数时调用的是Student类的析构函数,Student类的析构函数会释放掉_p,然后再去调用基类的析构函数,这样就不会内存泄漏。
如果不实现虚函数重写,那么就是普通调用,普通调用看的是当前类型,当前类型是Person*的指针,所以指向父类的那一部分,调用的就是父类的析构函数,因此子类的_p就没有释放掉,导致了内存泄漏。


2.3、override和final关键字

final:修饰虚函数,表示该虚函数不能被重写。
在这里插入图片描述

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
在这里插入图片描述


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

在这里插入图片描述


2.5、多态练习题

以下代码输出什么?
在这里插入图片描述

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;
}

分析如下图:
在这里插入图片描述

再来看,下面的代码输出什么?

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
	virtual void test() { func(); }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

这时候就不是多态调用了,因此输出B->0


3、抽象类

在虚函数的后面写上=0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

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;
	}
};

class BYD : public Car
{
public:
	virtual void Drive()
	{
		cout << "BYD-比亚迪" << endl;
	}
};

void func(Car* p)
{
	p->Drive();
	delete p;
}

int main()
{
	func(new Benz());
	func(new BMW());
	func(new BYD());
	return 0;
}

在这里插入图片描述


4、多态的原理

4.1、虚函数表

这里常考一道笔试题:sizeof(A)是多少?

class A
{
public:
	virtual void Func1() { cout << "Func1()" << endl; }
protected:
	int _a = 1;
};

int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

正常你可能认为是4,但是实际答案为8。因为Func1是虚函数,存储在虚函数表中,而A需要多存储一个指针变量来寻找虚函数表。
我们可以调试看看a对象:

在这里插入图片描述

_a变量占了4个字节,然后_vfptr是虚函数表指针,指向了虚函数表,所以又占了4个字节,内存对齐后恰为8字节。


4.2、多态条件的深入思考

总结前面多态的条件:
1、父类的指针或引用(思考:为什么不能是子类的指针或引用?为什么不能是父类对象?)
2、虚函数的重写(这个很好理解,如果不重写虚函数就不可能实现多态。)

为什么不能是子类的指针或引用?
因为只有父类的指针或引用可以既指向父类,又指向子类,当我指向父类,我可以到父类虚函数表中去找虚函数,当我指向子类,我就到子类的虚函数表中去找虚函数。而子类的指针或引用只能指向子类,不能指向父类,因此不能是子类的指针或引用。

为什么不能是父类对象?
先给出代码:

class Person
{
public:
	virtual void Func1() { cout << "Person::Func1()" << endl; }
	virtual void Func2() { cout << "Person::Func2()" << endl; }
protected:
	int _a = 1;
};

class Student : public Person
{
public:
	virtual void Func1() { cout << "Student::Func1()" << endl; }
protected:
	int _b = 2;
};

int main()
{
	Person p;
	Student s;
	p = s;
	Person* ptr = &s;
	Person& ref = s;
	return 0;
}

在这里插入图片描述


4.3、虚表存储位置

虚表是存储在哪里的?
A、栈 B、堆 C、数据段(静态区) D、代码段(常量区)

我们直接通过以下代码来判断:

class Person
{
public:
	virtual void Func1() { cout << "Person::Func1()" << endl; }
	virtual void Func2() { cout << "Person::Func2()" << endl; }
protected:
	int _a = 1;
};

class Student : public Person
{
public:
	virtual void Func1() { cout << "Student::Func1()" << endl; }
protected:
	int _b = 2;
};

int main()
{
	Person p;
	Person pp;
	Student s;
	int a = 10;
	int* ptr = new int;
	static int b = 10;
	const char* str = "hello";
	printf("栈区:%p\n", &a);
	printf("堆区:%p\n", ptr);
	printf("静态区:%p\n", &b);
	printf("常量区:%p\n", str);
	printf("虚表1:%p\n", *((int*)&p));
	printf("虚表2:%p\n", *((int*)&s));
	printf("虚表3:%p\n", *((int*)&pp));
	return 0;
}

在这里插入图片描述

查看运行结果,我们发现虚表1、2与常量区非常接近。基本上可以断定,虚表存储在常量区。并且虚表只有一份,同类型对象是共用的,而不是每个对象都单独有一份。


4.4、动态绑定和静态绑定

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


5、单继承和多继承关系的虚函数表

5.1、单继承中的虚函数表

我们先实现一个函数来打印虚函数表并调用虚函数:

typedef void (*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		FUNC_PTR f = table[i];
		printf("[%d]:%p->", i, f);
		f();
	}
	cout << endl;
}

下面给出测试代码:

class Person
{
public:
	virtual void Func1() { cout << "Person::Func1()" << endl; }
	virtual void Func2() { cout << "Person::Func2()" << endl; }
protected:
	int _a = 1;
};

class Student : public Person
{
public:
	virtual void Func1() { cout << "Student::Func1()" << endl; }
	virtual void Func3() { cout << "Student::Func3()" << endl; }
	void Func4() { cout << "Student::Func4()" << endl; }
protected:
	int _b = 2;
};

int main()
{
	Person p;
	Student s;
	int vft1 = *((int*)&p);
	int vft2 = *((int*)&s);
	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);
	return 0;
}

基于前面的介绍,我们知道p对象虚函数表中存储了Func1和Func2的地址。对象s虚函数表中存储了重写的Func1和Func2地址。那么Func3和Func4呢?有没有存储在虚函数表当中呢?
通过vs的监视窗口,我们是看不到的,如图:
在这里插入图片描述
接着我们打开内存,输入虚函数表的地址:
在这里插入图片描述
可以发现有三个函数地址。我们有充分的理由怀疑第三个地址就是Func3的地址。那么利用前面写的PrintVFT函数,我们将虚函数表指针传进去,打印函数地址并调用函数查看输出情况:
在这里插入图片描述
可以看到,派生类对象将基类对象虚函数表拷贝过来,完成重写虚函数覆盖之后,把自己类内的虚函数地址添加在后面。而Func4不是虚函数,所以不进虚函数表。以上就是单继承情况下的虚函数表。

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

在这里插入图片描述


5.2、多继承中的虚函数表

下面给出代码:

#include <iostream>

using namespace std;

typedef void (*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		FUNC_PTR f = table[i];
		printf("[%d]:%p->", i, f);
		f();
	}
	cout << endl;
}

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
protected:
	int _b1 = 1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1()" << endl; }
	virtual void func2() { cout << "Base2::func2()" << endl; }
protected:
	int _b2 = 2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
protected:
	int _d = 1;
};

int main()
{
	Derive d;
	int vft1 = *((int*)&d);
	int vft2 = *((int*)((char*)&d + sizeof(Base1)));
	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);
	return 0;
}

分析如下:
在这里插入图片描述


5.3、菱形继承中的虚函数表

下面给出代码:

#include <iostream>

using namespace std;

typedef void (*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		FUNC_PTR f = table[i];
		printf("[%d]:%p->", i, f);
		f();
	}
	cout << endl;
}

class A
{
public:
	virtual void func1() { cout << "A::func1()" << endl; }
	virtual void func2() { cout << "A::func2()" << endl; }
	virtual void func3() { cout << "A::func3()" << endl; }

protected:
	int _a = 1;
};

class B : public A
{
public:
	virtual void func1() { cout << "B::func1()" << endl; }
	virtual void func4() { cout << "B::func4()" << endl; }
protected:
	int _b = 2;
};

class C : public A
{
public:
	virtual void func1() { cout << "C::func1()" << endl; }
	virtual void func5() { cout << "C::func5()" << endl; }
protected:
	int _c = 3;
};

class D : public B, public C
{
public:
	virtual void func2() { cout << "D::func2()" << endl; }
	virtual void func6() { cout << "D::func6()" << endl; }
	virtual void func7() { cout << "D::func7()" << endl; }
protected:
	int _d = 4;
};

int main()
{
	D d;
	int vft1 = *((int*)&d);
	C* p = &d;
	int vft2 = *(int*)p;
	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);
	return 0;
}

分析如下:
在这里插入图片描述


5.4、菱形虚拟继承中的虚函数表

下面给出代码:

#include <iostream>

using namespace std;

typedef void (*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		FUNC_PTR f = table[i];
		printf("[%d]:%p->", i, f);
		f();
	}
	cout << endl;
}

class A
{
public:
	virtual void func1() { cout << "A::func1()" << endl; }
	virtual void func2() { cout << "A::func2()" << endl; }
	virtual void func3() { cout << "A::func3()" << endl; }

protected:
	int _a = 1;
};

class B : virtual public A
{
public:
	virtual void func1() { cout << "B::func1()" << endl; }
	virtual void func4() { cout << "B::func4()" << endl; }
protected:
	int _b = 2;
};

class C : virtual public A
{
public:
	virtual void func1() { cout << "C::func1()" << endl; }
	virtual void func5() { cout << "C::func5()" << endl; }
protected:
	int _c = 3;
};

class D : public B, public C
{
public:
	virtual void func1() { cout << "D::func1()" << endl; }
	virtual void func2() { cout << "D::func2()" << endl; }
	virtual void func6() { cout << "D::func6()" << endl; }
	virtual void func7() { cout << "D::func7()" << endl; }
protected:
	int _d = 4;
};

int main()
{
	D d;
	int vft1 = *((int*)&d);
	C* p = &d;
	int vft2 = *(int*)p;
	A* pp = &d;
	int vft3 = *(int*)pp;
	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);
	PrintVFT((FUNC_PTR*)vft3);
	return 0;
}

分析如下:
在这里插入图片描述


6、继承和多态常见问题

1、什么是多态?
多态分为静态多态和动态多态,静态多态就是函数重载,动态多态就是继承中重写虚函数+父类指针或引用调用

2、什么是重载、重写(覆盖)、重定义(隐藏)?
重载是在同一作用域下,函数名相同而参数列表不同(参数个数不同、参数顺序不同、参数不同)构成函数重载。重写是派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重定义是派生类中有一个与基类重名的函数,但不构成重写。

3、inline函数可以是虚函数吗?
不可以,编译器会直接忽略inline,因为虚函数要放到虚表中去,如果inline就展开了。

4、静态成员可以是虚函数吗?
不可以,没有隐藏的this指针,使用类型::成员函数的方式来调用无法访问虚函数表。

5、构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针实在构造函数中的初始化列表进行初始化的。

6、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,参考上面面试题。

7、对象访问普通函数快还是虚函数更快?
如果是普通对象,那就都一样快。如果是指针或引用对象,那么调用普通函数快。因为构成多态的话,需要到虚函数表中去找虚函数地址。

8、虚函数表是在什么阶段生成的?存储在哪里?
虚函数表在编译阶段就生成了,一般存储在代码段(常量区)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值