[C++]九 多态、虚函数、函数覆盖、抽象类

文章介绍了C++中的多态性,包括虚函数用于函数覆盖,实现动态绑定;抽象类用于定义接口,要求至少有一个纯虚函数;虚析构函数确保正确销毁派生类对象。此外,还讨论了多态的实现条件和原理,以及如何通过抽象类支持多态。
摘要由CSDN通过智能技术生成


虚函数与函数覆盖

普通的成员函数使用virtual关键字修饰就是虚函数,虚函数的主要用途是函数覆盖,函数覆盖的主要用途是多态。
虚函数的使用需要注意:

  • 静态成员函数不能设置为虚函数
  • 构造函数不能设置为虚函数,但是析构函数可以设置为虚函数
  • 如果声明和定义分离,只需要在声明时使用virtual关键字修饰
  • 函数覆盖:虚函数具有传递性,当基类中某一个成员函数设置为虚函数之后,派生类中的同名函数(函数名称相同、参数列表完全相同、返回值类型相关)会自动变为虚函数。此时使用派生类对象调用此函数的效果类似于函数隐藏,但是相比于函数隐藏,函数覆盖支持:
    • 多态
    • 在C++11中可以使用override关键字验证是否覆盖成功
#include <iostream>

using namespace std;

class Animal
{
public:
    virtual void eat() // 虚函数
    {
        cout << "吃东西" << endl;
    }
};

class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
};


int main()
{
    Animal a;
    a.eat();

    Dog d;
    // 类似于函数隐藏
    d.Animal::eat();
    d.eat();

    return 0;
}

多态

首先从字面意思上讲,多态的意思是“多种状态”,可以认为是“一个接口,多种状态”。接口在运行期间,根据传入的参数来决定具体调用的函数,最终采取不同的执行策略。
多态与模板的区别在于,多态虽然也支持多种数据类型,但是不同类型的处理逻辑可能不同;模板对所有类型的处理方式是相同的。
使用多态的条件有三个:

  1. 要使用公有继承
  2. 要有函数覆盖
  3. 基类引用/指针指向派生类对象
#include <iostream>

using namespace std;

class Animal
{
public:
    virtual void eat() // 虚函数
    {
        cout << "吃东西" << endl;
    }
};

class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
};

class Cat:public Animal
{
public:
    void eat()
    {
        cout << "吃鱼" << endl;
    }
};

class Husky:public Dog
{
    void eat()
    {
        cout << "吃熊" << endl;
    }
};

/**
 * @brief 基类引用
 */
void test_dt1(Animal& a)
{
    a.eat();
}

/**
 * @brief 基类指针
 */
void test_dt2(Animal* a)
{
    a->eat();
}


int main()
{
    Animal a1;
    Dog d1;
    Cat c1;
    Husky h1;
    test_dt1(a1);
    test_dt1(d1);
    test_dt1(c1);
    test_dt1(h1);

    Animal* a2 = new Animal;
    Dog* d2 = new Dog;
    Cat* c2 = new Cat;
    Husky* h2 = new Husky;
    test_dt2(a2);
    test_dt2(d2);
    test_dt2(c2);
    test_dt2(h2);

    delete a2;
    delete d2;
    delete c2;
    delete h2;

    return 0;
}

多态原理

拥有虚函数的类,会拥有一份虚函数表,此类中所有的对象共享这一张表,这些对象内部会增加一个隐藏的成员变量:虚函数表指针,指向虚函数表。可以使用sizeof运算符观察此指针的存在
当派生类继承了拥有虚函数的基类后,也会继承虚函数表,如果此时派生类覆盖了基类中的虚函数,则会修改这样表的内容为新的函数内容
如果派生类中新增了新的虚函数,则会在表的尾部新增此函数
每个类型对象的虚函数表指针都指向自己类的虚函数表。因此代码在运行阶段,可以通过查询这个虚函数表,来找到对应的类型应该调用的函数地址。
多态与继承一样,提高了代码的编码效率,牺牲了一部分执行效率

虚析构函数

当基类指针指向派生类对象时,虽然对象还是派生类对象,但是在销毁的时候会按照基类的销毁方式只调用基类的析构函数,不会调用派生类的析构函数,此时可能会导致内存泄漏等未定义的非正常现象出现

#include <iostream>

using namespace std;

class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }

    ~Animal()
    {
        cout << "Animal 析构函数" << endl;
    }
};

class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }

    ~Dog()
    {
        cout << "Dog 析构函数" << endl;
    }
};


int main()
{

    Dog* d = new Dog;
    Animal* a = d; // 对象是Dog,但是类型被Animal限制了

    cout << d << " " << a << endl;

    delete a; // 按照Animal的方式析构,但是实际上是Dog对象

    return 0;
}

使用虚析构函数,就可以解决这个问题,虚析构函数可以加入到虚函数表中,在对象销毁时,查询虚函数表依次调用各个继承层次的析构函数

#include <iostream>

using namespace std;

class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }

    virtual ~Animal()
    {
        cout << "Animal 析构函数" << endl;
    }
};

class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }

    ~Dog()
    {
        cout << "Dog 析构函数" << endl;
    }
};


int main()
{

    Dog* d = new Dog;
    Animal* a = d; // 对象是Dog,但是类型被Animal限制了

    cout << d << " " << a << endl;

    delete a;

    return 0;
}

因为在设计一个类时,编译器自动生成的析构函数不是虚函数,此类应用多态时,可能造成内存泄漏的问题。建议把一个类的析构函数设置为虚函数,除非这个类确定不会有派生类

抽象类

概念

如果某个类只表达一些抽象的概念,并不与具体的对象相联系,但是这个类又必须为它的派生类提供一个公共的框架,此时就需要用到抽象类。
如果一个类是抽象类,那么必须有至少一个纯虚函数;
如果一个类有至少一个纯虚函数,那么这个类就是抽象类。
纯虚函数是一种特殊的虚函数,纯虚函数只有函数声明,没有定义

使用方式

方式一

派生类继承抽象类,实现所有的纯虚函数。此时派生类变为普通的类,可以创建对象正常使用

#include <iostream>

using namespace std;

/**
 * @brief 形状类
 */
class Shape
{
public:
    // 纯虚函数
    virtual void perimeter() = 0;
    virtual void area() = 0;

    virtual ~Shape(){} // 虚析构函数
};

class Circle:public Shape
{
public:
    void perimeter()
    {
        cout << "2πR" << endl;
    }

    void area()
    {
        cout << "πR2" << endl;
    }
};


int main()
{
//    Shape s; 错误

    Circle c;
    c.area();
    c.perimeter();

    return 0;
}

方式二

如果派生类B没有把抽象基类A的所有纯虚函数实现,那么这个派生类B也会变为抽象类,直到它的派生类C把剩余的纯虚函数实现,派生类C才可以创建对象

#include <iostream>

using namespace std;

/**
 * @brief 形状类
 */
class Shape
{
public:
    // 纯虚函数
    virtual void perimeter() = 0;
    virtual void area() = 0;

    virtual ~Shape(){} // 虚析构函数
};

/**
 * @brief 多边形
 */
class Polygon:public Shape
{
public:
    void perimeter()
    {
        cout << "∑边长" << endl;
    }
};

/**
 * @brief 矩形
 */
class Rectangle:public Polygon
{
public:
    void area()
    {
        cout << "w*h" << endl;
    }
};


int main()
{
//    Shape s; 错误
//    Polygon p; 错误
    Rectangle r;
    r.area();
    r.perimeter();

    return 0;
}

在实际开发中,如果一个类有抽象基类,通常表示绝大多数接口都在这个抽象基类中规定

多态——抽象类

需要注意的是,抽象类支持多态,所以虽然抽象类没有对象,但是其指针或引用在代码是可以以指针或引用的方式存在的

#include <iostream>

using namespace std;

/**
 * @brief 形状类
 */
class Shape
{
public:
    virtual void perimeter() = 0;
    virtual void area() = 0;

    virtual ~Shape(){}
};

class Polygon:public Shape
{
public:
    void perimeter()
    {
        cout << "∑边长" << endl;
    }
};

class Rectangle:public Polygon
{
public:
    void area()
    {
        cout << "w*h" << endl;
    }
};

void test_dt1(Shape& s)
{
    s.area();
    s.perimeter();
}

void test_dt2(Shape* s)
{
    s->area();
    s->perimeter();
}


int main()
{
    Rectangle r1;
    test_dt1(r1);

    Rectangle *r2 = new Rectangle;
    test_dt2(r2);

    delete r2;

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值