多态
我们知道多态是c++语言特性非常重要的一大特性,今天就让我们一起来研究总结c++中的多态特性
1.多态的概念
1.1什么是多态
百度百科:
- 多态(Polymorphism)按字面的意思就是"多种状态"。在面向对象语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述–多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
我们可以自己简单的总结为一句话:去完成某个行为时,不同的对象去完成会产生不同的结果就叫多态(一个接口,多种方法)。生活中其实买车票就属于多态,成人买票全价,学生半价,婴儿免费。。。。。。
1.2c++中的多态
静态多态:静态多态就是重载,因为是在编译期决议确定,所以称为静态多态。
动态多态:动态多态就是通过继承重写基类的虚函数实现的多态,因为是在运行时决议确定,所以称为动态多态。
2.多态的实现
2.1多态的简单实现
先贴上一个多态的代码,然后依照此代码展开讲解。从图中我们可以看到,父子类的对象都调用了fun函数,通过fun函数调用不同的buy函数实现了多态行为,那么具体是怎么实现的呢,接着向下看。
2.2多态实现的条件
实现多态其实可以总结为需要俩大条件:
- 第一大条件:我们期望实现不同功能父子类中的目标函数必须定义为虚函数,且完成了函数的重写(有一些不懂的概念后边都会讲解到)
- 第二大条件:调用函数的对象必须是指针和引用(具体原因后面讲解)
3.虚函数
虚函数是实现多态的核心,我们接下来这一小节就来介绍什么是虚函数,且介绍上面说的重写是什么意思。
3.1虚函数定义
给类成员前加上virtual关键字的函数就叫做虚函数:
virtual void buy()
{}
3.2虚函数重写行为
什么叫虚函数重写?如果子类中有一个与父类完全相同的虚函数,就称子类的虚函数重写(覆盖)了父类的虚函数。完全相同指的是:函数名相同,返回值相同,参数列表也相同(这里有一个例外,斜变除外)
3.2.1普通重写
根据上面的定义重写:
class person
{
public:
virtual void buy()
{
cout << "全价" << endl;
}
};
class student:public person
{
public:
virtual void buy()
{
cout << "半价" << endl;
}
};
3.2.2协变(了解)
重写要求函数名相同,返回值相同,参数列表也相同,但这里协变算是一个特例,协变指虚函数的返回值可以是父类和子类对象的指针(引用也可以)
class A{};
class B : public A {};
class person
{
public:
virtual A* buy()
{
cout << "全价" << endl;
}
};
class student:public person
{
public:
virtual B* buy()
{
cout << "半价" << endl;
}
};
3.2.3不规范的重写行为
这里不规范的重写可以是函数名相同,返回值相同,参数列表也相同,但是子类可以不加virtual关键字,因为父类的虚函数被继承下来依然保留了虚函数属性,我们在子类中只是对他进行了重写
class person
{
public:
virtual void buy()
{
cout << "全价" << endl;
}
};
class student:public person
{
public:
void buy()
{
cout << "半价" << endl;
}
};
3.3进一步看重写
说了那么多,其实总结一句重写(覆盖)就是为了实现多态的一种语法,我们需要按照上面所说的规则定义函数,那重写的过程到底如何呢?
3.3.1虚函数表和虚函数表指针
- 虚函数表:存放虚函数地址的一个指针数组。
- 虚函数指针:是对象的一部分,它指向了虚函数表。
先来看内存中虚函数表和指针是怎么样的:用下面类定义一个b1对象
class Base
{
public:
virtual void func1()
{}
virtual void func2()
{}
private:
int a;
};
void Test1()
{
Base b1;
}
可以看出有一个a成员变量,还有一个_vfptr(虚函数表指针),这个指针指向的地址有俩个虚函数的地址,模型如下:
对象模型:我们重写过程就是基于这张虚函数表
虚表存在代码段,虚表指针存在数据段。
3.3.2单继承重写
有了虚函数表的概念,我们看继承后进行重写行为虚函数表中发生了什么变化:
此时我们Base1类中的虚函数表有已经重写了的fun1,拷贝下来的fun2,但是问题来了!!!我们本来的fun3()函数去哪里了???监视窗口的虚函数表中只有俩个虚函数的地址
我认为编译器可能在作妖,所以我们自己来验证一下fun3函数到底在不在:通过验证,fun3()果然是在的
验证代码贴一下,感兴趣的同学可以研究一下,就不多讲解了:(如果代码奔溃了重新生成解决方案)
typedef void(*FUNC) ();
void PrintVT(int* table)
{
cout << "虚函数表地址" << table << endl;
for (int i = 0; table[i] != 0; i++)
{
printf(" 第%d个虚函数地址 :0X%x,->\n", i, table[i]);
FUNC f = (FUNC)table[i];
f();
}
cout << endl;
}
int main()
{
Base1 bb;
PrintVT((int*)(*(int*)&bb));
system("pause");
return 0;
}
现在我们应该一个子类是怎么具体完成重写的了:
3.3.3多继承重写
如果根据如下方式进行继承,那么重写的虚函数表的格局又是怎么样的呢?
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;
};
void Test1()
{
Derive d1;
}
int main()
{
Test1();
system("pause");
return 0;
}
查看内存之后我们发现此时对象中分别存了Base1和Base2的两张虚表,他们中都重写了fun1(),问题又来了fun3()又不见了,此时他存在哪里呢?
再次进行自己打印:我们发现fun3()存在了Base1的虚表中,我们记住子类的虚函数会放在先继承的那个虚函数表里。
4.多态的原理
- 上面叭叭啾啾讲了那么多,其实就是在为多态的原理做铺垫,那么我们现在透过本质来看多态实现的原理:
接着用上面的代码:不过这次我们这次用两种不同的调用方式
class person
{
public:
virtual void buy()
{
cout << "全价" << endl;
}
};
class student:public person
{
public:
virtual void buy()
{
cout << "半价" << endl;
}
};
void fun(person& p)
{
p.buy();
}
int main()
{
person p;
person* pp = &p;
p.buy();//使用p对象调用
pp->buy();//使用指针模拟多态调用
system("pause");
return 0;
}
打开反汇编:
我们发现,如果使用p对象调用代码只有两行,而模拟多态的原理代码较p对象而言还是非常多的,因为使用p对象调用时,编译器在编译的阶段已经知道我们所需要调用的函数,而模拟多态的原理我们发现,系统需要去虚函数表中去查找我们想调用的函数。
静态绑定与动态绑定:
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数
重载 - 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用
具体的函数,也称为动态多态。
小结
- 所以我们现在已经知道多态的原理了,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
5.抽象类
5.1接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写(重写后函数的内部自己实现),达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数
5.2抽象类定义
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
为什么有抽象类(不实例对象的类),因为拿下面的代码来说,人本来就是抽象的东西,人本身应该有自己的特征,比如人应该是一个学生,或者是一个老师。实例出人这个类是没有意义的。
class person
{
public:
virtual void buy() = 0;
};
class student:public person
{
public:
virtual void buy()
{
cout << "半价" << endl;
}
};
5.3overrid和final
一对互斥的关键字,overrid将强制进行子类的重写,final声名基类的虚函数不可以进行重写。
6.多态常见的问题
- 什么是多态?答:参考 上文
- 什么是重载、重写(覆盖)、重定义(隐藏)?答:具体参考上文和笔者之前博客。
- 多态的实现原理?答:虚函数表
- inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式
无法访问虚函数表,所以静态成员函数无法放进虚函数表。 - 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始
化的。 - 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义
成虚函数。(因为可能delete一个父类的指针指向子类,多态原理会帮助我们释放干净) - 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是
引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 - 虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的,一般情况下存在代码段
的。 - 什么是抽象类?抽象类的作用?答:参考(抽象类)。抽象类强制重写了虚函数,另外抽象类体现出
了接口继承关系。
总结
- 多态我们需要了解构成多态的条件,构成多态的条件我们又必须知道重写原理,虚函数表/指针,系统怎么实现多态的,仔细的阅读上文你应该会有所收获,有什么写的错误或者不清楚的地方还请大家多多指出。(今天累了,不完整的地方明天补充)。