初入c++(六)虚函数实现多态,虚析构函数,虚函数表和多态实现机制,纯虚函数。

来源:互联网 发布:网络主播直播结束语 编辑:程序博客网 时间:2024/05/21 17:00

1.c++多态的概念以及用途。

1.1虚函数实现多态
通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数。
解决问题的办法:使用虚函数(virtual function),只需要在函数声明前面增加virtual关键字。看下面的代码。
#include <iostream>using namespace std;class people{public :people(char *name, int age);virtualvoid show();protected :char *m_name;int m_age;};people :: people (char *name, int age) : m_name(name), m_age(age){}void people :: show(){cout<<m_name<<", "<<m_age<<" old"<<endl;}class student : public people{public : student (char *name, int age, float score);virtual void show();protected :float m_score;};student :: student (char *name, int age, float score) : people(name, age), m_score(score){}void student :: show(){cout<<m_name<<", "<<m_age<<" old, "<<m_score<<endl;}int main(){people *p = new people("xiaoli", 22);p->show();p = new student("xiaozhang", 16, 95.6);p->show();delete p;return 0;}
结果:
aoli, 22 old
xiaozhang, 16 old, 95.6
如果不加virtual关键字,则运行结果是:
xiaoli, 22 old
xiaozhang, 16 old

有了虚函数,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,这就是多态(Polymorphism)!

c++中虚函数的唯一用处就是构成多态。

1.2借用引用也可以实现多态
在之前的学习中,可以了解到,指针在本质上是通过指针的方式实现的(引用只是对指针进行了封装,底层还是通过指针实现的)。
把上面的例子改一下:
int main()
{
people p("xiaoli", 22);
    student q("xiaozhang", 16, 95.6)

people &rp = p;
people &rq = q;

rp.show();
rq.show();

return 0;
}
实现的功能和上面的例子是一样的。


1.3虚函数的要点
虚函数对于多态具有决定性的作用,有虚函数才能构成多态!


对于虚函数还有以下几点注意点:
1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。


2) 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。


3) 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。


4) 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。


5) 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。


6) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。


构成多态的条件:
1.必须存在继承关系!
2.继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系!
3.存在基类的指针,通过该指针调用虚函数!
看下面的代码。
#include <iostream>using namespace std;class base{public :virtual void func();virtual void func(int);};void base :: func(){cout<<"base:func()"<<endl;}void base :: func(int a){cout<<"base:func(int)"<<endl;}class der : public base{public :void func();void func(char *);};void der :: func(){cout<<"der:func()"<<endl;}void der :: func(char *b){cout<<"der:func(char *)"<<endl;}int main(){base *p = new der;p->func();          //调用的是派生类的虚函数,构成了多态。p->func(100);       //调用的是基类的虚函数,因为派生类中没有函数遮蔽它。//p_>func("hello"); //出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。return 0;}
再来补充一下什么时候需要用到虚函数:
首先看成员函数所在的类是否会作为基类。
然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

2.虚析构函数

首先要明白,构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义!
先看代码
#include <iostream>using namespace std;class base{public :base();virtual ~base();protected :char *ptr;};base :: base(){ptr = new char[100];cout<<"base construct"<<endl;}base :: ~base(){delete [] ptr;cout<<"~base deconstruct"<<endl;}class der : public base{public :der();~der();protected :char *name;};der :: der(){name = new char[100];cout<<"der construct"<<endl;}der :: ~der(){delete[] name;cout<<"~der deconstruct"<<endl;}int main(){base *p = new der;delete p;return 0;}
将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;
也就是说,指针指向哪个类的对象就调用哪个类的函数。
(补充!!:指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数。如果指针是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,
在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的)。
在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,
那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。

3.虚函数表和多态实现机制。

强调!!!当通过指针访问类的成员函数时:
1.如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数。
2.如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;
也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

看一个例子:
#include <iostream>#include <string>using namespace std;//People类class People{public:    People(string name, int age);public:    virtual void display();    virtual void eating();protected:    string m_name;    int m_age;};People::People(string name, int age): m_name(name), m_age(age){ }void People::display(){    cout<<"Class People:"<<m_name<<"今年"<<m_age<<"岁了。"<<endl;}void People::eating(){    cout<<"Class People:我正在吃饭,请不要跟我说话..."<<endl;}//Student类class Student: public People{public:    Student(string name, int age, float score);public:    virtual void display();    virtual void examing();protected:    float m_score;};Student::Student(string name, int age, float score):    People(name, age), m_score(score){ }void Student::display(){    cout<<"Class Student:"<<m_name<<"今年"<<m_age<<"岁了,考了"<<m_score<<"分。"<<endl;}void Student::examing(){    cout<<"Class Student:"<<m_name<<"正在考试,请不要打扰T啊!"<<endl;}//Senior类class Senior: public Student{public:    Senior(string name, int age, float score, bool hasJob);public:    virtual void display();    virtual void partying();private:    bool m_hasJob;};Senior::Senior(string name, int age, float score, bool hasJob):    Student(name, age, score), m_hasJob(hasJob){ }void Senior::display(){    if(m_hasJob){        cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,并且顺利找到了工作,Ta今年"<<m_age<<"岁。"<<endl;    }else{        cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,不过找工作不顺利,Ta今年"<<m_age<<"岁。"<<endl;    }}void Senior::partying(){    cout<<"Class Senior:快毕业了,大家都在吃散伙饭..."<<endl;}int main(){    People *p = new People("赵红", 29);    p -> display();    p = new Student("王刚", 16, 84.5);    p -> display();    p = new Senior("李智", 22, 92.0, true);    p -> display();    return 0;}
各个类的对象内存模型如下所示:


这里的东西及原理有些复杂,况且还是单继承。对于多继承,复杂程度不可想象,这里不做深究,有兴趣的同学自己研究吧。知道怎么用虚函数的我,先满足一下。代以后再深究。

4.纯虚函数

在C++中,可以将虚函数声明为纯虚函数,
语法格式为:
               virtual 返回值类型 函数名 (函数参数) = 0;
1.纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
2. 最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

包含纯虚函数的类称为抽象类(Abstract Class)。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。看代码
#include <iostream>using namespace std;class line{public:line(float len);virtual float area() = 0;virtual float volume() = 0;protected:float m_len;};line :: line(float len): m_len(len){}class rec: public line{public:rec(float len, float width);float area();protected:float m_width;};rec :: rec(float len, float width): line(len), m_width(width){}float rec :: area(){return m_len * m_width;float area();protected:    float m_width;}Rec::Rec(float len, float width): Line(len), m_width(width){}float Rec::area(){return m_len * m_width;}class cuboid: public rec{public:cuboid(float len, float width, float height);    float area();    float volume();protected:    float m_height;};Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){}float Cuboid :: area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height);}float Cuboid :: volume(){return m_len * m_width * m_height;}class Cube: public Cuboid{public:    Cube(float len);    float area();    float volume();};Cube::Cube(float len): Cuboid(len, len, len){}float Cube::area(){return 6 * m_len * m_len; }float Cube::volume(){return m_len * m_len * m_len; }int main(){    Line *p = new Cuboid(10, 20, 30);    cout<<"The area of Cuboid is "<<p->area()<<endl;    cout<<"The volume of Cuboid is "<<p->volume()<<endl;      p = new Cube(15);    cout<<"The area of Cube is "<<p->area()<<endl;    cout<<"The volume of Cube is "<<p->volume()<<endl;return 0;} 
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

注意:
1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。

2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。