重庆网站托管外包公司哪家好,自己专业做网站,重庆地产网站建设,有名的公司本文将介绍C的另一个基于继承的重要且复杂的机制#xff0c;多态。
一、多态的概念
多态#xff0c;就是多种形态#xff0c;通俗来说就是不同的对象去完成某个行为#xff0c;会产生不同的状态。
多态严格意义上分为静态多态与动态多态#xff0c;我们平常说的多态一般…本文将介绍C的另一个基于继承的重要且复杂的机制多态。
一、多态的概念
多态就是多种形态通俗来说就是不同的对象去完成某个行为会产生不同的状态。
多态严格意义上分为静态多态与动态多态我们平常说的多态一般指动态多态。后文介绍的多态也是动态多态只在本部分介绍一下静态多态 1、静态多态 静态多态又称作静态绑定早绑定、前期绑定即在函数编译期间就决定了程序的行为即函数名修饰规则具体C/C二中有详细描述。 平常最经常用的静态多态就是函数重载。 2、动态多态 动态多态又称作后期绑定在程序运行期间再根据具体拿到的类型来调用具体的函数确认程序的具体行为。 我们平常说的多态一般指动态多态静态动态一般就说函数重载。 重载静态多态、虚函数重写动态多态、隐藏的区别 二、多态动态多态
从技术方面来说多态就是不同继承关系下的类对象去调用同一函数调用的函数必须是虚函数后文会介绍会产生不同行为。 1、多态的构成条件 1、调用的函数必须是虚函数且派生类必须为基类的虚函数进行重写。 2、必须用父类的指针 / 引用来调用虚函数。 为什么必须传父类的指针 / 引用这里初步解释后面会在原理部分详细解释——因为父子类的赋值兼容原则子类可以切片赋值给父类父类却不能赋值给子类因为可能会缺成员 那又为什么必须传指针 / 引用因为传对象的话子类只会把父类的那一部分成员拷贝过去但是不会拷贝虚函数表指针就不能成功调用对应的虚函数了 2、虚函数 被 virtual 修饰的类成员函数称为虚函数。 class Person
{
public:// 虚函数virtual void BuyTicket() { cout 买票-全价 endl;}
}; 2.1 虚函数的重写多态的条件之一 如果派生类中存在与父类完全相同函数名、函数返回值、函数参数都完全相同的虚函数就称作派生类的虚函数重写了父类的虚函数。 #include iostream
using namespace std;class Person
{
public:virtual void BuyTicket(){cout 全价购票 endl;}
};class Student :public Person
{
public:/*子类重写父类虚函数时如果不加 virtual 关键字虽然也可以构成重写子类继承下来父类的虚函数仍旧保持虚函数属性但是这种写法不规范可读性较差建议不要这么做*/virtual void BuyTicket(){cout 半价购票 endl;}
};void Test(Person p)
{p.BuyTicket();
}int main()
{Person p;Student s;Test(p);Test(s);return 0;
} 运行结果可以发现传父子类分别调用父子类的虚函数 2.2 多态的两个特殊情况 2.1.1 协变基类与派生类的虚函数返回值类型不同的时候 当派生类重写父类虚函数的时候基类与派生类的虚函数的返回值类型可以不同但是必须是父类 / 子类的指针或引用。 当派生类虚函数返回值是父类 / 子类的指针或引用时称作协变。 2.2.2 析构函数的重写 如果基类的析构函数也是虚函数这个时候只要派生类定义了析构函数不论是否加了 virtual 关键字都视作对基类的析构函数构成重写。 虽然基类和派生类的析构函数名字不同看似违背了虚函数的重写原则实际上编译器会对析构函数的名称做特殊处理在编译后所有析构函数的名称都会统一处理成 destructor #include iostream
using namespace std;class Person
{
public:virtual ~Person() { cout ~Person() endl; }
};class Student : public Person
{
public:virtual~Student() { cout ~Student() endl; }
};// 只有派生类Student的析构函数也定义了析构函数下面的delete对象调用析构函数才能构成多态才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 new Person;Person* p2 new Student;delete p1;delete p2;return 0;
} 父类调用析构函数子类调用析构函数先调用里面的子类析构再调用父类析构 3、C11检测虚函数是否重写的两个关键字 从上文的介绍可以看出C对虚函数的重写要求比较严格。在有些情况下比如函数名、返回值字母写反写错可能会无法构成重写导致无法构成多态。 但是这种错误在编译期间是不会报出的只有在程序运行时才会发现与预期结果不符这个时候才来debug得不偿失。 因此C11标准提供了两个帮助用户检测是否完成重写的关键字final 和 override 3.1 final final 修饰某个虚函数则这个虚函数不能再被重写 3.2 override override 修饰派生类虚函数检查派生类的虚函数是否基类的某个虚函数的重写如果不是比如拼写错了编译报错。 4、纯虚函数与抽象类 在虚函数的后面加上 0 这样的虚函数称作纯虚函数。 包含纯虚函数的类叫做抽象类又叫接口类在某类不代表具体实体的时候可以使用另一个意义是说明多态想在其多个子类中实现抽象类不能实例化出对象。 继承抽象类的派生类也不能实例化出对象只有当这个派生类对纯虚函数进行重写这个派生类才能实例化出对象。 因此纯虚函数在某种程度上间接强制了派生类的重写更体现了接口继承思想。 接口继承与实现继承 普通函数的继承是一种实现继承继承的是函数的实现目的是使用这个函数 虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态。 三、多态的实现原理重点
1、代码引入
#include iostream
using namespace std;class Test
{
public:virtual void test(){cout _num endl;}
private:int _num 1;
};int main()
{Test t;printf(%d, sizeof(Test));
} 让我们猜猜sizeof(Test) 应该是多少 很多人可能会说函数储存在代码段里不算在类大小里面那就应该是4字节32位系统 / 8字节64位系统 但实际上 x86环境下 x64环境下 这是为什么 通过内存窗口的观察我们可以看见Test对象里面除了储存了_num 成员变量还储存了一个叫做_vfptr的指针变量而一切指针变量大小在32位系统下都是4字节在64位系统下都是8字节。 这个_vfptr是什么这个指针我们叫做虚函数表指针指向虚函数表。v代表virtualf 代表function 2、虚函数表 虚函数表的本质是储存着一个类里面的所有虚函数地址的一个指针数组。一般情况下这个数组最后会放一个nullptr作为虚函数表的终止标记。注意不是储存着虚函数是储存着虚函数的地址虚函数还是储存在代码段里的 我们给出一个多态的代码 #include iostream
using namespace std;class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}virtual void Func2(){cout Base::Func2() endl;}void Func3(){cout Base::Func3() endl;}
private:int _b 1;
};// 派生类Derive继承Base并重写Func1
class Derive : public Base
{
public:virtual void Func1(){cout Derive::Func1() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;return 0;
} 调用一下监视窗口 我们可以发现 1、派生类对象 d 由两部分构成继承自父类的成员和自己的成员 2、派生类和父类都有一个虚函数表指针指向各自的虚函数表虚函数表里面储存着虚函数的地址。 3、派生类的虚函数表和父类的虚函数表不一样由于Func1完成了重写所以d的虚表 中存的是重写的Derive::Func1派生类完成重写了的虚函数覆盖了原有的父类虚函数。 所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。 4、派生类其实把父类的三个函数都继承了下来但是由于Func3不是虚函数所以并未放到虚函数表中。 派生类虚函数表的生成流程 1、先把基类的虚函数表拷贝到自己的虚函数表中 2、如果派生类重写了某个虚函数在虚函数表中用这个虚函数地址覆盖原父类的虚函数地址3、派生类如果自己增加了虚函数按照在派生类中的声明次序依次放到派生类虚函数表的后3、多态的原理 3、多态的实现原理 还是直接上代码 #include iostream
using namespace std;class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person
{
public:virtual void BuyTicket() {cout 买票-半价 endl; }
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
} 多态实现概念图 多态实现代码图 观察多态实现代码图 观察红色箭头可以看到p在指向mike对象时p-BuyTicket从mike的虚表中找到的虚 函数是Person::BuyTicket。 观察蓝色箭头可以看到p在指向johnson对象时p-BuyTicket在johson的虚表中 找到的虚函数是Student::BuyTicket。 这样就实现出了不同类的对象去调用同一函数时展现出不同的形态。 再看一下汇编代码 // 与多态无关的汇编代码都已去除
void Func(Person* p)
{
...p-BuyTicket();
// p中存的是mike对象的指针将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用不是在编译时确定的是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数但是mike是对象不满足多态的条件所以这里是普通函数的调
用转换成地址时是在编译时已经从符号表确认了函数的地址直接call 地址mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
} 就可以明白满足多态以后的函数调用不是在编译时确定的是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的因此叫做动态多态。 4、多态是如何实现的一句话总结 首先多态是一种基于继承和虚函数实现的机制派生类必须实现对虚函数的重写用来调用虚函数的函数必须传父类的指针或引用然后基类和派生类各有一张虚函数表通过传参的不同父类直接传子类切片对象内部的虚函数表指针会去各自的虚函数表里面寻找对应的虚函数地址从而实现调用同名函数时产生不同的行为达到多态的效果。 5、有关多态的一些小问题 如果子类不重写虚函数父子类的虚函数表一样吗 储存的虚函数的地址是一样的但是虚函数表毕竟是两张表储存虚函数表的地方不一样是分开存储的 如果有许多同类对象它们的虚函数表一样吗 一样同类对象共用一张虚函数表 也就是说虚函数表本质其实是个静态常量被所有同类对象共享 四、多继承关系下的虚函数表
之前所说的是单继承关系下的虚函数表那么多继承关系下的虚函数表是什么样的
PS菱形继承和菱形虚拟继承太过复杂这里只介绍普通多继承
继续上代码
#include iostream
using namespace std;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;
};// 先给虚函数函数指针取个别名VFPTR
typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[])
{cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}int main()
{Derive d;/*思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr1、先取b的地址强转成一个int*的指针2、再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针3、再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。4、虚表指针传递给PrintVTable进行打印虚表5、需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案再编译就好了。*/VFPTR* vTableb1 (VFPTR*)(*(int*)d);PrintVTable(vTableb1);VFPTR* vTableb2 (VFPTR*)(*(int*)((char*)d sizeof(Base1)));PrintVTable(vTableb2);return 0;
} 可以发现派生类继承了几个包含虚函数的父类就有几个虚函数表。派生类自己独有的虚函数会存放在第一个继承的基类的虚表里但是由于编译器的BUG并没有展示在内存窗口里面可以通过下图观察到 派生类自己独有的虚函数会存放在第一个继承的基类的虚表里