目录
前言
什么是多态
多态的定义及实现
虚函数
虚函数的重写
多态的构成条件
虚函数重写的两个例外
协变
析构函数的重写(重要!!!)
override 和 final(了解)
override
final
重载、重写、隐藏
抽象类
介绍
格式
抽象类实战
多态原理
虚函数表
多态原理
动态绑定 与 静态绑定
多态拓展
结语
前言
多态的学习,对继承的掌握有着一定要求,特别是切割赋值,继承后默认成员函数中的析构函数的相关问题
如果有想复习或者学习一下继承相关知识点的友友,可以看一看下面的这篇继承博客╰(*°▽°*)╯
【C++】面向对象三大特性之—— 继承 | 详解
什么是多态
现实世界中有很多例子都属于多态的范畴
比如,小猫叫是喵喵、小狗叫是汪汪;再比如备胎给女生发消息,和男神给女生发消息,结果是不一样的
所以我们由此可以引出多态的概念:多态就是不同的对象,做同一件事,结果不同
你看猫和狗,都是叫,一个是喵喵,一个是汪汪,结果不同
你看备胎和crush,都是发消息,一个是“哦”,一个是 ”你真贴心“,结果也不一样
综上两种,都可以称之为多态行为
多态的定义及实现
我们下文就以买票为例吧:
普通人买票是全票、学生买票是打折、军人买票是优先
虚函数
要实现多态,虚函数的了解是必须的,我们先来了解一下什么是虚函数
其实虚函数就是在普通的成员函数前面加一个virtual,如下:
class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};
我们可以看到,就是在成员函数前面加上一个virtual
当然这个virtual和继承中的菱形虚拟继承使用的virtual只是重复使用的关系而已,并无直接关联
就好像是C语言中的<<是左移,但是在C++中复用这个符号变成了流提取(cout)
虚函数的重写
虚函数的重写就是,我们这个Person类是父类,然后我们在其他类中继承了这个类,那么其他类中就会有这个虚函数,然后我们在其他类中再次将这个虚函数的函数体的内容重新根据不同类实现一遍而已
举个例子:
class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:virtual void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:virtual void buyticket(){cout << "军人——优先" << endl;}
};
虚函数的重写的注意事项!!!!
返回值、函数名、参数不能修改!!
能修改的只有函数里面的内容,相当于是拿父类那个虚函数的壳套在了这个函数上面
另外还有一个点就是:子类中的virtual可以不写,只要父类中的virtual写了,那么子类中的函数只要满足重写的其他条件,就是重写,举个例子:
class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:在这里加不加virtual都是虚函数的重写只要父类写了virtual就行void buyticket(){cout << "学生——打折" << endl;}
};
多态的构成条件
如果要构成多态的话,需要满足以下两个条件:
- 虚函数的重写
- 必须是父类的指针或引用调用
上面这两个条件,缺一不可,我们来看一个例子:
class Person
{
public:virtual void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:virtual void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:virtual void buyticket(){cout << "军人——优先" << endl;}
};void func(Person& p)
{p.buyticket();
}int main()
{Person per;Student stu;soldier sol;func(per);func(stu);func(sol);return 0;
}
解释一下这串代码:首先我们最上面写了三个类,一个Person做为父类,然后Student和soldier两个子类都继承了Person类
三个类里面都有virtual,两个子类都有重写虚函数
再往下看,我们写了一个函数func,这个函数的参数是一个父类的引用
最后我们在main函数里面实例化了三个类的三个对象,都用这个func
这时我们再来回头看看,都有虚函数的重写,也有父类的引用调用(func函数),所以我们可以断定,这串代码能够构成多态,结果如下:
再来检测一下是不是一定要这两个条件:
我们试一试去掉virtual,不让他形成虚函数重写:
class Person
{
public:void buyticket(){cout << "普通人——全票" << endl;}
};class Student : public Person
{
public:void buyticket(){cout << "学生——打折" << endl;}
};class soldier : public Person
{
public:void buyticket(){cout << "军人——优先" << endl;}
};
在其他地方没有改变的情况下,我们不让其满足虚函数重写的条件,我们能看到,全调用父类的那个函数去了
我们再试试不用父类的引用或指针:
void func(Person p)
{p.buyticket();
}
我们能看到,结果并没有改变,还是没有实现出多态这个行为
所以我们通过实践也得出了,如果想要实现多态行为,就必须满足两个条件:
- 虚函数的重写
- 必须是父类的指针或引用调用
虚函数重写的两个例外
协变
这个协变了解就好,现实中相当鸡肋,基本用不到,所以我们只需要了解有这么个东西,不要别人写的时候我们不知道就好
首先,协变是针对虚函数重写的
我们的虚函数要满足重写,就需要函数名、参数、返回值都不能改变
但是协变就是——返回值可以不同,但是必须是父类子类的引用或指针,我们来看一个例子:
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; }
};
上面代码的意思是说:A类是一个父类,B类是一个子类(继承A类),这时我们的Person和Student两个类里面的虚函数重写,可以返回值不同
父类中的是A*(父类指针),子类中的是B*(子类指针)
析构函数的重写(重要!!!)
一般情况下,析构函数并不会出什么问题,但是我们来看这样一种情况:
class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}int _a;
};class B : public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};int main()
{A* ptr = new B;delete ptr;return 0;
}
上面这一串代码的意思是:我们写了一个A类和一个B类,两个类之间是继承关系
但是我们再main函数里面,我们拿A* 的指针来指向B这个类,这时候就出问题了:
我们能看到,这个析构的调用是不是不太对啊,我们要两个类的析构都调用,但是他这就只调用了一个父类的析构,大大滴不对啊
而且根据我们在继承章节所学的知识,我们其实是应该调用子类的析构,因为我们在子类是不需要显示写析构的,编译器会在析构结束后自动调用父类的析构
如果对继承相关知识点有疑惑的话,可以看一下下面这篇文章,其中默认成员函数中的析构函数就有该知识点的讲解:
【C++】面向对象三大特性之—— 继承 | 详解
这么一听,感觉有点熟悉
这里是两个类,都要调用析构,我们要的是指向B类,就调用B类的析构,指向A类的,就调用A类的析构,这可不就是多态吗
如果这里能写成多态的话,我们的问题就迎刃而解了,因为他会自己调用B类的析构
这时我们回顾一下,多态需要的两个必须的条件:
- 虚函数的重写
- 必须是父类的指针或引用调用
A是父类,B是子类A* ptr = new B;
delete ptr;
光看代码的话,父类的指针或引用的条件是达到了,但是虚函数重写怎么办啊
虚函数的重写需要满足很苛刻的条件,一个是虚函数,一个是函数名、参数、返回值相同
当年祖师爷发现这个问题之后也是很苦恼,最后C++就发明了这么一种东西出来:
在编译完代码之后,所有的析构函数的函数名都会被改成destructer
这样的话,如果要解决上面的问题,我们就只需要在父类的析构函数后面加上一个virtual即可
因为上文提到过了,只要父类中的虚函数写了virtual,那么子类写不写其实无伤大雅,只不过最好加上,养成好习惯
综上,代码如下:
class A
{
public:A(){cout << "A()" << endl;}///父类析构函数加上virtual,构成虚函数重写virtual ~A(){cout << "~A()" << endl;}int _a;
};class B : public A
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}int _b;
};int main()
{main函数里面有父类的指针调用上面又有虚函数的重写,综上,构成多态指向子类,会调用子类的析构并且子类的析构会在调用完之后自动调用父类的析构(这是继承的知识点)A* ptr = new B;delete ptr;return 0;
}
override 和 final(了解)
override
这是C++11新添加的两个小语法
override是一个判断是否重写了的小语法,举个例子:
class A
{
public:virtual void Print(){cout << "支持博主" << endl;}
};class B : public A
{
public:加上了override,判断是否重写virtual void Print()override{cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}
我们简单地实现了一个多态,但是我们在虚函数的后面加上了一个override判断
这时候就是正常调用
如果我们试着不满足虚函数重写的条件呢?
class A
{
public:void Print(){cout << "支持博主" << endl;}
};class B : public A
{
public:void Print()override{cout << "一键三连" << endl;}
};
编译器就会报错,说没有完成重写
final
final这个语法就是:
- 修饰虚函数,表示这个虚函数不想被重写
- 修饰类,表示这个类不像被继承
class A
{
public://看这里virtual void Print()final{cout << "支持博主" << endl;}
};class B : public A
{
public:virtual void Print(){cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}
编译器就会报错,这是虚函数不能重写的情况
///看这里
class A final
{
public:virtual void Print(){cout << "支持博主" << endl;}
};class B : public A
{
public:virtual void Print(){cout << "一键三连" << endl;}
};void func(A& ref)
{ref.Print();
}int main()
{A a;B b;func(a);func(b);return 0;
}
这是不能被继承的情况
重载、重写、隐藏
这三个其实很好区分
首先是重载,重载的话有一个要求,就是两个函数想要重载就必须在同一个域里面,并且参数不相同,编译器会在后面根据参数的不同起不同的函数名,然后找地址调用
再来看看重写和隐藏
这两个其实是一个包含关系,隐藏里面包着重写
试想一下,隐藏,就只需要父类子类成员函数或成员变量名字相同,就会构成隐藏
但是重写却不仅仅要求名字相同,还有返回值,参数都相同才是,且必须是虚函数
所以我们可以这么界定这两个:
- 重写是一种特殊的隐藏
- 父类子类的同名函数,只要不构成重写的,就是隐藏
抽象类
介绍
抽象类是什么呢,试想一下:车,并不是一种具体的东西,这是一类东西
车里面有奔驰,宝马,法拉利等等,但是车,这是一个类型,是抽象出来的一个概念
所以,假设我们写了一个类,叫做car(车),但是车并不是一个具体的东西,只是一个类而已
所以我们就能将这个类设成一个抽象类,让其他的类来继承这个抽象类
格式
如果想要整出一个抽象类的话,我们只需要在虚函数后面加上一个 =0 即可
如下:
class car
{
public:virtual void comment() = 0;
};
注意,抽象类不能被实例化,所以我们继承了抽象类之后,必须要重写
而且不能直接将抽象类做为参数,只能是抽象类的引用或指针
抽象类实战
接下来我们简单拿抽象类实现一个多态,代码如下:
class car
{
public:virtual void comment() = 0;
};class benz : public car
{
public:virtual void comment(){cout << "奔驰" << endl;}
};class ferrari : public car
{
public:virtual void comment(){cout << "法拉利" << endl;}
};void func(car& c)
{c.comment();
}int main()
{benz be;ferrari fe;func(be);func(fe);return 0;
}
多态原理
虚函数表
在了解多态原理之前,虚函数表的了解是必不可少的
首先,我们写的所有虚函数,都会被放在虚函数表里面
可以这么理解,我们的对象里面有一个指针,这个指针指向一个函数指针数组,这个函数指针数组就是虚函数表
然后虚函数表里面全是虚函数的地址,这些虚函数和普通函数一样,都存在代码段那里
所以,如果没必要的话,不要什么函数都设成虚函数,因为这样子会增加编译器的负担,没有这个必要
重点来了!!
派生类继承了虚函数之后,会将虚函数的内容拷贝一份下来放进派生类的虚表里面
注意,不是同一个虚表,子类有自己的虚表
然后,当我们将虚函数重写了之后,重写的虚函数,就会将子类虚表中对应的虚函数给覆盖
多态原理
多态的原理就是,子类和父类的虚表是不一样的
当我们将虚函数重写了之后,重写后的虚函数会将原本子类中的那个虚函数给覆盖掉
而为什么要求要使用父类的指针呢?
回忆一下,我们在继承的时候提到过,当我们使用父类的指针或引用,指向或引用的对象是子类的时候,这个指针或引用,指向或引用的就是子类中父类的那一部分
这时我们再回头来看一下,虚表,也在引用的范围内(C++特殊规定)
而我们重写了之后的虚函数已经和原本父类的虚函数不一样了,这时再去拿父类指针或引用调用这个虚函数的时候,就能够实现多态的效果了
我们来画个图理解理解:
我们再来写一个例子:
class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}virtual void func2(){cout << "A::func2()" << endl;}void func3(){cout << "A::func3()" << endl;}
};class B : public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}
};void show(A& _a)
{_a.func1();_a.func2();_a.func3();
}int main()
{A a;B b;show(a);cout << endl << endl;show(b);return 0;
}
看我们上面的代码,我们写了两个虚函数在父类里面
但是我们之将func1重写了,func2没有重写
这时,func2就不会被覆盖,func1就会被覆盖,所以当我们调用的时候,func2因为没有重写所以其实还是父类中的那个函数,func1因为被重写了,所以就实现了多态
动态绑定 与 静态绑定
首先我们来回想一下,我们的多态,是在程序运行了之后,根据不同对象的虚表去找的函数,所以这其实是运行时绑定,也叫动态绑定
与之相反的是静态绑定,我们的普通成员函数,因为没有存进虚表里面,就是一个普通的函数,所以就会在编译的时候,将这个函数编译成一个代码段,根据这个代码段我们就能找到函数,所以这是在编译的时候就绑定的,也叫静态绑定
多态拓展
其实,不止是我们今天学的这些知识叫做多态,这太局限了
只是相对于C++来说,有这么一个章节的内容,叫做多态
其实在C++中,在现实生活中,很多场景我们都能叫其为多态
比如函数重载:我们看起来是调用了同一个函数,传了不同的参数进去之后,给到了不同的结果
虽然底层是函数重载,但光从表象上看,这就是多态行为
再比如这个:
int main()
{int a = 0, b = 1;char x = 'x', y = 'y';swap(a, b);swap(x, y);return 0;
}
能看到,我们都是调用swap这个函数,但是我们传的不同的参数,给出了不同的结果,这就是一种多态行为
聊完了C++的,再来聊一聊现实世界的
就好比我们开头举的例子:
crush和备胎给女生发消息,同样是发消息,得到的结果不同
同样是第一次做错事,美女和普通人得到的结果也不同
多态多态,就是多种状态,同一种行为的不同状态,不同人的同一种行为的不同状态
当我们将思路打开,理解透了多态的本质之后,即使现在学的忘记了,以后回来看一眼也就秒懂了
结语
看到这里,这篇博客有关多态的相关内容就讲完啦~( ̄▽ ̄)~*
如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)