多态(Polymorphism):通常是指同一个行为面对不同对象,呈现出不同的状态和结果。
举个例子: 乘坐公交买票时,成人买票是全价,学生买票是半价,而老人买票是免费的。
而对应到计算机里面:多态则由不同继承关系的类对象,去调用同一函数,而产生不同行为,那么具体怎么触发多态机制呢?
1.构成虚函数的重写(同函数名-同参数-同返回值(协变除外) )
2.有父类的指针*或者引用& 的调用
——虚函数就是被virtual关键字修饰的函数。
——虚函数就是为重写而生 ---------> 重写就是为了多态而生
——通过重写虚函数
class Man{ //父类
public: virtual void Func() //同名
};
class Kid :public Man{ //子类继承
public: virtual void Func() //同名
};
——如上面形式称:子类虚函数重写了父类虚函数。
注意: 当父类虚函数加上了virtual,子类在重写相同虚函数的时候virtual也可以不加,同样构成多态。
在了解虚函数的重写后我们发现,无论是重写还是隐藏还是重载,都是对相同函数名函数的一种重定义,那么这三者区别又在哪里呢?
上面提到了协变:是一种特殊的重写,虽然不满足重写:返回值相同的定义,但依然是满足重写规则的,可以理解为重写的特例。
class Man{
public:
virtual Man* f() {return new A;} //返回值是Man*类型
};
class Kid: public Man{
public:
virtual Kid* f() {return new B;} //返回值不同,是Kid* 类型
};
多态通过虚函数实现,那么虚函数在哪里呢?虚函数的指针放在虚函数表里!(以下简称虚表),虚表中存放着虚函数的指针。两者在物理存储上都本质放在代码段(常量区)中!
回顾一道经典面试题:问sizeof(Base)?
class Base{
virtual void func1();
int _b=1;
char a =1;
};
最终答案为sizeof(Base) = 12; 那是因为除了a和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; } //普通函数以示区分
};
class Derive:public Base {
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
};
int main(){ //创建两个对象
Base b;
Derive d;
return 0;
}
调试结果如下:
通过调试我们可以发现:
1.派生类对象d也有一个虚表指针,一部分是继承下来的成员,一部分是自己的成员
2.对象b和对象d的虚表是不一样的,在这里我们发现func1在对象d中完成了重写所以d中虚表是derive::func1,所以虚函数重写也叫覆盖,重写是语法层面的叫法,覆盖是底层的叫法。
3.虚函数本质是一个存放着虚函数指针的指针数组,一般会在结尾放一个nullptr。
4.对象中存的都是虚表指针! 不是虚表本身,虚表和虚函数都是放在代码段(常量区)上的。
5.虚表什么时候生成? ——编译阶段
6.对象中的虚表指针在哪里初始化? ——构造函数的初始化列表
什么是抽象类? **包含纯虚函数的类就叫抽象类。**纯虚函数是将虚函数初始化为0,即加上” =0“的虚函数
virtual void Derive() = 0; //纯虚函数
抽象类不能实例化出对象,抽象类的子类也不能实例化,子类要实例化必须重写虚函数。
静态绑定(前期绑定):又称静态多态,指在程序编译期间确定程序行为的过程。
动态绑定(后期绑定):又称动态多态,指在程序运行期间,根据拿到的类型确定行为。
一般构造函数不能作虚函数,为什么?
——比如若父类A作构造函数,在通过父类指针调用子类函数A* ptr = new B()时,会调用B类构造,但是B并未实例化,所以编译器会报错。
一般析构函数常作虚函数,为什么?
——析构函数作虚函数的好处:会在调用父类A析构的同时,会顺便也析构子类B,这样一次性都能释放是我们所期望的。
1.父类指针可以指向父类或者子类的对象。
Person* ptr1 = new Person; //父类指针指向父类对象
Person* ptr2 = new Student; //父类指针指向子类对象
2.子类对象可以传给父类对象,但父类对象不能传给子类对象。因为儿子是继承父亲的,父亲有的儿子一定有,而儿子有的父亲不一定有。
3.父类指针访问子类虚函数时候是调用的子类方法!
Person* ptr2 = new Student; //先调用的是子类Student的构造函数