一、多态(更多见day9)
1、多态条件
1)多态特性除了要在基类中声明虚函数,并在子类中形成有效的覆盖,还必须通过指针或者引用来调用虚函数,才能表现出来,直接通过对象无法进行多态调用。
2)调用虚函数的指针也可以是this指针,只要它是一个指向子类对象的基类指针,同样可以表现出多态的特性。
class Base{ public: virtual int cal(int x,int y){ retunr x+y; } //d.foo()-->foo(&d) //void foo(Base* this) //Base* this =&d; void foo(void){ cout<<cal(100,200)<<endl; } }; class Derived:public Base{ public: int cal(int a,int b){ return a*b; } } int main(void){ Derived d; Base b=d; cout<<b.cal(100,200)<<endl;//调用的是基类中的版本,不会形成多态调用 d.foo();//子类对象中是没有foo函数的,但是由于传进去的this虽然是Base*,但是指向的是Derived*类型的&d,所以在调用时,相当于this->cal(100,200),故形成多态调用 return 0; }
二、纯虚函数、抽象类、纯抽象类
1、纯虚函数
virtual void draw(void)=0;//纯虚函数
纯虚函数的一般形式如下:
virtual 返回类型 函数名(参数列表)[const]=0;
2、抽象类
1)如果一个类中包含了纯虚函数,那么它就是抽象类。例如,前述的Shape类就是一个抽象类,类并不包含具体的行为。所以编译器不允许抽象类实例化对象。如果实例化,会出现下面的错误:
2)另外,如果继承过来的基类具有纯虚函数,并且子类不做覆盖的话,那么子类也将变成抽象类。
3)如果一个类中的所有成员函数(不包括构造函数,析构函数)都是纯虚函数,那么这个类就叫做纯抽象类。
3)工厂模式举例
Team1负责解析 class PDFParse{ public: void prase(const char* pdffile)//解析图形,文本,图片函数 { OnCircle(); OnRect(); Ontext(); OnImage(); //。。。 } private: virtual void Oncircle(void)=0; virtual void OnRect(void)=0; virtual void OnText(void)=0; virtual void OnImage(void)=0; }; Team2负责绘图实现 class PDFRender:public PDFParse{ private: void OnCircle(void){ ... } void OnRect(void){ ... } void OnText(void){ ... } void OnImage(void){ ... }; }; int main(void){ PDFRender render; render.parse("something.pdf");//通过this指针实现多态 return 0; }
4)多态实现的原理(了解)
1:用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
2:存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
3:多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
4:多态用虚函数来实现,结合动态绑定.
5:纯虚函数是虚函数再加上 = 0;
6:抽象类是指包括至少一个纯虚函数的类。
实现原理:
(1)对于编译器来说,非虚函数的调用地址在在编译的时候就被绑定,这样的绑定称为早期绑定。如果是非虚函数,即使子类的指针或者引用向上造型到基类的指针或引用,调用同名函数时也只是调用基类中的函数,因为这在编译阶段就已经绑定好了,在执行时无法改变。
(2)在(1)中如果要使用基类的指针调用子类的函数,就要使用带虚函数
(3)在任何一个类中如果有一个虚函数,那么就会为这个类添加一个虚函数表指针(三级指针,函数指针一般为一级指针,虚表中的后半部分为虚函数指针数组,可知它为二级指针),指向虚函数表(简称虚表),如上图,foo(),虚表指针在对象中占四个字节大小,虚表不属于对象,而是通过虚表指针来取其中的内容。图中,基类中foo()函数为虚函数,子类继承之后也为虚函数,并且都存在虚表,并且覆盖了A类的foo()函数的起始地址,添加上自己类的地址。而bar函数没有被重写,则原封不动的继承该函数,起始地址也不变。
(4)虚表指针vptr属于晚绑定,会根据实际指针指向的类型或引用的实际类型进行调用。每个对象的虚函数的调用都是通过虚函数来进行索引的,就像数组有起始地址和下标一样。所以虚表指针的正确初始化就显得十分重要。那么虚表指针是何时进行创建和初始化的呢?虚表指针其实是在构造函数中被创建和初始化的,(3)中也说了,虚表指针属于对象的一部分,占4个字节的内存大小,所以在构造函数中初创建和初始化显得理所当然。
(5)在构造时,先要构造基类的基类子对象,编译器看到父类具有虚函数,就创建和初始化基类的虚表,当构造子类对象时,发现了子类的虚函数,就对基类需要覆盖的虚表进行覆盖,就如foo()函数被覆盖,但是bar()函数没有被覆盖。并且在子类中会有自身的虚表指针(区别于基类的虚表指针),这就是为什么通过指针或者引用调用时可以实现多态的原因。而直接使用对象调用时,不用虚表指针进行索引,直接调成员函数(通过基类的对象调用父类的函数(这个函数实现了虚函数,内部其实是this指针起了作用)除外)。
三、虚析构函数
1、引入
一个指向子类对象的基类指针进行析构的时候,只能调用基类的析构函数,而无法调用子类的析构函数,前面学到的方法是把基类的指针做一个向下造型进行析构。实际中并不这样做,而是使用虚析构函数来解决。
class Base{
public:
Base(void){
cout<<"Base::Base()"<<endl;
}
virtual ~Base(void){
cout<<"Base::~Base()"<<endl;
}
};
class Derived:public Base{
public:
Derived(void){
cout<<"Derived::Derived()"<<endl;
}
~Derived(void){
cout<<"Derived::~Derived()"<<endl;
}
};
int main(void){
Base* pb=new Derived;//如果不声明为虚析构函数,这种方式释放内存将有内存泄漏风险
delete pb;
return 0;
}
如果基类的析构函数为虚函数,那么子类的析构函数也是虚函数,可以对基类的析构函数进行有效覆盖。这时候再delete指向子类的基类指针,实际调用的是子类的析构函数,而子类的析构函数又会调用基类的析构函数,这样可以避免上述问题。
四、练习--薪资计算
员工属性:姓名,工号,职位级别,绩效工资,出勤率
经理:绩效奖金(元/月)
技术员:研发津贴(元/小时)
销售员:提成比率(百分比)
薪资=基本工资+绩效工资
基本工资=职位级别额度*出勤率
绩效工资:因职位不同而异
普通员工:基本工资一半
经理:绩效奖金*绩效因数(手动输入)
技术员:研发津贴*工作小时数*进度因数(手动输入)
销售员:销售额度(手动输入)*提成比例
技术主管:(技术员绩效工资+经理的绩效工资)/2
销售主管:(销售员绩效工资+经理绩效工资)/2
结果:打印员工数据,输入必须输入的数据,计算薪资