泰安网站制作工作室,青岛软件开发公司排名,天津天狮网络营销课程,宣传册设计及网站建设个人主页 文章目录 ⭐一、多态的概念#x1f384;二、多态的定义及实现1.多态的构成2.实现多态的条件3.虚函数的概念4.虚函数的重写和覆盖5.析构函数的重写6.协变7.override和 final关键字8.重载、重写/覆盖、隐藏这三者的区别 #x1f3e0;三、纯虚函数和抽象类的关系#… 个人主页 文章目录 ⭐一、多态的概念二、多态的定义及实现1.多态的构成2.实现多态的条件3.虚函数的概念4.虚函数的重写和覆盖5.析构函数的重写6.协变7.override和 final关键字8.重载、重写/覆盖、隐藏这三者的区别 三、纯虚函数和抽象类的关系️四、多态的原理1.虚函数表指针2.多态是如何实现的3.动态绑定和静态绑定 五、虚函数表1.概念2.虚函数和虚函数表两者的存储位置 ⭐一、多态的概念
多态(polymorphism)简单的来说就是多种形态。但多态又可以分为两种一种是编译是的多态称为静态多态还有一种是运行时的多态称为动态多态。下面我们就更进一步去了解一下什么是静态多态和什么是动态多态。
1.静态多态 静态多态主要就是函数重载以及函数模板它们传不同的类型参数就可以调用不同的函数通过参数不同就能够达到多种形态。而至于为什么会被称为是编译时的多态是因为它们的实参传递给形参的参数匹配是在编译时完成的。
2.动态多态 具体的来说就是去完成某个行为函数可以通过传不同的对象去完成不同的行为因此达到多种形态。 例如当我们要去买票时我们会发现普通人去买票时是全价票学生去买票时是优惠票而军人去买票时是优先买票。又或者同样是动物叫这一行为传猫对象过去就是”(ω)喵“传狗对象过去就是汪汪。
二、多态的定义及实现
1.多态的构成
多态是⼀种继承关系的下的类对象去调用同一函数从而产生了不同的行为。 例如Student继承了PersonPerson对象买票为全价票而Student对象买票则为优惠票。
2.实现多态的条件
实现多态有两个重要的条件 • 必须是指针或者引用调用的函数。 • 被调用的函数必须是虚函数。
如果想要实现多态的效果首先必须是基类的指针或引用因为只有基类的指针或引用才能指向派生类对象。其次派生类必须对基类的虚函数进行重新或者覆盖派生类才能有不同的函数从而达到多态的效果。
3.虚函数的概念
虚函数就是在类成员函数前面加上virtual修饰那么这个成员函数就被称为虚函数。注意非成员函数不能加上virtual进行修饰。
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
};4.虚函数的重写和覆盖
概念在派生类中有一个跟基类完全相同的虚函数既派生类的虚函数与基类的虚函数的返回值类型、函数名以及参数列表完全相同称派生类的虚函数重写了基类的虚函数。 注意在重写基类的虚函数时派生类的虚函数在不加上virtual的情况下也能构成重写因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性但这种写法不是很规范不建议这样使用。
class Person
{
public:virtual void BuyTicket(){cout 买票-全价 endl;}
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-打折 endl; }
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系⽽是由ptr指向的对象决定的。ptr-BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}通过上述代码以及运行结果我们可以发现子类Student中的BuyTicket重写了基类Person的BuyTicket。
5.析构函数的重写
我们首先要了解虽然析构函数的名字看起来不一样但实际上编译器对析构函数的名称做了特殊的处理编译后的析构函数名称统一处理为destructor因此析构函数的名称实际上都为destructor。
class A
{
public:virtual ~A(){cout ~A() endl;}
};class B : public A
{
public:~B(){cout ~B()-delete: _p endl;delete _p;}
protected:int* _p new int[10];
};int main()
{A* p1 new A;A* p2 new B;delete p1;delete p2;return 0;
}通过阅读上述代码如果我们在~A之前不加virtual是否会发生报错呢结果是肯定的因为如果 ~A()前面不加上virtual那么deletep2时只调用了A的析构函数而没用调用B的析构函数从而导致内存泄漏的问题这就是为什么在基类中的析构函数设计为虚函数。
6.协变
协变就是派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。
class A {};
class B : public A {};class Person {
public:virtual A* BuyTicket(){cout 买票-全价 endl;return nullptr;}
};class Student : public Person {
public:virtual B* BuyTicket(){cout 买票-打折 endl;return nullptr;}
};void Func(Person* ptr)
{ptr-BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}7.override和 final关键字
C对函数的重写要求比较严格如函数名字写错了或参数写错等都无法构成重载而这种错误在编译期间是不会报出的只有在运行时才会出现错误。因此在C11中提供了override帮助用户检测是否完成重写。如果我们不想让派生类重写这个虚函数那么就可以用final进行修饰。
1.override
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive() override { cout Benz-舒适 endl; }
};
int main()
{return 0;
}2.final
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout Benz-舒适 endl; }
};
int main()
{return 0;
}8.重载、重写/覆盖、隐藏这三者的区别 三、纯虚函数和抽象类的关系
在虚函数的后面写上 0 则这个函数就被称为纯虚函数。纯虚函数不需要定义实现因为要被派生类进行重写只需要声明即可。而包含纯虚函数的类叫做抽象类抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数那么派生类也是抽象类。 因此纯虚函数在某种程度上强制了派生类重写虚函数因为不重写实例化不出对象。
class Car
{
public:virtual void Drive() 0;
};class Benz :public Car
{
public:virtual void Drive(){cout Benz-舒适 endl;}
};️四、多态的原理
1.虚函数表指针
我们先来看一道题 下⾯编译为32位程序的运行结果是什么 A. 编译报错 B. 运行报错 C. 8 D. 12
class Base
{
public:virtual void Func1(){cout Func1() endl;}
protected:int _b 1;char _ch x;
};int main()
{Base b;cout sizeof(b) endl;return 0;
}根据我们之前学习的知识我们会觉得b的大小为8个字节。但实际上程序运行结果是12字节这是为什么呢这就是我们所要说的虚函数表指针。
Base类除了_b和_ch成员还多⼀个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。⼀个含有虚函数的类中都至少都有⼀个虚函数表指针因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中虚函数表也简称虚表。
2.多态是如何实现的
我们以买票为例来探讨多态的实现。
通过下图我们可以看到当满足多态条件后底层不再是编译时通过调用对象来确定函数的地址而是运行时到通过指向的对象的虚表中去确定对应的虚函数的地址这样就实现了指针或引用指向基类去调用基类的虚函数指向派生类去调用派生类对应的虚函数。 第⼀张图ptr指向的Person对象调用的是Person的虚函数第⼆张图ptr指向的Student对象调用的是Student的虚函数。 3.动态绑定和静态绑定
对不满足多态条件(指针或者引用调用虚函数)的函数调用是在编译时绑定也就是编译时确定调用函数的地址叫做静态绑定。
满足多态条件的函数调用是在运行时绑定也就是在运行时到指向对象的虚函数表中找到调用函数的地址也就做动态绑定。
五、虚函数表
1.概念
虚函数表本质是⼀个存虚函数指针的指针数组⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C并没有进行规定各个编译器自行定义的vs系列编译器会再后面放个0x00000000标记g系列编译不会放)
基类对象的虚函数表中存放基类所有虚函数的地址。
派生类的虚函数表中包含基类的虚函数地址派生类重写的虚函数地址以及派生类自己的虚函数地址三个部分。
派生类由两部分构成分别是继承下来的基类和自己的成员⼀般情况下继承下来的基类中有虚函数表指针自己就不会再生成虚函数表指针。但是要注意的是这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个就像基类对象的成员和派生类对象中的基类对象成员也独立的。
2.虚函数和虚函数表两者的存储位置
虚函数和普通函数⼀样的编译好后是⼀段指令都是存在代码段的只是虚函数的地址又被存放到了虚表中。
虚函数表的存储位置在C标准中并没有规定取决于不同的编译器在vs中虚函数表是存放在代码段(常量区)的。