多态
1. 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
例如:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
2. 多态的定义以及实现
2.1 多态构成条件
多态是在不同继承关系的类对象,去调用同一函数,并产生了不同的行为。
想要构成多态需要满足三个条件:
- 两个类必须是继承关系
- 必须通过基类的指针或者引用去调用虚函数
- 被调用的必须为虚函数,并且在派生类中必须对虚函数进行重写。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
void Func(Person& p)
{
p.BuyTicket();
}
2.2 虚函数
虚函数:就是被virtual修饰的成员函数称为虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.3 虚函数的重写
虚函数的重写(覆盖) : 派生类中有一个跟基类完全相同的虚函数(三同:虚函数名相同、返回值相同、参数相同),称为子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
虚函数重写的两个例外
- 协变(基类和派生类中的虚函数返回值类型不同)
派生类重写虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
注意:基类虚函数的返回类型和派生类返回类型必须构成继承关系。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
- 析构函数的重写(基类与派生类的析构函数名不同)
若基类的析构函数为虚函数,这时只要派生类只要定义析构函数,无论加不加virtual都与基类的析构函数都构成重写,虽然基类与派生类析构函数的名字不同。其实在这里编译器对析构函数做了特殊处理,编译后的析构函数名称都为destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
- 派生类虚函数重写可以不加virtual 。(建议:派生类的虚函数都加上virtual)
例题:以下程序输出结果是什么()
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(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
答案:B
原因是虚函数的重写,重写的是函数体的实现。函数的结构部分(函数名参数返回值等)用的都是父类的。
2.4 override 和 final
- final :修饰虚函数表示该函数不能被重写。
- override:检查派生类的某个虚函数是否重写了,若没有则编译报错。
2.5 重载、重写(覆盖)和重定义(隐藏)的对比
重载 :
- 两个函数在同一个作用域下
- 函数名相同、参数不同
重写(覆盖) :
- 两个函数分别在基类和派生类
- 函数名、参数、返回值必须相同(协变除外)
- 两个函数都为虚函数
重定义(隐藏):
- 两个函数分别在基类和派生类中
- 函数名相同
- 两个函数基类和派生类中的两个同名函数不构成重写就是重定义
3. 多态的原理
虚函数指针和虚函数表
首先我们来看一则代码并计算一下sizeof(Base)的大小:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
通过监视窗口我们发现b对象是8bytes,除了b成员之外还有一个_vfptr放在前面,对象中的这个指针我们叫做虚函数表指针。每个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址都要放到虚函数表(虚表)中。
为了近一步的了解虚函数表,观察以下代码:
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;
}
通过监视窗口我们可以发现,基类和派生类中除了本身函数的成员外,都有一个虚表指针,这个虚表指针指向的各自类的虚表。虚函数表存放的是各自类中虚函数的地址。
对象 d 虽然继承了 b对象中Func()1 和Func()2 ,但是d对Func1()进行了重写。所以我们在监视窗口看到 d对象的虚表中存放的是重写的Func1()和Func2()。这里解释了为什么重写也叫做覆盖,覆盖就是对虚表中的虚函数地址的覆盖。
注意: 虚表中只存放虚函数的地址,其他的成员函数不能进虚表中。虚函数表本身就是一个存放指针的指针数组,一般情况下这个数组最后一个元素为nullptr。
虚表
原理
下面代码中Person对象Mike 和Student对象Johnson传参给p ,调用的BuyTicket却不同。
多态是如何实现的呢?
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
int _i = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _j = 2;
};
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person Mike;
Func(&Mike);
Student Johnson;
Func(&Johnson);
return 0;
}
通过监视窗口我们发现,对象Mike中有虚表指针和成员变量_i,对象Johnson中有两个成员变量和虚表指针,并都指向各自的虚函数表。
当Mike传参给p时,p->BuyTicket会在Mike虚表中找到对应的虚函数Person::BuyTicket。
当Johnson传参给p时,p->BuyTicket会在Johnson虚表中找到对应的虚函数Student::BuyTicket。
这样就实现了不同对象去做同一件事情,表现出不同的状态。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
接下来我们可以从汇编的角度更加深入理解动态绑定和静态绑定
不满足多态
代码:
class Person {
public:
void BuyTicket() { cout << "买票-全价" << endl; }
private:
int _i = 1;
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _j = 2;
};
int main()
{
Person p;
Student s;
p = s;
p.BuyTicket();
return 0;
}
通过调试查看汇编码,发现只有两条汇编指令,并且是在程序编译是就确定的。
满足多态
代码:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
int _i = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _j = 2;
};
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Student s;
Person* p = &s;
p->BuyTicket();
return 0;
}
通过调试发现,相对于不构成多态所生成的汇编指令,这里生成的汇编指令更多。原因是程序在运行时,还要在对象的虚表中找到所调用的虚函数。这里就体现了动态绑定是在运行时确定的。
4. 抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
class Car
{
public:
virtual void Drive() = 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;
}
};
接口继承和实现继承
- 普通函数继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承的是一种接口继承,派生类继承的是基类虚函数的接口,目的就是重写虚函数,实现多态,继承的是接口。
5. 单继承中的虚函数
class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};·································
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};
通过监视窗口我们发现对象d中的虚表跟我们想象的不一样,虚表中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,解决这个问题有两种办法一是通过调试查看内存窗口、二是使用指针的方式打印虚表中的内容。
法一:
法二:
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
6. 多继承中的虚函数
既然有单继承自然也就有多继承,那我们思考一下?多继承的派生类有几个虚表?派生类中的自身虚函数放在那个虚表中?
运行以下代码:
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
//通过切片的方式
Base2* b2 = &d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)(b2));
PrintVTable(vTableb2);
return 0;
}
通过运行结果来看多继承派生类中的虚函数地址放在第一个继承基类部分的虚表中。
7. 继承和多态的有关问题
- 什么是多态?答:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
- 什么是重载、重写(覆盖)、重定义(隐藏)?答:重载:在同一作用域中、函数名相同、参数不同。重写:在不同作用域中、函数名、参数、返回值(协变除外)相同、都为虚函数。 重定义:在不同作用域中、函数名相同即可、两个函数基类和派生类中的两个同名函数不构成重写就是重定义。
- 多态的实现原理?答:通俗点来说就是到指向对象的虚表中找虚函数调用,指向父类调用父类的虚函数,指向子类调用子类的虚函数。
- inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。
- C++菱形继承的问题?虚继承的原理?答:菱形继承会导致子类中会用两份父类成员,因此会出现数据冗余和二义性的问题。虚基类只会在派生类中存放一份,通过虚基表中的偏移量找到并访问虚基类成员。
- 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。