wordpress 评论 原理韶关网站seo
前言:本文将介绍在32位平台下,c++的多态,通过本篇文章的学习你讲了解多态的原理,多态的底层还有一些不常见的关键字的介绍(final,override).
文章内容如下:
1:多态的概念
2:多态的定义与实现
3:多态的原理
4:抽象类
文章正式开始
1:多态的概念
多态:不同对象去完成相同的行为时,产生不同的状态。
我们可以用一个场景来增强对多态概念的理解: 12306铁路的买票
比如说:成人与学生,买票的价格是不一样的,成人买票需要全价,学生买票只需半价,在这个经典的列子中,成人与学生他们都是去做相同的行为(买票),但是在买的时候却出现了不同的状态(价格不同)。
在我们日常还有许许多多的例子比如:安卓手机买飞机票与苹果手机买飞机票的价格也不同,在比如说我们过年支付宝扫码,同样是扫码不同的人却获得了不同的红包大小。
按照类型来分:多态可以分为:动态的多态与静态的多态
静态多态:程序编译阶段就确定了函数的地址 :函数重载
动态多态:在程序运行期间通过去对象中虚表中找到覆盖的函数的地址
2:多态的定义与实现
前提:我们是在研究public继承过程中多态的条件。
1:(父子类中)虚函数必须实现重写(函数名,返回值,参数类型这三个必须相同)
2:父类对象的指针或者引用去调用实现重写的虚函数。
在介绍多态之前我们先来了解几个概念:虚函数,虚函数的重写
虚函数:在成员函数最前面+virtual关键字--->表示该成员函数是虚函数
虚函数重写:父子类中三同(函数名\参数\返回值)
下面我们通过代码再次加深对概念的理解。
class A
{
public:void fun1() { cout << "A::fun1() " << endl; }virtual void fun2() { cout << "A::virtual fun2()" << endl; }
private:int _a;
};
class B:public A
{
public:void fun1() { cout << "B::fun1() " << endl; }virtual void fun2() { cout << "B::virtual fun2()" << endl; }
private:int _b;
};
在上面两个类中: A类中:fun1为普通函数,fun2为虚函数
B类中: fun1也为普通函数,fun2为虚函数 所以根据概念我们可知:在A类和B类中fun1的关系为子类隐藏父类,fun2为重写的关系。知道了上述关系之后下面我们来实现多态调用的过程。
未满足多态的条件。调用情况下
多态调用的本质:如果是子类对象给父类对象的指针或引用,那么在多态调用的时候会去调用子类中重写的函数,如果是父类对象给父类对象的指针或者引用则调用父类中的函数,走的函数匹配原则。
其实c++在确定多态调用的情况下还给了几种特殊的情况,如果满足则也构成多态的条件。
1:协变:虚函数的返回值可以不同,但是该返回值如果是父子类的指针或引用则也满足重写。
如下图展示:
这个AB类与我们上面的类的关系一样B继承A,通过代码我们也可以知道尽管两个函数的返回值不一样,但是c++做了一个特殊处理,只要返回值为父子类指针或引用类型就可以构成多态。
2:析构函数的重写
父子类中析构函数会被做一个特殊的处理,在编译阶段编译器会将父子类中的析构函数的函数名改为一样的,也就是说我们只需要在父子类析构函数前面加一个virtual虚函数关键字那么这两个函数也构成重写。
如下图:
在这个场景中我们可以看到,这个代码是有问题的因为,没有完成对子类对象的释放工作存在内存泄露,编译器经过编译阶段将父子类析构函数的函数名改为一样的,导致只调到了子类的析沟,所以在这种情况下我们需要完成对析构函数的重写,才能防止内存泄露。
所以我们默认给父子类中都加上virtual关键字,实现对虚函数的重写。
总结:析构函数的特殊处理,对于父子类析构函数建议加上virtual。
3:只要父类中加了虚函数关键字virtual,子类可以不加,也构成重写但是一般建议加.
c++的重写:是对内容进行重写,对于接口会被继承下来。
c++重载、重写(覆盖)、隐藏(重定义)概念的区分:
两个关键字的介绍:c++11 :final override
final:1:final修饰一个类表明一个类不能被继承。(将一个类中的构造函数私有,这个类也不能被继承)。
2:final修饰虚函数表明该虚函数不能被重写,如果函数被重写编译器会直接报错
override关键字:能够检验一个派生类重写虚函数是否正确,不正确编译器直接报错。
3:多态的原理
在了解虚函数原理之前我们得先了解虚函数表指针/虚表指针.
我们先来看一个普通的类(含有虚函数)他的大小会是多少?
代码如下:
class A
{
public:virtual void fun1(){cout << "fun1" << endl;}
private:int _a;
};int main()
{cout << sizeof(A) << endl;return 0;
}
这个类大小如果按照我们之前所学的知识我们可能认为它的值为4,但是并不是这样,因为对于类对象的存储有三种模型。
1:将函数与变量都存在对象中.(显然这个模型不适合存储,太浪费空间了,调用的函数相同)
2:将函数存在公共的区域,对象中只存储成员变量(适用于无虚函数的类)
3每个对象除了存储成员变量还会存储类函数表地址,函数表里为类成员函数的地址。(有虚函数的类).
所以上面类A就为第三种存储模型,所以计算的大小为8(一个成员+一个指针).
在cpp中我们将这个新加的指针_vfptr称为:虚函数表指针,他指向一个虚函数表(本质是一个函数指针数组)
那么在单继承中多态的原理是什么呢?
class A
{
public:virtual void fun1(){cout << "A:fun1()" << endl;}virtual void fun2(){cout << "virtual void fun2()" << endl;}
private:int _a;
};
class B :public A
{
public:virtual void fun1(){cout << "B:fun1()" << endl;}
private:int _b;
};
在这个场景中:B类继承A,会将A类中的虚函数表也给继承下来,只需要对完成重写的函数进行地址的覆盖就行,在继承过程中并不会增加新的虚函数表。如果B中还有其它虚函数则会在A中虚表中再加入一个地址。
总结:虚函数表会被继承下来且不会增加再生成一个虚函数表,重写的虚函数会覆盖原父类中的虚函数,派生类的虚函数会被添加到基类中虚函数表中。
将单继承可以看作下列的模型:子类=父类+自身变量
那么在多继承中又是什么样子的呢?
class A
{
public:virtual void fun1(){cout << "A:fun1()" << endl;}virtual void fun2(){cout << "virtual void fun2()" << endl;}
private:int _a;
};class C
{
public:virtual void fun1(){cout << "virtual void fun1()" << endl;}
private:int _c;
};class D :public A, public C
{
public :virtual void Fund(){cout << " virtual Fund()" << endl;}
private:int _d;
};
我们看到在多继承中:只要继承了几个父类就有几张虚表,并且如果派生类中还有虚函数,那么它会被添加到先继承类中的虚函数表中。多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
总结:多态的原理:通过基类对象的指针或者引用去引用派生类对象,那么在运行阶段,就会在虚函数表中找到覆盖的函数调用。
补充知识:虚函数也是一个函数,最终形成可执行代码,所以虚函数是存在代码段的,而虚函数的地址被存放在虚函数表中的,而对象中存的是虚表指针,而在vs与g++下虚函数表也是存在代码段的(常量区).
4:抽象类
概念:含有纯虚函数的类叫做抽象类.
纯虚函数:在虚函数后面+ =0的就叫做纯虚函数.
如果一个类继承抽象类不做任何其它处理,那么这个类也叫抽象类。
特点:抽象类不能实例化出对象,除非派生类中重写纯虚函数,规范了派生类的重写,另外纯虚函数也更加的体现出了接口继承.
接口继承与实现继承
普通函数的继承可以称为实现继承,而派生类虚函数的继承是接口继承是为了重写该虚函数,为了实现多态。
本章知识点分享完毕。