四. 多态性和虚函数

发布时间 2023-12-04 17:18:44作者: Beasts777

文章参考:

《C++面向对象程序设计》✍千处细节、万字总结(建议收藏)_白鳯的博客-CSDN博客

1. 多态性概述

所谓多态性,就是不同对象接收不同消息时,产生不同的动作。这样就可以用相同的接口访问不同的函数,从而实现一个接口,多种方法

从实现方式上看,多态性分为两种:

  • 编译时多态:
    • 在C++中,编译时多态与连编(把函数名和函数定义连接在一起)有关。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。
    • 优缺点:在程序编译时就知道调用函数的全部信息。因此,这种连编类型的函数调用速度快、效率高,但缺乏灵活性
    • 实现方式:主要通过函数重载运算符重载实现。
  • 运行时多态:
    • C++中通过动态连编实现。动态连编在程序运行时生效,即当程序调用到某一函数名时,才去寻找和连接其程序代码。
    • 优缺点:降低了程序的运行效率,但增强了程序的灵活性。
    • 实现方式:主要通过虚函数实现。

2. 虚函数

定义:

虚函数的声明是在基类中进行的,通过virtual关键字将基类的成员函数声明为虚函数。语法如下:

virtual 返回值 函数名 (形参表){
    函数体;
}

当基类中的某个成员函数被定义为虚函数时,就可以在派生类中对该虚函数进行重新定义,并使用基类调用派生类的实现。

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Person{
    private:
        string flower;
    public:
        Person(string flower = "鲜花"): flower(flower){}
        string getFlower(){
            return this->flower;
        }
        virtual void show(){
            cout<< "人类喜欢:"<< this->flower<< endl;
        }
    };
    
    class Man: public Person{
    public:
        Man(string flower = "油菜花"): Person(flower){}
        // 派生类中的virtual可加可不加
        virtual void show(){
            cout<< "男人喜欢:"<< this->getFlower()<< endl;
        }
    };
    
    class Woman: public Person{
    public:
        Woman(string flower = "西兰花"): Person(flower){}
        void show(){
            cout<< "女人喜欢:"<< this->getFlower()<< endl;
        }
    };
    
    int main(void){
        Person *p = nullptr;
        Person per;
        Man m;
        Woman w;
        per.show();
        p = &m;
        p->show();
        p = &w;
        p->show();
        return 0;
    }
    
  • 输出:

    人类喜欢:鲜花
    男人喜欢:油菜花
    女人喜欢: 西兰花
    

注意:

  • 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。如不写,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。
  • 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  • 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
  • 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。

3. 虚析构函数

问题:

假设现在有一个派生类B,其基类为A,我们建立一个A类型的指针p,再new一个B类型的对象,并令p指向B的地址。那么当使用delete 释放指针p所指向的内存时,系统只会执行基类的析构函数,不执行派生类的析构函数

这是因为当撤销指针p所指向的空间时,采用了静态连编的方式,只执行了基类A的析构函数,而不会执行基类B的析构函数。

解决方法:如果希望程序执行动态连编时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。格式为:

virtual ~类名(){
    函数内容;
}

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base{
    public:
    	virtual ~Base() {
    		cout << "调用基类Base的析构函数..." << endl;
    	}
    };
    
    class Derived: public Base{
    public:
    	~Derived() {
    		cout << "调用派生类Derived的析构函数..." << endl;
    	}
    };
    
    int main() {
    	Base *p;
    	p = new Derived;
    	delete p;
    	return 0;
    }
    
  • 输出:

    调用基类Base的析构函数...
    调用派生类Derived的析构函数...
    

注意:

  • 虽然基类和派生类的析构函数名不一致,但如果将基类的析构函数声明为析构函数,那么其派生类的析构函数也都会变成虚函数
  • 只有在上述情况下,才会出现只调用基类,而不调用派生类析构函数的情况。如果使用的栈空间,或者直接用派生类类型的指针,就不会出现这种问题。

4. 纯虚函数

纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

5. 抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类。对于抽象类的使用有以下几点规定:

  • 因为抽象类中包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类使用,不能家里抽象类对象。
  • 不允许从具体类(不包含纯虚函数的类)中派生出抽象类。
  • 抽象类不能用作函数的返回类型、参数类型或是显式转换的类型
  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
  • 如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

6. 实例

问题:

利用C++的多态性,计算三角形、矩形的面积。

代码:

#include <iostream>
#include <string>
using namespace std;

// 定义一个抽象类
class Figure{
// 使用protected,便于派生类直接访问
protected:
    double x;
    double y;
public:
    Figure(double m, double n): x(m), y(n){}
    // 使用需析构函数,避免调用析构函数时只调用基类的
    virtual ~Figure(){
        cout << "Figure is destroyed..."<< endl;
    }
    // 纯虚构函数
    virtual double get_area() = 0;
};

class Triangle: public Figure{
public:
    Triangle(double b, double h): Figure(b, h){}
    ~Triangle(){
        cout<< "Triangle is destroyed..."<< endl;
    }
    double get_area(){
        return x*y/2;
    }
};

class Square: public Figure{
public:
    Square(double b, double h): Figure(b, h){}
    ~Square(){
        cout<< "Square is destroyed..."<< endl;
    }
    double get_area(){
        return x*y;
    }
};

int main(void){
    Figure *p = nullptr;
    p = new Triangle(5, 10);
    cout<< p->get_area()<< endl;
    delete p;
    p = new Square(5, 10);
    cout<< p->get_area()<< endl;
    delete p;
    return 0;
}

输出:

25
Figure is destroyed...
Triangle is destroyed...
50
Figure is destroyed...
Square is destroyed...