多态的体现
(什么是多态)
多态,字面上说就是多种形态;通过一种方式与其互动,它可以展现出不同的形式;举个例子:我们打印机打印文件,我们向其输入的指令都是打印,给打印机的全都是纯白的A4纸,打印机可以打印出黑白的内容,也可以打印出彩色的内容;因为我们的需求不同所以打印机的内部操作也不相同;
这就是多态展现出的效果,接下来我们看看代码实现的多态;
class A
{
public:
virtual void fun()
{
cout << "A->fun():黑白打印" << endl;
}
int _a = 1;
};
class B :public A
{
public:
virtual void fun()
{
cout << "B->fun():彩印" << endl;
}
int _b = 2;
};
void print(A* ptr)这就像一个打印机作为一个接口
{
ptr->fun();
}
int main()
{
A a;
B b;
print(&a);
print(&b);
return 0;
}
看我们通过一个接口print,这个接口就像打印机一样接受了我们的A和B类的白纸,由于白纸的要求不同,所以输出了不同的现象;接下来我们说说实现多态的细节;
实现多态
要向实现多态,我们需要先学习多态的语法;多态要求满足两个条件:
1.需要通过父类的指针或引用来调用虚函数
2.子类的虚函数需要满足重写的条件(父类函数一定是虚函数)
我们先说第二点,首先虚函数是什么呢?虚函数在语法上就是在函数前面加上virtual关键字,(注:这个virtual关键字和之前继承的virtual没有任何关系);加上这个关键字之后我们的类中的函数就变成了虚函数了;之后就是满足重写;
重写
重写需要满足的要求是父类和子类的返回值,参数,函数名一致;这样就构成了重写,但重写有两个例外:
1.协变(可以使得父子函数的返回值不相同)
当子类和父类函数的返回值是父子关系的指针或引用的时候也可构成重写;
当返回值构成这种关系,返回值不相同也能构成重写
class parent
{};
class child :public parent
{};
2.析构函数名字不相同
父类和子类的析构函数的名字不相同时,用父类指针或引用接收,再调用delete关键字析构时,也可以满足重写从而实现多态;
// 多态
class parent
{};
class child :public parent
{};
class A
{
public:
virtual parent* fun()
{
cout << "A->fun():黑白打印" << endl;
return nullptr;
}
int _a = 1;
virtual ~A()
{
cout << "这是A的析构函数" << endl;
}
};
class B :public A
{
public:
virtual child* fun()
{
cout << "B->fun():彩印" << endl;
return nullptr;
}
int _b = 2;
virtual ~B()
{
cout << "这是B的析构函数" << endl;
}
};
void print(A* ptr)//这就像一个打印机作为一个接口
{
ptr->fun();
}
int main()
{
//A a;
//B b;
//print(&a);
//print(&b);
A* pa = new A;
A* pb = new B;
delete(pa);
cout << "-------------------" << endl;
delete(pb);
return 0;
}
看我们传递给delete的都是父类的指针按道理来说,delete是通过指针类型来释放内存的;而这里指针的类型是父类按道理说,只会释放父类部分的内容,子类独有部分不会释放,使得释放不完全;但是结果很意外,我们的析构函数成功的将子类的全部内容都释放了;这就是虚函数重写的意义;你看我们的析构函数也变成虚函数的时候,这个时候就会形成重写构成多态,从而使得,delete这个接口也满足多态;但这样似乎违背了重写的三同(参数,返回值,函数名)中的函数名相同;其实这是编译器在编译时做了处理,析构函数的名字统一处理成了destructor;
如果我们不使用virtual关键字也就不构成多态了你看下面的结果:
由于析构函数没有加virtual修饰所以delete时直接通过指针类型来析构内容,以至于只通过析构函数析构了父类的内容;
第二点的条件和例外讲完了;
需要使用父类的指针或者引用来调用
第一点,要使用父类的指针或者引用来调用,不能直接使用类对象或者其他方式调用;如
了解完了实现多态的条件,我们接下来深入底层来研究一下,多态究竟是如何形成的,多态究竟做了什么;
多态的原理
多态究竟做了什么呢,我们通过调试来一探究竟;
还是使用这份代码
// 多态
class A
{
public:
virtual void fun()
{
cout << "A->fun():黑白打印" << endl;
}
int _a = 1;
};
class B :public A
{
public:
virtual void fun()
{
cout << "B->fun():彩印" << endl;
}
int _b = 2;
};
void print(A* ptr)//这就像一个打印机作为一个接口
{
ptr->fun();
}
int main()
{
A a;
B b;
print(&a);
print(&b);
return 0;
}
那什么是虚函数表指针呢?它是用来干嘛的呢?
虚函数指针虚函数表
当我们的类中有虚函数的时候就会在类中产生一个虚函数表指针,这个指针指向的是虚函数表,虚函数表就是一个函数指针数组,它里面装的是我们的虚函数的地址;
通过强制类型转换来获得指针大小的虚函数指针
为了证明虚函数表在内存中真正的存在我们可以使用这样一段代码来打印虚函数表;
class A
{
public:
virtual void fun1()
{
cout << "C->fun1():";
}
virtual void fun2()
{
cout << "C->fun2():";
}
virtual void fun3()
{
cout << "C->fun3():";
}
};
class B:public A
{
public:
virtual void fun4()
{
cout << "C->fun4():";
}
virtual void fun5()
{
cout << "C->fun5():";
}
virtual void fun6()
{
cout << "C->fun6():";
}
};
typedef void(*vfptr)();
void printvtable(vfptr* vftable)//打印虚函数表
{
for (int i = 0; vftable[i] != NULL; i++)
{
vftable[i]();
printf(" vftable[%d]->%p\n", i, vftable[i]);
}
}
int main()
{
A a;
B b;
printvtable(*(vfptr**)&a);
cout << "-------------------------" << endl;
printvtable(*(vfptr**)&b);
return 0;
}
由此我们可以看到虚函数表的存在;
注意:这是vs编译器在虚函数表的末尾做了处理将末尾位置置为了0;其他编译器下可能无法使用这个代码打印出虚表;
此外如果代码在vs上面也跑不过可能是编译时没有在表末尾置为0只需要清理解决方案再重新生成即可
子类对象会继承父类的虚表
我们还能看见我们的子类对象是继承了父类对象的虚表的,上面我们可以看到我们,子类对象的虚表变长了很多;
重写的底层——覆盖
由上面这点,我们知道了子类会继承父类的虚表,那么我们回顾一下重写的要求它要求父子函数的返回值参数函数名相同;当这些条件都满足的时候,就会发生覆盖现象子类会将继承过来的虚表中的父类的虚函数地址覆盖写上自己的虚函数的地址,这就是重写的真正意义!
这就是重写的真正的操作;
拓展:
我们知道了子类会继承父类所以有时候子类函数可以省略virtual关键自也可以构成隐藏
多态实现过程
当我们有了这个表之后,我们再看我们多态需要满足的两个条件;首先我们规定了只能用父类执政或引用来调用虚函数;所以在我们传递子类对象给父类的指针的时候,指针会切割的指向子类父类部分;
而这个时候由于是父类指针在调用,首先会通过this指针指向虚表,然后由虚表指向我们相应的内容的虚函数所在位置;而又因为重写的原因虚函数表中此时都已经是子类的虚函数了,由此只会提供子类的虚函数让父类指针调用!
所以这个时候我们就成功的找到了我们子类中的虚函数,而不是因为指针类型而去找指针类型的函数;
所以多态的实现其实非常简单,无非就是多了一层指针指向而已,不是直接使用this指针指向我们的函数,而是通过this指针指向虚表虚表再指向我们的虚函数,这样就使得接口可以通过指针内部的内容来判断使用哪个虚函数,从而实现多态;
重载重写重定义
了解了这些之后,我们来对比一下我们学的一些概念:
重载:在同一区间内,通过符号表中的命名规则(参数不同)可以使得同名函数存在;
重写(覆盖):在子类虚函数和父类虚函数返回值,名字,参数都相同(除开两个例外);
重定义(隐藏):在子类继承了父类的成员函数时,如果子类写了同名函数(重写除外)就会形成隐藏,从而显示调用只会调用子类的成员函数;
抽象类,override,final关键字
override关键字是用来检测子类虚函数是否重写了,如果没有重写的话就会报错:
使用方式:在子类函数参数后加上override即可,可以检查子类函数是否重写
final关键字用来检查函数是不是被重写,有了final就代表它是最后被重写的函数,可以防止下面的类继承它时重写它;
使用方法:在参数()后加上final
抽象类:
是指一种不能被继承的类,这种类中有一种叫做纯虚函数的函数;纯虚函数的形式是这样的:
virtual void fun()=0;
只要在虚函数的后方加上=0就是纯虚函数;
class A {
public:
virtual void fun() = 0;
};
class B :public A {
public:
virtual void fun()
{
cout << "B->fun()" << endl;
}
};
class C :public A {
public:
virtual void fun()
{
cout << "C->fun()" << endl;
}
};
void print(A* ptr)
{
ptr->fun();
}
int main()
{
B b;
C c;
print(&b);
print(&c);
return 0;
}
上面的A类就是抽象类;
抽象类无法实例化出对象
所以如果有类继承了抽象类的话,那么一定要重写纯虚函数,否则我们的子类也无法实例化,这样一来我们的类就成功的有了强制性的操作,必须重写;
多继承
当继承了多个含有虚函数类时,子类会产生多个虚表;
class A
{
public:
virtual void fun()
{
cout << "A->fun()" << endl;
}
int _a = 1;
};
class B
{
public:
virtual void fun()
{
cout << "B->fun()" << endl;
}
int _b = 2;
};
class C:public A ,public B
{
public:
virtual void fun()
{
cout << "C->fun():";
}
virtual void fun1()
{
cout << "C->fun1():";
}
int _c = 3;
};
typedef void(*vfptr)();
void printvtable(vfptr* vftable)
{
for (int i = 0; vftable[i] != NULL; i++)
{
vftable[i]();
printf(" vftable[%d]->%p\n", i, vftable[i]);
}
}
int main()
{
C c;
printvtable(*((vfptr**)&c));
B* ptr = &c;
printf("\n");
printvtable(*((vfptr**)ptr));
A* pa = &c;
B* pb = &c;
pa->fun();
pb->fun();
return 0;
}
通过偏移值来查看我们第二个表位置从而实现传递位置;
调试发现c有两个虚表
再打印两个虚表发现,c自己的虚函数储存在了第一个虚表中
在vs中多继承的时候两个表中的相同(同名同参同返回值)函数被c的函数覆盖时虚表中的指针是不一样的;其实这是vs对指针进行了处理,按道理应该是一样的,下面就是转到反汇编后两个表中对于指针进行的操作;
静态绑定和动态绑定
静态绑定就是在编译时就确定的函数行为,就比如我们的cin>>和cout<<函数重载(通过参数类型来匹配行为);通过给出的类型就可以确认我们函数的行为;
动态绑定则是在运行时展示确定的行为就比如我们的多态,我们拿到的是父类的指针这是表面的现象,但是我们并不清楚,指针内部的数据是谁的,只有在运行时我们获得内部数据才能确定应该产生的行为