河南网站建设优化推广,门窗网站免费模板,青岛wordpress建站,东莞横沥三江工业区一、引言 众所周知#xff0c;C有三大特性#xff0c;它们分别是封装、继承和多态#xff0c;在之前的文章中已经详细介绍过封装和继承了#xff0c;今天我们将一起学习多态相关的知识#xff0c;如果还想了解封装、继承相关的知识#xff0c;可以跳转到以下链接#xf…一、引言 众所周知C有三大特性它们分别是封装、继承和多态在之前的文章中已经详细介绍过封装和继承了今天我们将一起学习多态相关的知识如果还想了解封装、继承相关的知识可以跳转到以下链接 1、封装C类和对象上-CSDN博客 2、继承C继承-CSDN博客
二、多态的概念 1、概念 通俗来讲多态表示多种状态即就是说当面对不同类型、不同特点的对象时处理一个问题时采用不同的方式从而产生不同的效果这就是多态 2、分类 事实上多态细分之下有两种它们分别是静态多态和动态多态我们常说的多态事实上代指动态多态也就是我们今天将要主要讨论的内容在详细了解了多态的相关知识之后我们将再来理解这两个概念 3、从实际的角度认识多态 上面我们介绍了多态的概念这样我们可以按图索骥大概举几个日常生活中常见的多态的实际应用 (1).打滴滴 在打滴滴时新人用户常常会享受较大的优惠力度小编记得在我第一次打滴滴时价格优惠到了4元那天的路程还挺远的如果放在今天可能会在十元往上这里就用到了多态的相关知识(猜测)当一个新人用户和一个老用户同样的调用打车接口时却对应了不同的优惠力度这正好对应了多态的概念 (2).买票系统 我们日程生活中会进行各种各样的买票操作比如各个景点或者是买回家的车票不难发现常见的对象会被平台分为普通身份、学生、军人等 当这些对象同样调用买票接口时普通身份会全家买票学生是半价买票军人常见的则是优先买票很明显不同的对象调用同一接口产生了不同的效果对应了多态的概念 通过以上两个常见的概念我们可以感受到多态的相关知识是存在在我们生活中的方方面面的
三、多态的定义及实现 1、虚函数 虚函数即就是被virtual关键字修饰的函数
class Person
{
public:virtual void buy_t(){cout 全价购票 endl;}
}; 2、虚函数的重写 虚函数的重写派生类中有一个函数跟基类的虚函数三同(即函数名、函数参数、函数返回值都相同)的函数那么就称该派生类重写(覆盖)了基类的虚函数例如
class Person
{
public:virtual void buy_t(){cout 全价购票 endl;}
};
class Student : public Person
{
public:void buy_t(){cout 半价购票 endl;}
};以上的情况我们就说Student类重写了Person类中的buy_t函数 但是需要注意的是虚函数重写存在以下两个例外 (1).协变(基类与派生类函数返回值不相同) 派生类重写基类虚函数是与基类函数返回值类型不同当且仅当一个继承体系的返回值对应的返回了一个继承体系(并不限制一定是本地的继承体系)的指针或引用这时候仍然构成虚函数重写称为协变(了解即可不推荐使用)例如
class A{};class B : public A {};class Person {public:virtual A* f() {return new A;}};class Student : public Person {public:virtual B* f() {return new B;}}; (2).析构函数的重写(基类与派生类析构函数名不相同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同 看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处 理编译后析构函数的名称统一处理成destructor 所以为什么要这样特殊处理析构函数使它可以构成虚函数重写呢我们从下面一个例子来看
class Person {public:virtual ~Person() {cout ~Person() endl;}};class Student : public Person {public:virtual ~Student() { cout ~Student() endl; }};// 只有派生类Student的析构函数重写了Person的析构函数下面的delete对象调用析构函
数才能构成多态才能保证p1和p2指向的对象正确的调用析构函数。
int main(){Person* p1 new Person;Person* p2 new Student;
delete p1;delete p2;}return 0; 上面的代码中p1、p2都是Person*的变量随后调用delete对这两个动态申请的空间进行释放事实上delete对于自定义类型会调用对应类的析构函数此时就产生了一个问题两个空间都会调用Person的析构函数这是我们不想看到的我们希望的是对于p1调用Person的析构函数而对于p2则是调用Student的析构函数 这时候我们可以认真的观察一下我们上面的需求好像就是使用基类的指针来调用同一个函数同时我们想让该调用动作对于不同的对象产生不同的效果是的这就是我们前面多态讨论过的需求现在只有一个条件还没有满足就是函数名并不相同所以我们顺理成章的想到要让编译器对析构函数名进行特殊处理这样在将基类的析构函数写为虚函数时自然的就解决了上面的问题 3、多态的构成条件 多态是在继承关系中不同的类对象调用同一函数产生了不同的行为比如Student继承了Person这时候Person对象全价买票Student对象半价买票所以首先的多态是存在在继承关系中的 在继承关系中要构成多态还有两个条件 (1).必须通过基类的指针或者饮用调用函数 (2).被调用的函数必须是虚函数同时派生类对基类的虚函数进行重写 下面是构成多态的一个完整例子 #include iostream
using namespace std;
//多态
class Person
{
public:virtual void buy_t(){cout 全价购票 endl;}
};
class Student : public Person
{
public:void buy_t(){cout 半价购票 endl;}
};
void func(Person rp)
{rp.buy_t();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
} 这一段代码的运行结果如下
四、C11中提供的两个相关的关键字override和final 经过上面的讲解我们发现C中构成重写从而构成多态的过程时非常严格的而在平常的代码工作中我们很容易会犯一些错误比如大小写的问题、字母顺序的问题这些问题产生时是很难发现的对于这些问题只是没有构成重写但并没有编译、链接的错误不会报错非常头疼所以在C11中我们提供了override和final两个关键字它们两个可以帮助我们检查这一类问题 1、final该关键字有两个作用 (1).修饰虚函数被修饰的函数不能被重写
class Person
{
public:virtual void buy_t ()final//final修饰了该函数{cout 全价购票 endl;}
};
class Student : public Person
{
public:void buy_t()//这个位置会报错无法重写“final”函数 Person::buy_t{cout 半价购票 endl;}
}; (2).修饰一个类被修饰的类不能被继承 #include iostream
using namespace std;
//多态
class Person final//使用final修饰这个类
{
public:virtual void buy_t(){cout 全价购票 endl;}
};
class Student : public Person//这个位置会报错不能将final类类型用作基类
{
public:void buy_t(){cout 半价购票 endl;}
}; 2、override检查派生类函数是否重写了基类某个虚函数如果没有就报错 class Person
{
public:virtual void buy_t(){cout 全价购票 endl;}
};
class Student : public Person
{
public:void buy_tx() override//override修饰该函数//该位置报错使用override修饰的函数不能重写基类成员{cout 半价购票 endl;}
};
五、对比重载、重写(覆盖)、重定义(隐藏) 六、抽象类 1、概念 在虚函数的函数头之后加上0此时该函数被称为纯虚函数包含纯虚函数的类叫做抽象类(也叫做接口类)抽象类不能实例化出对象。派生类继承之后也不能实例化出对象只有重写了纯虚函数派生类才能实例化出对象纯虚函数规范了派生类必须重写它更能体现出接口继承 下面的代码体现出了这种接口继承的思想
#include iostream
using namespace std;
//多态
class Person
{
public:virtual void buy_t() 0;};
class Student : public Person
{
public:void buy_t(){cout 半价购票 endl;}
};
class Teacher :public Person
{
public:void buy_t(){cout 十倍价钱购票 endl;}};
void func(Person rp)
{rp.buy_t();
}
int main()
{Teacher t;Student s;func(t);func(s);return 0;
} 下面是以上代码的执行结果 2、接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数
七、多态的原理 1、虚函数表 (1).引入
// 这里常考一道笔试题sizeof(Base)是多少
class Base{public:virtual void Func1(){cout Func1() endl;}private:int _b 1;}; 我们先通过打印的方式看一下这个问题的结果是多少 (2).解决问题 可以看到结果输出了8(这里要强调一下小编实在x86的环境下输出的环境或者平台改变可能会影响结果)这是为什么呢或许含有虚函数的类对象进行了一些特殊处理接下来我们通过调试的方法来看一下该类对象模型是怎样的 经过上面的调试窗口我们知道原来在Base类中除了_b成员还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代 表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表那么派生类中这个表放了些什么呢我们接着往下分析 为了符合多态的情景我们先对上面的代码做出以下改造
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base// 2.Derive中重写Func1// 3.Base再增加一个虚函数Func2和一个普通函数Func3class 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;};class Derive : public Base{public:virtual void Func1(){cout Derive::Func1() endl;
}private:int _d 2;};int main(){Base b;Derive d;return 0;} 接下来我们一起观察这个加强版继承体系的类对象模型从而说明派生类中的虚表有什么不同 可以观察到继承之后的d对象模型中分为两个部分分别是Base部分和自己的成员而在Base部分中也有一个_vfptr指针这意味着d不会生成自己的虚表指针而是以继承的形式沿用了Base类的指针而两个指针指向的位置是不同的这就是说两个类的虚表是不同的事实上的确是这样的派生类会首先继承基类的虚表然后对于重写过的函数将新的函数指针覆盖原本的函数指针形成了属于自己的虚表 2、多态的实现 经过上面对于虚表指针和虚表的认识我们大概也可以想到多态究竟是如何实现的 事实上多态的实现原理就是虚表指针存在在父子类中基类的部分所以必须使用基类的指针或者引用调用(不能直接使用对象调用是因为对象的切片赋值会丢失信息而指针和引用的切片赋值不会)同时通过虚表指针我们就可以找到虚表父子类的虚表不同找到的函数也就不同这时候就实现了多态调用函数 3、动态绑定与静态绑定 (1). 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载 (2). 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态
八、结语 这就是本期有关多态的全部内容了感谢大家的阅读欢迎各位于晏、亦菲和我一起交流、学习、进步 、