一.多态的概念
简单的讲就是同一事物在不同条件下所呈现出来的不同形态
举例:火车站的同一窗口成人售票就是全价票,学生就是半价票。这就是同一事物,但是在不同的条件下可以呈现处不同的形态。有点见人说人话,见鬼说鬼话的意思。
二.多态的实现
#include<iostream>
#include<Windows.h>
using namespace std;
class Person
{
public:
virtual void Buy()
{
cout << "全价票" << endl;
}
private:
};
class Student:public Person
{
public:
virtual void Buy()
{
cout << "半价票" << endl;
}
private:
};
void Test(Person& a)
{
a.Buy();
}
int main()
{
Person p;
Test(p);
Student s;
Test(s);
system("pause");
return 0;
}
从上边的代码中我们很容易就可以看出,实现多态的必要的三个条件:
- 首先必须有继承
- 在父类和子类中都要有虚函数(下边会讲解什么是虚函数),并且子类和父类中的虚函数要构成重写(后边会解释重写的概念)
- 通过父类的指针或者引用来调用虚函数
注意:我们可以看出,普通调用和对象类型有关,但是多态的调用和具体对象有关
1.虚函数
虚函数的概念很简单就是在函数前加virtual关键字
注意:只有非成员函数才可以声明为虚函数
virtual void show()
{
cout << "ambition" << endl;
}
2.虚函数和纯虚函数的对比
请看博客:https://blog.csdn.net/alidada_blog/article/details/82894821
3.什么函数不能声明为虚函数??
- 构造函数
因为虚函数需要存放在虚表中,这时候虚表就需要有虚表指针。但是虚表指针是在调用构造函数的时候完成初始化的。但是又由于虚表是在程序编译的时候生成的,所以如果将构造函数声明为虚函数,即使生成了虚表,但是不能有虚表指针。虚表中的虚函数需要虚表指针来访问。
- 静态函数
因为没有this指针,那么将无法存入虚标中
- operator=
没有意义,自身并不会构成重写
- 内联函数
因为内联函数没有地址
- 友元函数
因为友元函数不是成员函数
注意:只有非静态的成员函数才可以声明为虚函数
- 析构函数
注意:析构函数最好声明为虚函数
因为在有些情况下可以保证析构的顺序,防止内存泄漏。
例如:A* a = new B; delete a;
这种 情况下只是析构了a对象,但是并没有析构B,只有声明为虚函数,才可以保证两者都被析构掉,并且保证了的调用析构函数的顺序(先构造的后析构,后构造的先析构)(在继承中子类会先调用父类的构造函数再去调用子类的构造函数)
4.重写(覆盖)
- 重写是针对基类中虚函数的,在派生类中实现一个与基类虚函数原型(返回值类型、函数名字、参数列表)相同的虚 函数,即派生类与基类中虚函数的原型完全相同,才称之为对基类虚函数的重写。
- 构成重写的条件
1.)首先必须得有继承
2.)父类和子类中都必须有虚函数
3.)父类和子类中的虚函数必须是函数名相同,参数列表相同,返回值相同
- 构成重写的两个例外:
1.)协变
父类中虚函数返回父类对象的指针或引用,子类与父类同名虚函数返回子类对象的指针或引用,此种情 况也构成重写,但是此时派生类与基类虚函数返回值类型不同。
2.)析构函数
父类中的析构函数如果是虚函数,只要子类的析构函数显式提供,就构成重写,此种情况派生类与基类虚 函数函数名字不同。
- 注意:在够成重写时父类的虚函数必须有virtual关键字,子类中的可以构成重写的函数可加可不加,建议最好加上
5.针对重写常见的面试问题
重载,重写(覆盖),重定义(隐藏)三者的区别:
- 重载
1.)必须在同一作用域(同一个类中)
2.)函数名相同,参数列表不同(参数个数,顺序,类型)
3.) 返回值可同,可不同
- 重写(覆盖)(重写大多出现在实现在多套中)
1.)必须有继承(最好是公有继承)
2.)父类和子类中必须有虚函数
3.)父,子两类中的虚函数必须是函数名相同,参数列表相同,返回值相同
注意:我们可以理解重写就是特殊的重定义
- 重定义(隐藏)(出现在继承中)
1.)必须有继承(最好是公有继承)
2.)父,子两类中有函数名相同的函数
5.动态绑定(动态联编)和静态绑定(静态联编)
- 静态绑定
程序在编译的时候进行确认地址
- 动态绑定
程序在运行的时候进行确认地址
- 注意:通常虚函数是动态绑定 ,普通函数是静态绑定,缺省参数值也是静态绑定
为什么有两种类型联编???既然动态联编如此之好,为什么不默认就是动态联编???
有两大原因:
- 效率
为了使程序在运行阶段进行决策,必须采用一些方法来跟踪基类指针或引用只想的对象类型,这增加了额外的开销处理。例如,如果不用作基类,则就不需要动态联编,同样如果子类中没有重定义基类的虚函数则也不需要动态联编。这种情况下使用静态联编可以提高效率。因为静态联编的效率高,所以将静态联编设置为默认的
- 概念模型
在设计类时,可能包含一些派生类没有重新定义的成员函数。不将这些函数作为虚函数有两方面的好处:首先效率高。其次,指出不要重新定义该函数。
三.带有虚函数的类的剖析
1.之前的博客中讲解过了空类的大小为1个字节,那么为什么是1字节勒??(这里就不做代码演示了,有兴趣的同学可以看我以前的博客https://blog.csdn.net/alidada_blog/article/details/81262152)
所谓类的实例化就是在内存中分配一块地址.(空类同样可以被实例化),每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.因为如果空类不隐含加一个字节的话,则空类无所谓实例化了(因为类的实例化就是在内存中分配一块地址。 继承这个类后这个类大小就优化为0了。这就是所谓的空白基类最优化。
2.拥有虚函数的类的大小是多少???为什么是四个字节??
拥有虚函数的类的大小是四个字节,这四个字节用来存放虚函数指针。一般将存放 虚函数的位置称作为虚函数表,简称虚表,将指对象前4个字节指向虚表的指针称为虚表指针。
class A
{
public:
virtual void show()
{}
};
class B
{
public:
};
int main()
{
A a;
B b;
cout << "A->" << sizeof(a) << endl;
cout << "B->" << sizeof(b) << endl;
return 0;
}
3.虚表存在于每个类中,子类和父类中都有一个虚表。子类中的虚表存放子类的虚函数,父类中的虚表存放父类的虚函数。他们互不干扰。接下来我们打印一次虚表:
class Base
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
private:
int a;
};
class Driver:public Base
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
virtual void func3()
{
}
virtual void func4()
{
}
private:
int b;
};
typedef void(*VFUNC)();
void PrintVtable(int* vtable)
{
cout << "虚表" << vtable << endl;
for (int i = 0; vtable[i] != 0; ++i)
{
printf("vtable[%d]->%p \n", i, vtable[i]);
VFUNC f = (VFUNC)vtable[i];
f();
}
cout << endl;
}
int main()
{
Base p;
Driver s;
PrintVtable((int*)(*((int*)&p)));
PrintVtable((int*)(*((int*)&s)));
system("pause");
return 0;
}
5.从上边的图我们可以看出,虚函数在虚表中的存放顺序是按照虚函数在类中声明的顺序来存放的,这也就解释了编译器是如何调用在虚表中存放的虚函数的。
本文是根据《C++primer plus》和自己的理解整理所得!!!!具体可看《C++primer plus》的第13章
如果有什么错误希望大家指出!!1