网站的关键词在哪里设置,Wordpress页面方块,做网站公司价格多少,移动互联网开发多态的核心是虚函数#xff0c;本文从虚函数出发#xff0c;根据原理慢慢推进得到结论#xff0c;进而理解多态
1.虚函数
先看一下下面的代码#xff0c;想想什么导致了这个结果 #include iostream
using namespace std;class A
{
public:virtual void test(){co…多态的核心是虚函数本文从虚函数出发根据原理慢慢推进得到结论进而理解多态
1.虚函数
先看一下下面的代码想想什么导致了这个结果 #include iostream
using namespace std;class A
{
public:virtual void test(){cout A endl;;}
};class B : public A
{
public:void test(){cout B endl;;}
};class C : public B
{
public:void test(){cout C endl;;}
};void Test(A r)
{r.test();
}int main()
{A a;B b;C c;Test(a);Test(b);Test(c);return 0;
}
结果是 如果我们去掉A里面的virtual呢 我们可以看到前后两次结果不同为什么呢函数形参为什么是以A来接收的调用时为什么还有区别呢这就需要接触虚函数了。
1虚函数和虚函数表
当我们在父类声明了一个虚函数后这个函数就被存在常量区了同时在这个类里又多了一个新的隐藏成员叫虚函数表这个成员要算在整个类的大小里面。这个虚函数表就是专门存虚函数的地址的本质是函数指针数组根据不同机器指针大小也不同。对于父类而言无论创建多少对象它们都共用一个虚函数表即对于同一种类函数都是一样的。这里要分清虚函数是存在常量区的而不存在类里类中存的是虚函数表。
2重写
虚函数有什么用呢当子类实现一个和父类虚函数函数名、参数、返回值完全一样的函数时就叫做重写。重写是一种特殊的隐藏是在多态中的一种语法而隐藏只要求函数名相同是继承中的语法。重写的意义在于子类也有一个新的虚函数表虽然函数前没有加声明virtual父类前必须加当子类显式写了这个函数就会存到常量区虚函数表存函数的地址第一句指令的地址。对于这个子类无论创建多少个对象它们都使用同一个针对子类的虚函数表。如果说有多个虚函数而子类没有重写那个没有重写的函数就使用父类的对应的函数反正没区别。
3对多态的理解
到这里我们对虚函数表、虚函数和重写有了一定了解实际就是在最初的父类的函数前加上virtual让该函数进入虚函数表子类重写会让虚函数表存的函数不同在调用的时候明明是调用的同一个函数但得到的结果是针对每一种类不同的。这就叫多态即多种形态针对不同的类有不同的表现形态。
4对多态调用方式的理解
函数形参为什么是以A来接收的
我们进一步关注Test(A r)这个函数前面我们讲了赋值兼容转换因此当B和C传进去的时候r都会指向子类中的父类部分这里相当于给它们的父类部分取别名。也就是说r无论接收的是A还是B还是C最终都会被切割成A的模样A中也有虚函数表但是内容是不是都一样呢
很明显虚函数表的作用就凸显出来了A、B、C都有一个虚函数表在B、C切割成A后虚函数表被保留了下来当我们用r去调用虚函数时编译器会默认去虚函数表找到对应的函数三种虚函数表的函数在函数名、参数、返回值上都相同但存的函数地址不同根据不同的函数地址就能找到不同的函数实现这也是重写的意义所在。
至此我们应该能够理解前面所说虚函数、虚函数表、重写存在的意义了它们的出现都最终服务于实现一件事——多态即根据不同类在调用同一函数时体现出不同状态。
5是否有其它调用方式
事实上使用A调用本质就是利用了赋值兼容转换将多个子类都切割成父类的形式再根据它们虚函数表的值的差异调用不同的同名函数体现出类与类之间的区别。很明显除了引用指针也适合但赋值呢赋值不是也遵循赋值兼容转换吗 从实验上看是不行的但也好理解。r都已经完全变成A类型了再去调用B或C的成员就不太说得过去了。你可以将这里理解成一种特殊处理支不支持都说得过去但从形式上来说不支持更合理。
6多态的条件
很多课程都喜欢先说条件再将原因而如果我们慢慢推进到这里自然就理解了。
多态需满足条件父类函数想和子类形成差异的第一个函数就叫父类函数写virtual父类如果不写virtual而子类写virtual那第一个写virtual的才叫父类你可以将virtual当作一个多态开始的标志后续的所有子类写不写virtual无所谓子类覆盖/重写父类的虚函数调用时使用父类的指针或引用特别注意不能用赋值。
2.协变
上面说过要重写函数必须保证函数的函数名、参数、返回值相同。但有唯一一个例外可以在返回值不同时能构成重写就是协变基本不用即返回值可以是父子类的引用或指针。
下面这段代码是能跑过的 #include iostream
using namespace std;class A
{
public:virtual A test(){cout A endl;;return *this;}
};class B : public A
{
public:B test(){cout B endl;;return *this;}
};void Test(A r)
{r.test();
}int main()
{A a;B b;Test(a);Test(b);return 0;
}注意返回值可以加const返回值也可以是其它类但必须是父子关系 #include iostream
using namespace std;class C
{};class D : public C
{};class A
{
public:virtual const C* test(){cout A endl;C* c new C;return c;}
};class B : public A
{
public:const D* test(){cout B endl;D* d new D;return d;}
};void Test(A r)
{r.test();
}int main()
{A a;B b;Test(a);Test(b);return 0;
}注意父子关系顺序不能反父类返回值对应父类的虚函数 协变几乎不用了解即可。我们大部分情况还是要保证函数名、参数、返回值相同讨论的时候也是跳过这个特殊情况的。
3.析构函数的重写
理解析构函数的重写可以加深我们对析构函数的理解顺便能够解释为什么所有的析构函数都会被处理成destructor() #include iostream
using namespace std;class A
{
public:~A(){cout A endl;}
};class B : public A
{
public:~B(){cout B endl;delete p;}int* p;
};int main()
{A* a new B;delete a;return 0;
}这段代码会导致内存泄漏因为当delete a的时候会根据a的类型去调用析构函数这里就只会去调用A的析构函数 联系到上面的重写很快我们就会想到使用virtual修饰父类的析构函数让析构函数进入虚函数表。但是很明显父类和子类的类名是不可能相同的所以类的析构函数做了特殊处理即都重命名为~destructor()这样就符合了虚函数的要求 我们可以看到这里根据虚函数表就能成功调到子的析构函数了同时对于所有继承而言子的析构调用完成之后都会逐级向上调用父的析构函数