C++多态

C++多态

一.多态概念


在类这一方面,多态就是:
同一个父类函数由不同的子类对象去调用,产生不同的行为

二.多态定义与实现

学习了多态的概念之后,下面我们来看一下如何构成多态

1.多态的构成条件

多态的构成条件比较严格

1.被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写

2.必须通过父类的指针或者父类的引用来调用这个虚函数

那么什么是虚函数呢?
什么又是重写呢?
为什么必须要通过父类的指针或者引用来调用这个虚函数呢?
这些我们都会一一说明的
最后一点涉及到多态的原理,等我们介绍完多态的原理之后大家就明白了

2.虚函数的重写

虚函数就是被virtual关键字修饰的类的成员函数
比如:

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

Person当中BuyTicket这个函数就是一个虚函数
那么什么又是重写呢?

比方说:

class Student : public Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票-半价" << endl; 
	}
	//子类的虚函数重写父类的虚函数时,可以不用加virtual关键字
	//因此下面这种写法也是正确的
	//void BuyTicket() { cout << "学生买票-半价" << endl; }
};

学生类这个子类继承了Person类,又实现了一次BuyTicket函数
这就是重写

3.多态函数调用与普通函数调用

1.多态的演示-多态调用

介绍完虚函数重写之后
下面我们来演示一下多态调用

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

class Student : public Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票-半价" << endl; 
	}
	//子类重写父类的虚函数时,可以不用加virtual关键字
	//因此下面这种写法也是正确的
	//void BuyTicket() { cout << "学生买票-半价" << endl; }
};

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "军人买票-优先" << endl;
	}
	//void BuyTicket() { cout << "军人买票-优先" << endl; } yes
};

//这里的参数必须是父类的指针或者引用
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sr;
	Func(ps);
	Func(st);
	Func(sr);
	return 0;
}


同理父类的指针调用也属于多态调用

这种调用称为多态调用
多态调用的特点是:
对于传入的父类指针或者引用
1.如果 指向父类则会调用父类的虚函数
2.如果 指向子类则会调用子类的虚函数

2.普通调用

对于一个函数调用来说如果不满足多态调用,那么就是普通的函数调用
下面我们传入父类对象来看看:
是不是真的只有当使用父类的引用或者指针进行函数调用时才会发生多态调用?

此时我们发现:只传入父类对象的话,不会构成多态,
因此这三次调用都是调用的父类的虚函数
属于普通调用

3.为什么父类对象不能实现多态呢?

这一点我们介绍多态的原理的时候会详细说明的

4.虚函数重写的注意事项

注意:

1.子类的虚函数重写父类的虚函数时,可以不用加virtual关键字

因此下面这种写法也是正确的
void BuyTicket() { cout << "学生买票-半价" << endl; }

因为继承之后父类的虚函数依旧在子类中保持虚函数的属性
因此子类的虚函数重写父类虚函数时可以不用加virtual关键字

不过为了代码的可读性,不建议这样使用,最好还是加上virtual

2.协变:父类与子类虚函数的返回值类型可以不同

但是要求返回值必须是父子类关系的指针或者引用

例如:

关于协变大家了解即可,并不常见

5.析构函数的重写

我们之前在介绍继承的时候曾经提到过:
编译器会对析构函数名进行特殊处理,处理成destructor()
当时说这是因为多态的原因
今天我们就来解释一下为什么要这样处理呢?

如果析构函数不加virtual的话
对于下面这种情况,就会发生内存泄漏:

class Person
{
public:
	~Person()
	{
		cout << "Person类的析构函数调用" << endl;
		delete[] _p;
		_p = nullptr;
	}
protected:
	int* _p = new int[20];
};

class Student : public Person
{
public:
	~Student()
	{
		cout << "Student类的析构函数调用" << endl;
		delete[] _s;
		_s = nullptr;
	}
protected:
	int* _s = new int[30];
};

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

父类指针ptr指向子类对象,析构时应该要调用子类对象的析构函数才能对该子类对象进行析构
但是:

调用的是父类的析构函数
因此这里发生了想要析构子类对象,却调用了父类的析构函数的bug
也就发生了内存泄漏
因此析构函数建议实现成多态调用
也就是必须要把析构函数定义为虚函数
可是当子类的析构函数对父类的析构函数进行重写时,
又必须要满足析构函数的函数名相同
因此编译器特殊处理了一下,把析构函数的名字统一为destructer

因此建议父类的析构函数加上virtual,设计为虚函数

此时就可以了

6.重载,重写和隐藏的对比总结

7.C++11 override 和 final

在介绍继承的时候我们提到过,类定义的时候加上final之后
这个类就无法被继承了
其实这个final也可以用来修饰虚函数

final修饰虚函数,表示该虚函数不能再被重写

override:检查子类虚函数是否完成了对父类虚函数的重写
如果没有重写,那么编译时就会报错

三.抽象类

1.纯虚函数和抽象类的概念


就像这样:

2.接口继承和实现继承

3.抽象类的实例

//只要有一个纯虚函数,这个类就是抽象类,就无法实例化出对象
class Animal
{
public:
	//纯虚函数
	virtual void Say() = 0;
};

class Cat : public Animal
{
public:
	//子类重写父类的纯虚函数
	virtual void Say()
	{
		cout << "猫在说话" << endl;
	}
};

int main()
{
	//Animal a;//err,Animal是一个抽象类,无法实例化出对象
	//由于子类Cat重写了父类Animal这个抽象类的纯虚函数,因此Cat可以实例化对象
	Cat c;
	c.Say();
	return 0;
}

四.几道题目承上启下

下面我们来做几道题目
这几道题目起到一个承上启下的作用

1.题目1:多态调用和接口调用问题

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

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

答案是B选项

解析:
子类B继承了父类A的test函数
然后调用父类A的test函数,test函数通过this指针调用func函数

这里的this指针是A*类型,是父类的指针类型
而且子类B对func这个虚函数进行了重写,因此父类指针调用func函数时满足了多态调用

于是调用子类func函数的实现
又因为虚函数的继承是一种接口继承,
子类完成重写时重写的父类虚函数的实现,子类的虚函数用的还是父类虚函数的接口
因此缺省值val用的是父类的,因此缺省值是1

答案选B

2.题目2:虚表指针问题

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

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

如果按照内存对齐规则来计算,答案是4

可是结果是:
32位平台下是:8个字节
64位平台下:16个字节

为什么呢?
调试看一下

3.虚表指针初始化的位置

既然虚表指针也是这个类的成员变量,那么我们来看看这个虚表指针是什么时候,在哪里进行初始化的呢?

可以看出:
虚表指针是在构造函数当中生成,一般情况下虚表指针是在构造函数的初始化列表的起始位置进行初始化的

五.多态的原理

1.虚函数表

刚才借由一个题目引出了虚函数指针和虚函数表
下面我们写一份代码

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


并且通过监视窗口来看一下他们的对象内容

2.多态的原理

1.多态的原理,多态调用的本质

我们借助反汇编看一下

int main()
{
	Base b;
	Derive d;
	//普通调用,编译链接时确定函数地址
	//Func3是普通函数,进行普通调用
	Base* ptr = &d;
	ptr->Func3();

	//多态调用,运行时去虚表里面找虚函数的地址
	//Func3是虚函数函数,且Func1被子类重写了,而且是父类指针ptr所调用的,满足多态的条件,进行多态调用
	ptr->Func1();
	return 0;
}


因此我们就得出了多态的原理

2.为什么父类对象不能实现多态呢?

3.多态原理总结

4.补充


下面我们来验证一下
1.虚表是不是存在代码段?

2.一个类只有一张虚表吗?

3.静态多态和动态多态

六.经典题目

1.题目1:多继承中指针偏移问题

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
//继承顺序:先继承Base1,后继承Base2
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

根据切片的知识,我们可以得出答案选C

这个指针偏移的知识会用于我们最后打印多继承的虚函数表

2.题目2:菱形虚拟继承中对象初始化顺序问题

#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 B class D

E:class B class C class A class D

F:class A class B class A class C class A class D

答案选A
因为A只有一份,是最先初始化的
对于B和C来说,他们的成员在初始化列表当中的声明顺序就是继承顺序
因此先初始化B,再初始化C
最后初始化D,因此选A

七.不同继承体系中的虚函数表

1.打印虚表

1.如何打印?

刚才我们是通过监视窗口来查看虚表,但是VS2019编译器的监视窗口并不准确,因为子类新增的虚函数在监视窗口当中就没有显示

因此下面我们来打印一下虚表
虚表:全称是虚函数指针数组,本质是一个函数指针数组

如何打印一个函数指针数组呢?
其实跟打印指针数组是一样的,函数指针也是指针,只不过可以回调而已,没什么很神奇的地方

那么如何打印一个指针数组呢?
遍历该数组,打印每一个指针不就行了吗?

在VS编译器下虚表是以NULL结尾的,因此可以借助这一特性来打印虚表

2.开始打印

在这里,我们可以借助于函数指针能够进行回调的特性,
边打印虚函数的地址边调用该虚函数

1.typedef函数指针类型


因此我们可以typedef一下

typedef void (*VFPTR)();
2.打印函数的定义

下面就是遍历数组,打印指针即可

//参数是一个函数指针数组
//void PrintVFTable(VFPTR _vft[])
//由于数组在传参时会退化成指针,因此也可以这么写:
//因为VS编译器下虚表最后以NULL结尾,因此可以这样打印虚表
void PrintVFTable(VFPTR* _vft)
{
	for (int i = 0; _vft[i] != nullptr; i++)
	{
		printf("[%d] : %p -> ", i, _vft[i]);
		//利用函数指针回调对应的函数
		_vft[i]();
	}
	cout<<endl<<endl;
}
3.调用函数打印虚表

我们知道,虚表是由虚表指针指向的,而虚表指针存放在对象模型的前4个字节(32位平台)或者前8个字节(64位平台)当中

因此我们需要找到前4个或者前8个字节的值,然后将这个值传给这个打印虚表的函数即可完成打印

下面我们分别以32位平台和64位平台为例演示一下

32位平台:

第一步:取到该函数地址的前4个字节
前4个字节,不就正好是一个int类型的大小吗?
直接强转为int不就可以吗?
这样做是不行的,因为:
&d是一个Derive*类型,无法强转为int类型

那应该怎么办呢?可以强转为int*类型然后解引用
此时取出的就是前4个字节的值了,是一个int类型

为了方便传参,我们将这个int类型强转为VFPTR*类型
(注意:int可以强转为指针类型,但是指针类型不能强转为int)

void test()
{
	Derive d;
	d._b = 1;
	d._d = 2;
	PrintVFTable((VFPTR*)(*(int*)&d));//32位平台
}

可以看出,虚表当中的确有子类新增的虚函数(func4)
而且子类新增的虚函数放在父类虚函数的虚表当中了

64位平台:

同理,对于64位平台,指针的大小是8个字节,因此只需要取到前8个字节即可,而sizeof(long long)等于8,因此可以利用long long来打印

void test()
{
	Derive d;
	d._b = 1;
	d._d = 2;
	PrintVFTable((VFPTR*)(*(long long*)&d));//64位平台
}


2.单继承虚函数表

刚才我们已经通过打印虚表看到了单继承当中的虚表的个数和内容

因此可以得出结论:
单继承中如果父类有虚函数,那么子类就只有一张虚表

3.多继承虚函数表

1.先上结论

多继承的虚表
1:该子类有多少个有虚函数的父类,该子类就有多少个虚表
2:该子类新增的虚函数会放到第一个虚表当中
3:如果该子类所继承的父类全都没有虚函数,那么如果该子类有虚函数,那么该子类就有一张虚表

为什么需要这样呢?
因为每一个父类只要有虚函数就有实现多态的可能,都需要有独属于自己的虚表

2.打印虚表

下面我们通过打印虚表来验证一下
这里以32位平台为例进行打印

根据结论,我们可以知道Derive有两张虚表,
第一张是Base1的,另一张是Base2的
Derive新增的虚函数放在第一张虚表当中

因此我们只需要打印Base1的虚表和Base2的虚表即可
我们可以利用切片的方式来进行打印

void test1()
{
	Derive d;
	d._a = 1;
	d._b = 2;
	d._c = 3;

	//法一:利用切片
	Base1* b1 = &d;
	Base2* b2 = &d;
	PrintVFTable((VFPTR*)*(int*)b1);
	PrintVFTable((VFPTR*)*(int*)b2);
}

法二就是直接进行偏移打印

第一张虚表对应虚表指针的位置跟单继承的虚表指针的位置一样,都是&d

如何找到第二张虚表对应虚表指针的位置呢?
将&d强转为char*,然后向后偏移sizeof(Base1)个字节即可

void test1()
{
	Derive d;
	d._a = 1;
	d._b = 2;
	d._c = 3;

	//法二:硬搞
	PrintVFTable((VFPTR*)*(int*)&d);
	PrintVFTable((VFPTR*)*(int*)((char*)&d + sizeof(Base1)));
}

无论是哪一种,打印结果都是:

验证成功

4.菱形继承虚函数表

1.先上结论

首先我们要先说明:菱形继承其实就是普通的多继承,只不过具有代码冗余和二义性的缺陷

因此菱形继承的结论跟多继承的结论是一样的,只不过类A的虚函数的地址在类B当中有一份,在类C当中也有一份而已

2.打印虚表

跟打印多继承的虚表一样的操作

这里直接利用切片的指针偏移来打印了

void test1()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	B* b = &d;
	C* c = &d;
	PrintVFTable((VFPTR*)*(int*)b);
	PrintVFTable((VFPTR*)*(int*)c);
}

5.菱形虚拟继承虚函数表

1.虚函数重写二义性的问题

B和C虚继承A,
如果类A的虚函数同时被类B和类C重写了
此时类D继承了B和C形成菱形虚拟继承

如果类D没有对该虚函数进行重写,那么就会编译报错

为什么呢?
因为如果类A的指针或者引用指向类D,此时进行指向子类的多态调用,因为类A的func1函数被类B和类C都重写了,而没有被类D重写,因此出现了二义性,不知道该调用B的重写还是C的重写

如何解决:
1:问题发生在类D继承了B和C之后,因此类D重写func1就能够解决此问题,此时多态调用指向子类时调用类D的func1,不存在二义性
2:不要让类B和类C同时对func1进行重写

2.先上结论

菱形虚拟继承的虚表
1:类A单独有一份虚表
2:如果类B和类C没有自己单独定义的虚函数的话就没有虚表,此时类A单独一份虚表,类D新增的虚函数单独一份虚表
3:如果类B和类C有虚表的话,(VS2019编译器下)虚表在虚基表上面,
此时类A一份虚表,类B一份虚表,类C一份虚表,类D新增的虚函数在类B的虚表当中

3.打印虚表

依旧是利用指针偏移来做

void test1()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	B* b = &d;
	C* c = &d;
	A* a = &d;
	PrintVFTable((VFPTR*)*(int*)a);
	PrintVFTable((VFPTR*)*(int*)b);
	PrintVFTable((VFPTR*)*(int*)c);
}

八.补充总结


静态成员函数可以使用类名::的方式突破类域访问,也就是可以不通过对象(this指针)来访问,此时就没有访问虚表,而多态要通过虚表来实现,因此静态成员函数不能是虚函数

以上就是C++多态的全部内容,希望能对大家有所帮助!

转载请说明出处内容投诉
CSS教程_站长资源网 » C++多态

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买