欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多态与虚函数

程序员文章站 2024-03-17 08:14:22
...

多态与虚函数

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

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,只是起形式上的作用,告诉编译系统“这是纯虚函数”。
  • 纯虚函数不具备函数的功能,不能被调用。只有在派生类中重新定义以后,派生类才能实例化出对象。
相关标签: 虚函数