文章目录
- 多态的概念
- 多态的定义
- 虚函数
- ```override```和```final```关键字
- 重载、覆盖、隐藏
- 抽象类
- 抽象类的定义
- 接口继承和实现继承
- 多态的原理解析
- 虚函数表
本篇总结C++
中多态的基本内容和原理实现和一些边角内容
多态的概念
首先要清楚多态是什么,是用来做什么的?
多态从字面意思来讲,就是多种形态,完成一个事情,不同的人去完成会有不同的结果和状态,这样的情况就叫做多态
多态的定义
多态是不同继承关系的类对象,在调用一个函数的时候会产生不同的行为,比如同样是买票这个操作,普通人就是全票,学生就是半票,本篇的例子也会从这个例子出发,进行多态中具体的距离和深层次的理解
构成多态的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,并且派生类要对基类的虚函数进行重写
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通票全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "学生票半价" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
}
上面是对多态的最初始定义,也是很基础的定义,从中可以看出多态的基本用法和实现的功能
虚函数
虚函数的定义:
虚函数通俗来说,就是被virtual
修饰的类成员函数就是虚函数
虚函数的重写:
虚函数的重写就是,当派生类中有一个和基类完全相同的虚函数,那么就称之为子类的虚函数重写了基类的虚函数,虽然子类可以不加virtual
,但是并不标准,最好加上
虚函数的例外:
-
协变
协变就是,派生类重写基类虚函数的时候,与基类虚函数返回值类型不同,比如基类的虚函数返回的是基类成员的指针和引用,派生类返回的是指针和引用的时候,也算是虚函数重写,这种情况就叫做协变 -
析构函数重写
如果基类的析构函数是虚函数,那么派生类的析构函数默认会和基类的析构函数构成重写,虽然名字和函数名不同,但是依旧是,这是因为编译器进行编译后,把析构函数的名称统一处理为destructor
,这样也算是重写
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通票全价" << endl;
}
virtual Person* f()
{
return new Person;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "学生票半价" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
override
和final
关键字
C++11
中,引入了两个关键字,这两个关键字就是用来辅助进行虚函数多态的多种复杂情形,避免出现疏忽而导致错误的情况出现:
final:修饰虚函数,表示这个虚函数不能被重写了
class Student :public Person
{
public:
virtual void BuyTicket() final
{
cout << "学生票半价" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
class Child :public Student
{
public:
virtual void BuyTicket()
{
cout << "小孩免票" << endl;
}
};
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有就报错
class Person
{
public:
/*virtual*/ void BuyTicket()
{
cout << "普通票全价" << endl;
}
virtual Person* f()
{
return new Person;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket() override
{
cout << "学生票半价" << endl;
}
virtual Student* f()
{
return new Student;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
class Child :public Student
{
public:
virtual void BuyTicket() override
{
cout << "小孩免票" << endl;
}
};
void func(Person& p)
{
p.BuyTicket();
}
多态在使用的过程中是十分复杂的,因此使用时需要注意逻辑能否清楚的表示,可能只是稍微变了一点点内容,就使得整个意思全然变换,下面对比一下继承多态中的一些概念:
重载、覆盖、隐藏
- 重载指的是,函数名在一个作用域,并且函数名相同,参数不同的情况,那么这两个函数就构成了函数重载,编译器在进行处理的时候会根据参数形成不同的函数表,由此来对应不同的情况
- 重写指的是,两个函数在基类和派生类的作用域下,前提是函数名、参数、返回值都一样的情况下,如果是虚函数,那么就构成了重写,其中子类可以不写
virtual
,可以理解为虚函数的属性被从基类中继承了下来,但是并不推荐这样写,其中要注意特殊情况,比如协变和析构函数的情况 - 隐藏指的是,两个函数在基类和派生类的作用域下,当函数名相同的时候,如果不符合重写的定义那么就是重定义了,比如在继承中见到的很多种情况
抽象类
抽象类的定义
在虚函数后面写上等于0,就说明这个函数是纯虚函数,有纯虚函数的类就叫做抽象类,抽象类的特点是不可以实例化出一个具体的对象,而派生类被继承后也不能实例化对象,只有在重写了虚函数的前提下,才能实例化对象
纯虚函数体现了派生类要重写的这个规则,同时也体现出了接口继承的概念
接口继承和实现继承
- 接口继承(
Interface Inheritance
)是指从一个纯虚基类(pure virtual base class
)继承而来,目的是为了实现一个类的接口,使得派生类必须实现该接口中定义的所有纯虚函数。接口继承的主要目的是实现类的接口复用,它并不关心实现细节。在接口继承中,派生类只需要实现基类中定义的纯虚函数,不需要关心基类中其他的数据和函数
class Shape
{
public:
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape
{
public:
void draw() override
{
// 实现圆形的绘制
}
};
class Square : public Shape
{
public:
void draw() override
{
// 实现正方形的绘制
}
};
- 实现继承(
Implementation Inheritance
)是指从一个普通的基类(非纯虚基类)继承而来,目的是为了实现基类中已有的函数或数据。实现继承的主要目的是实现代码复用,它关心基类中的实现细节。在实现继承中,派生类会继承基类中所有的成员函数和数据成员,并且可以重写这些函数以改变它们的行为
class Person
{
public:
void sayHello()
{
std::cout << "Hello, I am a person." << std::endl;
}
};
class Student : public Person
{
public:
void sayHello() override
{
std::cout << "Hello, I am a student." << std::endl;
}
};
int main()
{
Student s;
s.sayHello(); // 输出: "Hello, I am a student."
return 0;
}
接口继承是指派生类只继承了基类的接口(也就是纯虚函数),而没有继承基类的实现。这种方式使得派生类必须实现基类中的所有纯虚函数,从而使得派生类和基类的实现是分离的,实现了接口和实现的分离。这种继承方式常常用于实现抽象类和接口,强制要求派生类实现接口中的所有函数
实现继承是指派生类继承了基类的接口和实现,包括数据成员和函数实现。这种方式使得派生类可以复用基类的代码,从而减少了代码的重复编写,同时也保证了派生类和基类的一致性。但是,这也意味着派生类和基类的实现是紧密耦合的,基类的修改可能会影响到派生类的行为
多态的原理解析
虚函数表
对于一个使用了多态的类,创建一个对象看其内部的内容:
会发现这当中和预想的结果并不一样,原因就在于这当中多了一个数组,这个指针数组实际上是叫做虚函数表指针数组,严格意义来说,这个数组中存放的是虚函数的函数地址,虚函数表也叫做虚表,那么问题来了:为什么要这么设计呢?
下面来做实验,对前面的类进行改造:
在Person
类中加一个虚函数和一个普通函数,而在Student
类中只重写一个虚函数:
class Person
{
public:
// Person类中有两个虚函数和一个普通函数
virtual void BuyTicket()
{
cout << "普通票全价" << endl;
}
virtual void func1()
{
cout << "void func1()" << endl;
}
void func2()
{
cout << "void func2()" << endl;
}
private:
int _person; // 定义一个变量
};
class Student :public Person
{
// 继承类中只重写一个虚函数,剩下的不进行重写
public:
virtual void BuyTicket()
{
cout << "学生票半价" << endl;
}
private:
int _student; // 定义一个变量
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
}
实验结果如下:
对实验结果进行分析得出下面的结论:
- 派生类的
Student
对象中也有一个虚表指针,其中是由两个部分组成的,一个是父类成员和自己的成员,虚表指针中也是存在的一部分是自己的成员 - 在基类和派生类中的虚表地址是不一样的,但是在虚表的具体内部中会发现,有一个函数指针地址是一样的,还有一个不一样,那么说明在
Student
类中重写的函数发生了改变,因此虚函数的重写才叫做覆盖,覆盖指的就是虚表中对于虚函数的覆盖 - 对于虚表内的内容,只有被继承下来的虚函数才会放到虚表中,其余函数不会放入虚表中
- 虚函数表本质上就是一个存放虚函数指针的指针数组,这与一开始的结论是一样的
虚函数表的生成过程:
- 基类中的虚表拷贝到派生类的虚表中
- 如果派生类中重写了虚表的某个函数,那么就进行覆盖的过程
- 派生类自己新有的虚函数按照在类内的次序放到派生类虚表的最后
虚函数和虚表的存储位置:
虚函数存放在虚表,虚表存放在对象中,这样的回答是错误的!
虚表中存的是虚函数的指针,并不是虚函数,虚函数和普通的函数是一样的,都是存放在代码段中,只是将它的指针放到了虚表中,而在对象中存放的是虚表的指针,也不是虚表,虚表在vs
下也是存放在代码段的位置中
多态的调用原理:
在知道了虚表的存在和原理后,其实可以理解前面的一些内容了
当指向的对象是Person
类的时候,此时会在Person
类的虚表中找到对应的函数并进行调用,当对象是Student
类的时候,原理相同,借助这个原理就实现了多态,用不同的对象去运行会产生不同的结果,而多态的函数调用也不是直接确认的,而是在运行的过程中,在对象的内部自动取识别,去获取的
动态绑定和静态绑定
静态绑定也叫做前期绑定或者是早绑定:在程序编译期间就确定了程序的行为,也叫做静态多态,比如说函数重载就是比较典型的例子
动态绑定也叫做后期绑定或者是晚绑定:在程序运行期间,根据具体拿到的类型来确定程序的具体行为和调用的具体函数,比如说动态多态就是这样的例子