C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 虚函数、重写、抽象类与虚析构

C++ 多态基础之虚函数、重写、抽象类与虚析构

作者:星恒随风

C++中多态性是一种允许以统一的方式使用不同类对象的能力,通过虚函数、重写(Override)和抽象类(Abstract Classes)实现,虚析构函数也是一个重要的概念,特别是在涉及继承和动态内存分配时,下面介绍C++ 多态基础之虚函数、重写、抽象类与虚析构,感兴趣的朋友一起看看吧

前言

继承解决了类之间的代码复用问题,但仅仅有继承,还不能让程序根据对象的实际类型自动执行不同的行为。

例如,普通人、学生和军人都可以执行“买票”这个行为,但具体规则不同:

普通人:全价买票
学生:优惠买票
军人:优先买票

如果程序通过大量 if-else 判断类型,代码很快就会变得臃肿:

if (type == "Person")
{
    cout << "全价买票" << endl;
}
else if (type == "Student")
{
    cout << "优惠买票" << endl;
}
else if (type == "Soldier")
{
    cout << "优先买票" << endl;
}

多态提供了一种更自然的方式:

调用者只面向统一的基类接口,具体执行哪个版本,由对象的实际类型决定。

本文主要讲解多态的基本使用,包括:

一、多态是什么?

1.1 多态的含义

多态的英文是:

polymorphism

可以简单理解为:

同一个接口,在面对不同类型的对象时,表现出不同的行为。

例如,所有动物都有“叫”这个行为:

animal.talk();

但是实际对象不同,输出也不同:

Dog:汪汪
Cat:喵

调用形式没有改变,行为却随着对象类型发生变化,这就是多态。

1.2 编译时多态

编译时多态也叫静态多态。

常见形式包括:

函数重载
运算符重载
函数模板
类模板

例如函数重载:

#include <iostream>
#include <string>
using namespace std;
void print(int value)
{
    cout << "int: " << value << '\n';
}
void print(const string& value)
{
    cout << "string: " << value << '\n';
}
int main()
{
    print(10);
    print("hello");
    return 0;
}

编译器根据参数类型,在编译阶段确定调用哪个函数:

print(10)      -> print(int)
print("hello") -> print(const string&)

因此,这种多态称为编译时多态。

1.3 运行时多态

运行时多态也叫动态多态。

它的特点是:

编译阶段只能看到基类接口,真正执行哪个函数,要在运行阶段根据对象的实际类型决定。

例如:

Animal& animal = dog;
animal.talk();

虽然 animal 的类型是 Animal&,但它实际引用的是 Dog 对象,因此会调用:

Dog::talk()

本文后面所说的“多态”,主要指这种基于虚函数实现的运行时多态。

二、一个最小多态示例

2.1 定义基类

#include <iostream>
using namespace std;
class Person
{
public:
    virtual void buyTicket() const
    {
        cout << "普通人买票:全价" << '\n';
    }
};

在成员函数前加上:

virtual

这个成员函数就成为虚函数。

2.2 定义派生类

class Student : public Person
{
public:
    void buyTicket() const override
    {
        cout << "学生买票:优惠" << '\n';
    }
};
class Soldier : public Person
{
public:
    void buyTicket() const override
    {
        cout << "军人买票:优先" << '\n';
    }
};

StudentSoldier 都重新实现了 buyTicket()

这种行为称为:

虚函数重写
virtual function overriding

2.3 使用基类引用统一调用

void enterStation(const Person& person)
{
    person.buyTicket();
}
int main()
{
    Person person;
    Student student;
    Soldier soldier;
    enterStation(person);
    enterStation(student);
    enterStation(soldier);
    return 0;
}

运行结果:

普通人买票:全价
学生买票:优惠
军人买票:优先

enterStation() 内部始终只有一行:

person.buyTicket();

但传入对象不同,最终行为不同,这就是运行时多态。

三、运行时多态的构成条件

C++ 中要形成常见的运行时多态,通常需要满足下面几个条件。

3.1 存在继承关系

class Student : public Person
{
};

通常使用 public 继承,因为它表达的是:

Student is a Person

也就是“学生是一种人”。

3.2 基类声明虚函数

class Person
{
public:
    virtual void buyTicket() const
    {
    }
};

只有非静态成员函数才能成为虚函数。

下面这些函数不能声明为虚函数:

普通非成员函数
static 成员函数
构造函数

析构函数可以声明为虚函数,而且多态基类通常需要虚析构函数。

3.3 派生类完成虚函数重写

class Student : public Person
{
public:
    void buyTicket() const override
    {
    }
};

派生类函数需要满足虚函数重写规则。

3.4 通过基类指针或引用调用

Student student;

Person* ptr = &student;
ptr->buyTicket();

或者:

Person& ref = student;
ref.buyTicket();

基类指针或引用既可以指向基类对象,也可以指向派生类对象:

Person person;
Student student;

Person* p1 = &person;
Person* p2 = &student;

因此,程序运行时才需要判断它实际指向哪种对象。

四、为什么按值传递不能实现预期多态?

4.1 错误示例

void enterStation(Person person)
{
    person.buyTicket();
}

调用:

Student student;
enterStation(student);

这里不会保留完整的 Student 对象。

参数传递时,会创建一个新的 Person 对象,只复制 Student 中的基类部分。

4.2 对象切片

这种现象称为对象切片:

Student 对象
├── Person 基类部分 ─────→ 新的 Person 对象
└── Student 独有部分      被舍弃

函数内部的 person 已经是独立的 Person 对象,所以不会再调用 Student 的版本。

4.3 正确写法

使用引用:

void enterStation(const Person& person)
{
    person.buyTicket();
}

也可以使用指针:

void enterStation(const Person* person)
{
    if (person != nullptr)
    {
        person->buyTicket();
    }
}

在多态体系中,通常通过指针或引用使用对象,避免把派生类对象按值复制成基类对象。

五、虚函数和虚函数重写

5.1 什么是虚函数?

在非静态成员函数前加上 virtual

class Animal
{
public:
    virtual void talk() const
    {
        cout << "Animal::talk()" << '\n';
    }
};

talk() 就是虚函数。

5.2 派生类可以不写 virtual 吗?

可以。

只要基类函数已经是虚函数,派生类中的重写函数仍然具有虚函数属性:

class Dog : public Animal
{
public:
    void talk() const
    {
        cout << "汪汪" << '\n';
    }
};

这段代码可以形成重写。

不过,更推荐写成:

class Dog : public Animal
{
public:
    void talk() const override
    {
        cout << "汪汪" << '\n';
    }
};

override 可以让编译器检查它是否真的重写了基类虚函数。

六、虚函数重写规则

6.1 基本要求

派生类函数重写基类虚函数时,通常需要满足:

函数名对应
参数列表对应
const 和 volatile 限定对应
引用限定符对应
返回类型相同,或者满足协变规则

正确示例:

class Base
{
public:
    virtual void func(int value) const
    {
    }
};
class Derived : public Base
{
public:
    void func(int value) const override
    {
    }
};

6.2 const 不同不能构成重写

class Base
{
public:
    virtual void func() const
    {
    }
};
class Derived : public Base
{
public:
    void func() override
    {
    }
};

这段代码会编译失败。

因为:

Base::func() const

和:

Derived::func()

是两个不同的成员函数。

6.3 参数不同不能构成重写

class Base
{
public:
    virtual void func(int value)
    {
    }
};
class Derived : public Base
{
public:
    void func(double value)
    {
    }
};

参数类型不同,因此不能构成重写。

Derived::func(double) 会隐藏基类中的同名函数。

6.4 返回值通常必须相同

class Base
{
public:
    virtual int func()
    {
        return 1;
    }
};
class Derived : public Base
{
public:
    double func()
    {
        return 2.0;
    }
};

这不是合法的虚函数重写。

不过,如果返回的是具有继承关系的指针或引用,可以使用协变返回类型。

七、override:让编译器检查重写

7.1 没有 override 时容易漏掉错误

class Car
{
public:
    virtual void drive() const
    {
    }
};
class Benz : public Car
{
public:
    void drive()
    {
        cout << "Benz:舒适" << '\n';
    }
};

Benz::drive() 少了一个 const,所以它没有重写基类函数。

如果不写 override,这种错误可能不会立即暴露。

7.2 使用 override

class Benz : public Car
{
public:
    void drive() override
    {
        cout << "Benz:舒适" << '\n';
    }
};

编译器会报错,提示这个函数没有重写任何基类虚函数。

正确写法:

class Benz : public Car
{
public:
    void drive() const override
    {
        cout << "Benz:舒适" << '\n';
    }
};

7.3 推荐写法

下面两种写法都合法:

virtual void drive() const override;
void drive() const override;

现代 C++ 中通常使用更简洁的写法:

void drive() const override;

八、final:禁止继续重写或继承

8.1 禁止继续重写虚函数

class Car
{
public:
    virtual void drive() const
    {
    }
};
class Benz : public Car
{
public:
    void drive() const final
    {
        cout << "Benz:舒适" << '\n';
    }
};

后续派生类不能再重写 drive()

class SmartBenz : public Benz
{
public:
    void drive() const override
    {
    }
};

这会编译失败。

8.2 禁止类继续被继承

class Benz final : public Car
{
};

下面的代码会编译失败:

class SmartBenz : public Benz
{
};

8.3 final 的使用场景

final 适合用于:

类的语义不允许继续派生
虚函数行为已经确定
框架需要关闭某个扩展点

不要只是为了“看起来更严格”,就随意给所有类添加 final

九、默认参数和虚函数的陷阱

看下面的代码:

#include <iostream>
using namespace std;
class A
{
public:
    virtual void func(int value = 1)
    {
        cout << "A -> " << value << '\n';
    }
    virtual void test()
    {
        func();
    }
};
class B : public A
{
public:
    void func(int value = 0) override
    {
        cout << "B -> " << value << '\n';
    }
};
int main()
{
    B object;
    object.test();
    return 0;
}

输出结果是:

B -> 1

9.1 函数版本是动态绑定的

object 的实际类型是 B,所以 func() 最终调用:

B::func()

9.2 默认参数是静态绑定的

func() 这条调用语句写在:

A::test()

中。

因此编译器使用 A::func() 中声明的默认值:

int value = 1

最终结果是:

调用 B::func
使用 A 中的默认参数 1

也就是:

B -> 1

9.3 实际建议

不要在虚函数的不同重写版本中设置不同的默认参数。

可以把默认调用单独封装成非虚函数:

class A
{
public:
    void func()
    {
        func(1);
    }

    virtual void func(int value)
    {
    }
};

十、协变返回类型

一般情况下,基类和派生类重写函数的返回类型需要相同。

但 C++ 允许一种特殊情况:协变返回类型。

10.1 指针协变

class Animal
{
public:
    virtual Animal* clone() const
    {
        return new Animal(*this);
    }
    virtual ~Animal() = default;
};
class Dog : public Animal
{
public:
    Dog* clone() const override
    {
        return new Dog(*this);
    }
};

基类返回:

Animal*

派生类返回:

Dog*

由于 Dog 继承自 Animal,所以这是合法的协变返回。

10.2 引用协变

class Base
{
public:
    virtual Base& self()
    {
        return *this;
    }
};
class Derived : public Base
{
public:
    Derived& self() override
    {
        return *this;
    }
};

协变只适用于类类型的指针或引用,不适用于普通值类型。

十一、重载、重写和隐藏

这三个概念名字相似,但含义不同。

11.1 重载

重载发生在同一作用域中:

class Calculator
{
public:
    void calculate(int value)
    {
    }

    void calculate(double value)
    {
    }
};

特点:

同一作用域
函数名相同
参数列表不同
是否为虚函数没有要求

11.2 重写

重写发生在继承体系中:

class Base
{
public:
    virtual void func() const
    {
    }
};
class Derived : public Base
{
public:
    void func() const override
    {
    }
};

特点:

基类和派生类
基类函数是虚函数
函数声明满足重写规则
用于运行时多态

11.3 隐藏

派生类出现同名成员时,会隐藏基类同名成员:

class Base
{
public:
    void func()
    {
    }
};
class Derived : public Base
{
public:
    void func(int value)
    {
    }
};

调用:

Derived object;
object.func(10);
// object.func();  // 编译失败

可以显式调用基类函数:

object.Base::func();

或者使用:

class Derived : public Base
{
public:
    using Base::func;

    void func(int value)
    {
    }
};

11.4 对比表

概念作用域函数名参数列表是否要求虚函数
重载同一作用域相同不同不要求
重写基类和派生类对应对应基类必须是虚函数
隐藏基类和派生类相同可以相同或不同不要求

十二、纯虚函数和抽象类

12.1 纯虚函数

在虚函数声明后写上:

= 0

就得到纯虚函数:

class Shape
{
public:
    virtual double area() const = 0;
};

纯虚函数表示:

基类只规定接口,但不提供适合所有派生类的默认实现。

12.2 抽象类

包含纯虚函数的类称为抽象类。

下面的代码无法通过编译:

Shape shape;

抽象类不能直接实例化对象。

但可以定义抽象类的指针和引用:

Shape* ptr = nullptr;
Shape& getShape();

12.3 派生类需要完成实现

class Circle : public Shape
{
public:
    explicit Circle(double radius)
        : _radius(radius)
    {
    }
    double area() const override
    {
        return 3.1415926 * _radius * _radius;
    }
private:
    double _radius;
};

Circle 实现了 area(),因此可以创建对象。

如果没有重写纯虚函数:

class Circle : public Shape
{
};

那么 Circle 仍然是抽象类。

12.4 抽象类可以拥有普通成员

抽象类仍然可以包含:

普通成员变量
普通成员函数
构造函数
析构函数
已经实现的虚函数
静态成员

例如:

#include <string>
#include <utility>
using namespace std;
class Shape
{
public:
    explicit Shape(string name)
        : _name(std::move(name))
    {
    }
    virtual double area() const = 0;
    const string& name() const
    {
        return _name;
    }
    virtual ~Shape() = default;
private:
    string _name;
};

抽象类不能直接创建对象,不代表它不能提供公共实现。

十三、纯虚析构函数

析构函数也可以声明成纯虚函数:

class Base
{
public:
    virtual ~Base() = 0;
};

这样 Base 会成为抽象类。

不过,纯虚析构函数仍然必须提供定义:

Base::~Base() = default;

完整写法:

class Base
{
public:
    virtual ~Base() = 0;
};

Base::~Base() = default;

原因是销毁派生类对象时,执行完派生类析构函数后,仍然需要调用基类析构函数。

十四、多态基类为什么需要虚析构函数?

14.1 问题示例

class Base
{
public:
    ~Base()
    {
        cout << "~Base()" << '\n';
    }
};
class Derived : public Base
{
public:
    Derived()
        : _data(new int[100])
    {
    }
    ~Derived()
    {
        cout << "~Derived()" << '\n';
        delete[] _data;
    }
private:
    int* _data;
};

使用基类指针指向派生类对象:

Base* ptr = new Derived;
delete ptr;

如果基类析构函数不是虚函数,通过基类指针删除派生对象会产生未定义行为。

不能依赖“某个编译器上只调用了基类析构函数”这样的现象,因为未定义行为没有可靠结果。

14.2 正确写法

class Base
{
public:
    virtual ~Base() = default;
};

此时:

Base* ptr = new Derived;
delete ptr;

析构顺序是:

Derived::~Derived()
Base::~Base()

14.3 两种常见设计

允许通过基类指针删除:

class Base
{
public:
    virtual ~Base() = default;
};

禁止通过基类指针删除:

class Base
{
protected:
    ~Base() = default;
};

可以简单记成:

允许多态删除:
public virtual 析构
禁止多态删除:
protected non-virtual 析构

十五、一个完整的多态示例

15.1 定义抽象基类

#include <iostream>
#include <string>
#include <utility>
using namespace std;
class Shape
{
public:
    explicit Shape(string name)
        : _name(std::move(name))
    {
    }
    virtual double area() const = 0;
    virtual void draw() const = 0;
    const string& name() const
    {
        return _name;
    }
    virtual ~Shape() = default;
private:
    string _name;
};

15.2 定义圆形

class Circle : public Shape
{
public:
    explicit Circle(double radius)
        : Shape("圆形")
        , _radius(radius)
    {
    }
    double area() const override
    {
        return 3.1415926 * _radius * _radius;
    }
    void draw() const override
    {
        cout << "绘制半径为 " << _radius << " 的圆" << '\n';
    }
private:
    double _radius;
};

15.3 定义矩形

class Rectangle : public Shape
{
public:
    Rectangle(double width, double height)
        : Shape("矩形")
        , _width(width)
        , _height(height)
    {
    }
    double area() const override
    {
        return _width * _height;
    }
    void draw() const override
    {
        cout << "绘制一个 "
             << _width << " × " << _height
             << " 的矩形" << '\n';
    }
private:
    double _width;
    double _height;
};

15.4 使用基类引用调用

void showShape(const Shape& shape)
{
    cout << "名称:" << shape.name() << '\n';
    cout << "面积:" << shape.area() << '\n';
    shape.draw();
    cout << '\n';
}
int main()
{
    Circle circle(3.0);
    Rectangle rectangle(4.0, 5.0);
    showShape(circle);
    showShape(rectangle);
    return 0;
}

调用者只依赖:

const Shape&

增加新的图形类型时,showShape() 通常不需要修改。

十六、常见问题

16.1 派生类重写时可以不写 virtual 吗?

可以。

但建议写 override,让编译器检查。

16.2 构造函数可以是虚函数吗?

不可以。

构造函数的任务是创建对象,此时还不存在一个完整对象供动态分派使用。

16.3 析构函数可以是虚函数吗?

可以。

多态基类通常需要虚析构函数。

16.4 static 成员函数可以是虚函数吗?

不可以。

静态成员函数没有 this 指针,也不属于某个具体对象。

16.5 抽象类可以定义指针吗?

可以:

Shape* ptr = nullptr;

但不能直接创建抽象类对象。

16.6 虚函数默认参数有多态性吗?

没有。

虚函数体动态绑定,默认参数静态绑定。

16.7 private 函数可以重写 public 虚函数吗?

可以。

重写规则和访问权限是两套不同的规则。

但是派生类对象无法直接调用自己的 private 函数。

总结

运行时多态的核心可以概括为:

基类提供统一接口
派生类重写具体行为
调用者使用基类指针或引用
运行时根据实际对象选择函数

多态真正的价值在于:新增派生类时,原有调用代码通常不需要增加大量类型判断。

调用者只需要依赖稳定的基类接口,每个派生类负责实现自己的行为。

到此这篇关于C++ 多态基础之虚函数、重写、抽象类与虚析构的文章就介绍到这了,更多相关C++ 虚函数、重写、抽象类与虚析构内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文