c++多态

目录

前言

实际场景

多态的定义

构成多态的规则

规则1:

规则2:

特殊情况

第一种:协变

第二种:析构函数的重写 

第三种:普通继承与接口继承

重写(覆盖)、重定义(隐藏)的关系

c++11的override和final

final

override

抽象类

概念

意义

多态的原理

虚函数表指针

多态的原理

关于虚表的几个常见问题

补充概念:动态绑定与静态绑定

多继承关系的虚函数表 


前言

在看多态之前,建议先看看往期关于继承的讲解,因为多态这里需要用到继承,而继承也是实现多态的基础

实际场景

1、小明和他父亲今年过年打算回老家,于是它们两个都用各自的身份证去app上买票,但它们两个买票的价格不同,小明父亲是全价买票,但小明作为一个学生买票是半价。那么卖票的app是怎么分辨它们各自的身份买票的价格呢?要实现这个逻辑我们可以使用多态的方式

 2、张三的女朋友小美是一个资深的支付宝用户,有一天,支付宝推出了一个活动“扫码抢红包”,小美扫了几毛钱,抱着尝试的心态,小美叫张三也来试试,但张三没有用过支付宝。于是小美就帮他注册了一个新的支付宝账号并且绑定实名。当张三去参加这个扫码抢红包的活动时,居然扫到了20块钱,小美很好奇为什么会这样,于是她又叫了几个朋友去参加这个活动。最终发现一个规律:为了吸引用户,越不经常用支付宝的人扫到的红包越多。那么支付宝是如何实现这个逻辑的呢?其实这个逻辑也可以用多态的方式完成

多态的定义

所谓多态,就是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为。

例如学生继承了人,人买票全价,学生买票半价

如下代码实现的就是多态

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

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		std::cout << "买票-半价" << std::endl;
	}
};
void func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	//运行结果:
	//买票-全价
	//买票-半价
	return 0;
}

上述代码,实现了多态调用,即传给func的对象类型是Person,就调用Person的买票函数,传给func的对象类型是Student,就调用Student的买票函数。

(上述代码在构成多态的规则章节会一直用)

我们已经看到了多态的简单场景,那么怎么样才能实现多态呢?

很显然,要构成多态是需要一些规则的,我们接下来就是要详细了解这些规则,以及一些特殊的情况

构成多态的规则

规则1:

构成多态的两个函数必须满足虚函数重写规则

 首先,我们需要先了解一下什么是虚函数

虚函数:被virtual修饰的函数就是虚函数

重写:就是在一个父子继承类中的同名函数的返回值和参数类型都相同,总结一下就是三同(返回值、参数类型、名字),并且满足重写的两个函数必须是虚函数,例如上面的代码实现中,Person类和Student类的Buyticket函数就满足重写,并且我们称子类的虚函数重写了父类的虚函数

规则2:

必须通过基类的指针或者引用调用虚函数

也就是说在上面的代码实现中,如果func函数的形参不是Person&或Person*,那么就无法实现多态调用

上述,我们说的两个规则都是普遍情况,但有时候会出现特殊情况,也就是不满足上述两个规则但也构成多态的情况

特殊情况

第一种:协变

协变说的是在有些情况下,虚函数的返回值不同也能构成多态

但协变也有要求,就是虚函数的返回值必须是父子类关系的指针或者引用,如下代码

class Person
{
public:
	virtual Person& BuyTicket()
	{
		std::cout << "买票-全价" << std::endl;
		return *this;
	}
};

class Student : public Person
{
public:
	virtual Student& BuyTicket()
	{
		std::cout << "买票-半价" << std::endl;
		return *this;
	}
};
void func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	//运行结果:
	//买票-全价
	//买票-半价
	return 0;
}

 上述代码演示的协变是父子类关系的引用,而父子类关系的指针同样也可以

第二种:析构函数的重写 

析构函数的重写说的是两个类的析构函数名字不相同,但它们可以构成重写关系

如下代码A类和B类的析构函数构成重写

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

class B : public A
{
public:
	virtual ~B()
	{
		std::cout << "~B()" << std::endl;
	}
};

 对于析构函数的重写这种特殊情况,其实是有意义的,如下场景中就一定要用到多态,而多态就要满足虚函数的重写

class A
{
public:
	~A()
	{
		std::cout << "~A()" << std::endl;
	}
};

class B : public A
{
public:
	~B()
	{
		std::cout << "~B()" << std::endl;
	}
};

int main()
{
	A* pA = new A;
	delete pA;
	A* pB = new B;
	delete pB;
    //运行结果:
    //~A()
    //~A()
	return 0;
}

我们可以看到,在上述代码中,pA指向的对象只调用一次A类的析构是没问题的,但pB指向的对象是一个B类型,正常来说是要先调用一次B类析构,再调用一次A类析构,但上述代码调用析构函数的时候只调用了一次A类的析构函数,B类的析构函数并没有调用,这是由于上述代码是普通调用,而不是多态调用,对于普通调用是根据指针的类型来调用析构的,多态调用是根据指针指向的类型,上述场景就必须要用到析构函数的多态调用,而用到多态调用就必须满足析构函数的重写

实际上,我们只从表面上看的话会感觉A类的析构和B类的析构明显不是一个名字,但为了实现析构函数重写,编译器会把父子类的析构函数统一改名为destructor,所以从底层来看,析构函数的重写并没有违背多态必须满足函数名相同这个条件

第三种:普通继承与接口继承

 父类的虚函数加上virtual关键字,子类不加virtual关键字也能构成多态

如下调用也是多态调用

class Person
{
public:
	virtual void BuyTicket()
	{
		std::cout << "买票-全价" << std::endl;
		return *this;
	}
};

class Student : public Person
{
public:
	void BuyTicket()//子类不加virtual
	{
		std::cout << "买票-半价" << std::endl;
		return *this;
	}
};
void func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	func(p);
	func(s);
	//运行结果:
	//买票-全价
	//买票-半价
	return 0;
}

对于为什么上述代码也能实现多态,是因为当满足多态的时候我们所说的重写其实是重写定义,那么当父类加上virtual关键字以后,子类会把父类的虚函数声明接口继承下来,而定义则由子类自己提供。

若不满足多态,此时是普通继承,子类会把父类的接口和实现都继承下来

所以如果不实现多态,不要把函数定义成虚函数。

但注意:虽然这里的子类没有加上virtual关键字,但也是一个虚函数

重写(覆盖)、重定义(隐藏)的关系

相同点:

1.两个函数分别在基类和派生类的作用域

2.函数名相同(析构函数的重写在表面看不相同,但底层是相同的)

不同点:

相比于隐藏,重写的要求更高,主要体现在以下两点

1.重写要在满足隐藏的条件上还要满足返回值和参数类型相同,而隐藏只需要满足函数名相同即可

2.重写的两个函数必须是虚函数,而隐藏不要求两个函数是虚函数

c++11的override和final

final

我们先讨论一个问题,现在有一个需求就是实现一个类,这个类不能被继承,如何实现呢?

第一种方法:我们可以通过把这个类的构造函数私有,此时如果有另一个类继承了它,那么这个类调用自己的构造函数时就必须调用父类的构造函数,但父类构造函数私有了,此时就会报错

如下代码:

 

//实现一个A类,其他类不能继承A类
class A
{
private:
	A()
	{}
};

class B : public A
{
    //子类默认构造会调用父类的构造
};
int main()
{
	B b;//报错
	return 0;
}

第二种方法:c++11中提供了新的关键字final,这个关键字的作用就是让这个关键字修饰的类为最终类,从而无法被继承 

//实现一个A类,其他类不能继承A类
class A final//final修饰类无法被继承
{
public:
	A()
	{}
};

class B : public A
{

};
int main()
{
	B b;//报错
	return 0;
}

 final除了上述作用以外还有一个作用,当final修饰的是函数时,该函数无法被重写,如下代码

//final修饰函数,该函数无法被重写,如下代码会报错
class A 
{
public:
	virtual void func() final
	{}
};

class B : public A
{
	virtual void func()
	{}
};

override

当我们进行重写的时候,要求是子类必须重写父类,但可能由于各种各样的原因可能会发生子类没有重写父类的情况,而c++11就提供了一个关键字override,这个关键字修饰子类函数时要求子类虚函数必须发生重写,如果没有重写就会报错,如下代码会发生报错

//父类没有虚函数,子类无法重写,故如下代码也会报错
class A 
{
public:
	void func() 
	{
		std::cout << "A::func()" << std::endl;
	}
};

class B : public A
{
	virtual void func()override
	{
		std::cout << "B::func()" << std::endl;
	}
};

抽象类

概念

要了解抽象类我们先了解下什么是纯虚函数

纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

而包含纯虚函数的类叫做抽象类(也叫接口类),抽象类无法实例出对象,如下

//如下代码,报错
class A
{
public :
	virtual void func() = 0;
};

int main()
{
	A a;//抽象类无法实例化出对象
	return 0;
}

当然,抽象类除了上述这个用途以外, 当一个派生类继承抽象类的时候,如果没有发生纯虚函数的重写,那么派生类也无法实例化出对象,因为如果这个派生类没有发生重写那么他就会把纯虚函数普通继承下来,而有纯虚函数的类是抽象类,故派生类也成了抽象类,如下

class A
{
public :
	virtual void func() = 0;
};

class B : public A
{
};

int main()
{
	A a;//抽象类无法实例化出对象
	B b;//B把A中纯虚函数的接口和实现继承下来,B也是抽象类,无法实例化出对象
	return 0;
}

 所以抽象类也规范了他的派生类必须重写纯虚函数,纯虚函数更体现出了接口继承

意义

抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。

而所谓的抽象类,也就是把众多类抽取出共同的特征

就比如学校的图书管理系统,对于图书管理系统中的每个对象其实都有其所对应的身份,就好比老师、学生、保安、校长等,但这些身份的共同特征都是人,于是我们可以把人这个特征抽离出来弄成抽象类,这会使得这个图书管理系统结构更完善

多态的原理

虚函数表指针

先看一道常见的面试题:如下代码运行结果为

class A
{
public:
	virtual void func()
	{}
	int _a;
	char _ch;
};

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

 答案是12,其实如果按照我们以前普通类来分析,类中不存储函数,那么类的大小就是int+char,根据内存对齐规则应该是8,但注意若一个类中有虚函数,那么除了自身的成员函数之外,这个类还会存储一个指针,即__vfptr,其实v指的是virtual,f指的是function,而它指向的内容其实是一个虚函数表,我们称这个指针为虚函数表指针,简称虚表指针

 而虚函数表指针实际上是一个函数指针数组,里面存储的是虚函数的地址

多态的原理

我们上面说过,只要一个类中有虚函数那么它就会有一个虚函数表指针存储函数的地址

那么根据多态的条件,满足多态的子类和父类都肯定有一个虚函数表指针

而当我们有了虚函数表指针之后,就能用虚函数指针实现多态,具体过程如下

以下代码为实现多态的两个类

class Person
{
public:
	virtual void func1()
	{
		std::cout << "Person::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Person::func2()" << std::endl;
	}
	virtual void func3()
	{
		std::cout << "Person::func3()" << std::endl;
	}
};
//func1和func2完成重写,func3是虚函数但不重写
class Student : public Person
{
public :
	virtual void func1()
	{
		std::cout << "Student::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Student::func2()" << std::endl;
	}
};

第一步,对于Person类的虚函数表指针,Student类会继承下来,不过会对重写的虚函数地址进行修改,改为自己重写的虚函数地址,如图

我们可以看到,对于没有重写的虚函数func3,这两个类的地址是完全相同的,不同的是发生重写的func1和func2函数

第二步:当Person的指针指向Student类型的时候,会发生切片现象,此时父类指针指向的就是Student对象中属于父类的一部分,对于上面没有成员变量的类来说,子类中属于父类的一部分就是虚函数表指针,而此时,对于父类的指针p2,虽然它指向的对象是子类,但可以通过虚函数表指针找到对应的函数地址,不管是父类还是子类,都能通过统一的方式(虚函数表指针)来找到其所对应的函数,

注意:子类来说,如果它重写了某个虚函数,此时它的虚表当中存储的地址是自己重写的虚函数

于是对于上述调用的模型就如下图:

关于虚表的几个常见问题

第一个:虚函数存在哪里?

这里要注意虚表中存储的只是虚函数的地址,而虚函数跟普通成员函数一样存储在代码段

第二个:虚表存在哪?

对于这个问题,我们通过代码进行测试

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



#include <iostream>
#include <utility>
#include <stdio.h>
int main()
{
	int a;//栈
	static int b;//静态区
	int* p1 = new int;//堆
	const char* p2 = "xxxxx";//常量区
	Person p;
	Person* p4 = &p;
	int d1 = *(int*)p4;
	int d2 = (int)&a;
	printf("Person虚表地址与栈区的距离(单位字节):%ld\n", std::abs(d1-d2));
	d2 = (int)&b;
	printf("Person虚表地址与静态区的距离(单位字节):%ld\n", std::abs(d2-d1));
	d2 = (int)p1;
	printf("Person虚表地址与堆区的距离(单位字节):%ld\n", std::abs(d2 - d1));
	d2 = (int)p2;
	printf("Person虚表地址与常量区的距离(单位字节):%ld\n", std::abs(d2 - d1));
	return 0;
}

经过测试,vs下的虚表在代码段/常量区

补充概念:动态绑定与静态绑定

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

实际上,函数重载编译阶段根据函数参数名修饰规则进行的,在表面上我们可以说它是自动调用参数匹配的函数,但本质上其实函数重载也是一种多态

多继承关系的虚函数表 

之前,我们聊的都是关于单继承的虚函数表的问题,而在c++中除了有单继承外还有多继承,在聊多继承之前我们先看看如下代码的运行结果是什么?

 

class Student
{
public:
	virtual void func1()
	{
		std::cout << "Student::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Student::func2()" << std::endl;
	}
	int _a;
};

class Teacher
{
public:
	virtual void func1()
	{
		std::cout << "Teacher::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Teacher::func2()" << std::endl;
	}
	int _b;
};

class Assistant : public Student , public Teacher 
{
public:
	virtual void func1()
	{
		std::cout << "Assistant::func1()" << std::endl;
	}
	int _c;
};

int main()
{
	std::cout << sizeof(Assistant) << std::endl;
	return 0;
}

如果按照之前单继承的规则来玩,这里的Assistant类继承了两个int成员以及自己的一个int成员加上一个虚表指针那么答案应该是16,可真的是这样吗?

实际上,这里的答案是20,这恰恰说明了一点,那就是这里的Assistant类中继承了两张虚表,一个是Student类的虚表,一个是Teacher类的虚表

于是我们可以得出初步结论:多继承的类中可以有多张虚表,继承了几个有虚函数的类就有几张虚表

但是如果发生切片现象的话怎么办?解决方法如下

但是,如果上述的Assistant类自己还有虚函数的话是放在Student类虚表还是Teacher类虚表还是两个都放呢?也就是如下情况Assitant类中的虚函数func3放到哪里?

class Student
{
public:
	virtual void func1()
	{
		std::cout << "Student::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Student::func2()" << std::endl;
	}
	int _a;
};

class Teacher
{
public:
	virtual void func1()
	{
		std::cout << "Teacher::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Teacher::func2()" << std::endl;
	}
	int _b;
};

class Assistant : public Student , public Teacher 
{
public:
	virtual void func1()
	{
		std::cout << "Assistant::func1()" << std::endl;
	}
	virtual void func3()
	{
		std::cout << "Assistant::func3()" << std::endl;
	}
	int _c;
};

 接下来,我给出一个打印虚表的函数,可以用此函数进行测试,注意此代码打印虚函数表是根据vs下的虚函数表的最结尾有nullptr,但vs下的虚函数表可能会被增量覆盖掉,程序崩溃的话就清理一下再编译就可以了

class Student
{
public:
	virtual void func1()
	{
		std::cout << "Student::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Student::func2()" << std::endl;
	}
	int _a;
};

class Teacher
{
public:
	virtual void func1()
	{
		std::cout << "Teacher::func1()" << std::endl;
	}
	virtual void func2()
	{
		std::cout << "Teacher::func2()" << std::endl;
	}
	int _b;
};

class Assistant : public Student , public Teacher 
{
public:
	virtual void func1()
	{
		std::cout << "Assistant::func1()" << std::endl;
	}
	virtual void func3()
	{
		std::cout << "Assistant::func3()" << std::endl;
	}
	int _c;
};
typedef void(*VF_PTR)();
void PrintVFP(VF_PTR* pf)//打印虚表的函数
{
	for (size_t i = 0; pf[i] != nullptr; ++i)
	{
		std::cout << pf[i] << std::endl; //打印虚表函数地址
		VF_PTR f = pf[i];
		f();
	}
}


int main()
{
	Assistant a;
	Student* p1 = &a;
	Teacher* p2 = &a;
	std::cout << "Student:" << std::endl;
	PrintVFP((VF_PTR*)(*(int*)p1));
	std::cout << std::endl << std::endl << "Teacher:" << std::endl;
	PrintVFP((VF_PTR*)(*(int*)p2));
	return 0;
}

运行结果如下:

可以发现,如果Assistant类自己还实现了一个虚函数,他会把虚函数放到先继承的类的虚表当中 

而以上就是关于多继承关系的虚函数表的全部内容,同时本篇博文也到了尾声,关于多态这里的东西其实还有菱形继承的多态和菱形虚拟继承的多态,但这些的复杂程度太高了,日常我们也不用这些,并且也建议不要用!谢谢大家,希望大家可以多多点赞收藏评论一下,蟹蟹蟹蟹

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>多</span><span style='color:red;'>态</span>

    C++

    2024-04-24 15:26:01      48 阅读
  2. 八股文 c++

    2024-04-24 15:26:01       44 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-24 15:26:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-24 15:26:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-24 15:26:01       82 阅读
  4. Python语言-面向对象

    2024-04-24 15:26:01       91 阅读

热门阅读

  1. Ubuntu中如何压缩和解压文件

    2024-04-24 15:26:01       35 阅读
  2. JVM(1)

    2024-04-24 15:26:01       181 阅读
  3. 物联网社区信息化管理系统设计的毕业论文

    2024-04-24 15:26:01       89 阅读
  4. 面试 Python 基础八股文十问十答第五期

    2024-04-24 15:26:01       197 阅读
  5. intellij idea的快速配置详细使用

    2024-04-24 15:26:01       37 阅读
  6. Python实现深度学习

    2024-04-24 15:26:01       28 阅读
  7. 深入浅出MySQL-03-【MySQL中的运算符】

    2024-04-24 15:26:01       25 阅读
  8. System1和System2

    2024-04-24 15:26:01       32 阅读
  9. Android如何管理多进程

    2024-04-24 15:26:01       38 阅读
  10. 经典的目标检测算法

    2024-04-24 15:26:01       26 阅读
  11. python实现DIY高考倒计时小软件

    2024-04-24 15:26:01       32 阅读
  12. 迭代器模式

    2024-04-24 15:26:01       31 阅读