多态与虚函数

多态与虚函数

在引入虚函数概念之前,我们先看这个例子

class Person
{
public:
     void BuyTickets()
     {
          cout << "person: 买票-全价" << endl;
     }
protected:
     string _name; // 姓名
};

class Student : public Person
{
public:
     void BuyTickets()
     {
          cout << "student: 买票-半价 " << endl;
     }
protected:
     int _num; //学号
};
int main()
{
     Person p, *pp; //定义父类对象p和对象指针pp
     Student s;    //定义子类对象s


     pp = &p;   //对象指针pp指向父类对象p
     pp->BuyTickets();

     pp = &s;     //对象指针pp指向子类对象s
     pp->BuyTickets();

     return 0;
}

这里写图片描述
这个程序的运行结果不是预想的,为什么会出现这样的情况呢?
在C++中规定:父类的对象指针可以指向它公有继承的子类对象,但是当其指向其公有继承的子类对象时,他只能访问子类中从父类继承来的成员,而不能访问子类中定义的成员。
所以当我们执行语句
pp = &s; //对象指针pp指向子类对象s
pp->BuyTicket();
后,指针pp指向了子类对象s,但是执行语句“pp->BuyTicket();”后,调用的不是子类的派生成员函数BuyTicket();而是子类中从基类继承继承来的同名函数BuyTicket()。

要解决这种情况,我们需要将基类的成员函数前面加上关键字virtual,并在子类中重写。

class Person
{
public:
     virtual void BuyTickets()
     {
          cout << "person: 买票-全价" << endl;
     }
protected:
     string _name; // 姓名
};

class Student : public Person
{
public:
     virtual void BuyTickets()
     {
          cout << "student:买票-半价 " << endl;
     }
protected:
     int _num; //学号
};
int main()
{
     Person p, *pp; //定义父类对象p和对象指针pp
     Student s;    //定义子类对象s


     pp = &p;   //对象指针pp指向父类对象p
     pp->BuyTickets();

     pp = &s;     //对象指针pp指向子类对象s
     pp->BuyTickets();

     return 0;
}

这里写图片描述
这样就和我们预想的结果一样了。可是为什么把基类中的成员函数定义为虚函数运行结果就正确了呢?
这里写图片描述
这是因为,关键字virtual指示C++编译器,函数调用 “pp->BuyTickets();” 要在运行时确定所要调用的函数,即要对该调用进行动态连编。因此,程序在运行时根据指针pp所指向的实际对象,调用该对象的成员函数。

我们把这种使用同一种调用形式,调用的确是同一类族中不同的类的虚函数称为多态。运行时的多态性。可见,虚函数可使C++支持运行时的多态性。

多态形成的条件:(协变例外)

1、虚函数的重写(virtual + 函数完全相同)
2、父类的指针或引用(指向父类调父类,指向子类调子类)
协变:函数返回值可以不相同,返回值是父子继承关系的指针或引用就可以。
多态:与类型无关,与对象有关

多态可以划分为:编译时多态和运行时多态。多态的实现和连编这一概念有关。
所谓连编就是把函数名与函数体的程序代码连接(联系)在一起的过程。静态连编就是在编译阶段完成的连编。动态联编是运行阶段完成的连编。

对虚函数定义的几点说明:

  1. 由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是子类从父类公有继承。因此,通过定义虚函数来使用多态性机制时,子类必须从它的父类公有继承。
  2. 必须首先在父类中定义虚函数。
  3. 在子类对父类中声明的虚函数重新定义时,关键字virtual不是必须的,因为虚函数属性可从父类中继承得到。虽然如此,但最好还是在子类中使用virtual,这样可以省却用户查看类定义以确定其是否为虚函数的麻烦。
  4. 一个虚函数无论被公有继承多少次,它仍然保持虚函数的特性。
  5. 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  6. 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做是非内联的。
  7. 构造函数不能是虚函数,但是最好把父类的析构函数声明为虚函数。因为父类的指针有可能指向父类对象,也有可能指向子类对象,当父类指针指向子类对象时,只会析构父类对象,不会对子类对象析构,会造成内存泄漏。若把析构函数声明为虚函数,就构成多态,只与对象有关,父类指向子类对象时,就会调子类的析构。
//最好把基类的析构函数定义为虚函数
class A
{
public:
     ~A()
     {
          cout << "~A()" << endl;
     }
};

class B : public A
{
public:
     ~B()
     {
          cout << "~B()" << endl;
     }
};

int main()
{
     A* p = new B;//父类的指针new出子类对象,
//这样会出现问题,没有调子类的析构函数,会造成内存泄漏
     delete p;  // p->destructor() + free(p) 
             //所以必须把基类的析构函数定义为虚函数

     return 0;
}

运行结果:
运行结果显示:只析构了父类对象,而没有析构子类对象,这样会造成内存泄漏。
把基类的析构函数定义为虚函数后:

class A
{
public:
     virtual ~A()
     {
          cout << "~A()" << endl;
     }
};

class B : public A
{
public:
     ~B()
     {
          cout << "~B()" << endl;
     }
};

int main()
{
     A* p = new B;  
     delete p; // p->destructor() + free(p) 
             //所以必须把基类的析构函数定义为虚函数

     return 0;
}

这里写图片描述
这样就会调子类的析构了。

纯虚函数

有时,基类往往表示一种抽象的概念,它并不与具体的事物相联系。这时在基类中将某一成员函数定义为虚函数,并不是基类本身要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。
纯虚函数是在声明虚函数时被“初始化”为0的函数。

class Person
{
public:
    virtual void Display() = 0; // 纯虚函数
protected:
    string _name; // 姓名
};

class Student : public Person
{
public:
    virtual void Display()//子类必须重写,子类才能实例化出对象
    {
        cout << "Student" << endl;
    }
};

int main()
{
    //Person p; //error: “Person”:不能实例化抽象类
    Student s;

    return 0;
}
  • 纯虚函数没有函数体,它后面的“=0”并不表示函数返回值为0,只是起形式上的作用,告诉编译系统“这是纯虚函数”。
  • 纯虚函数不具备函数的功能,不能被调用。只有在派生类中重新定义以后,派生类才能实例化出对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值