【C++ 程序设计】第 6 章:多态与虚函数

目录

一、多态的基本概念

(1)多态

(2)虚函数

(3)通过基类指针实现多态

(4)通过基类引用实现多态

(5)* 多态的实现原理

二、多态实例

三、多态的使用

四、虚析构函数

五、纯虚函数和抽象类

(1)纯虚函数 

(2)抽象类 

(3)虚基类




一、多态的基本概念

  • 多态分为编译时多态和运行时多态
  • 编译时多态主要是指函数的重载(包括运算符的重载):对重载函数的调用,在编译时就可以根据实参确定应该调用哪个函数,因此称为编译时多态。编译阶段的多态称为静态多态
  • 运行时多态则和继承、虚函数等概念有关:本章中提及的多态主要是指运行时多态。运行阶段的多态称为动态多态

(1)多态

程序编译阶段都早于程序运行阶段,所以静态绑定称为早绑定,动态绑定称为晚绑定。

静态多态和动态多态的区别,只在于在什么时候将函数实现和函数调用关联起来,是在编译阶段还是在运行阶段,即函数地址是早绑定的还是晚绑定的。

在类之间满足赋值兼容的前提下,实现动态绑定必须满足以下两个条件:

  1. 必须声明虚函数。
  2. 通过基类类型的引用或者指针调用虚函数。

(2)虚函数

⚫ 所谓“虚函数” ,就是在函数声明时前面加了 virtual 关键字的成员函数。

  • virtual关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。
  • 静态成员函数不能是虚函数。
  • 包含虚函数的类称为“多态类” 。
  • 声明虚函数成员的一般格式如下:
virtual 函数返回值类型 函数名(形参表);
在类的定义中使用 virtual 关键字来限定的成员函数即成为虚函数。
  • 再次强调一下,虚函数的声明只能出现在类定义中的函数原型声明时,不能在类外成员函数实现的时候
派生类可以继承基类的同名函数,并且可以在派生类中重写这个函数。
  • 如果不使用虚函数,当使用派生类对象调用这个函数,且派生类中重写了这个函数时,则调用派生类中的同名函数, 即 “隐藏” 了基类中的函数。
当然,如果还想调用基类的函数,只需在调用函数时,在前面加上 基类名及作用域限定符 即可。
关于虚函数,有以下几点需要注意:
  1. 虽然将虚函数声明为内联函数不会引起错误,但因为内联函数是在编译阶段进行静态处理的,而对虚函数的调用是动态绑定的,所以虚函数一般不声明为内联函数
  2. 派生类重写基类的虚函数实现多态,要求函数名、参数列表及返回值类型要完全相同。
  3. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
  4. 只有类的非静态成员函数才能定义为虚函数,静态成员函数和友元函数不能定义为虚函数
  5. 如果虚函数的定义是在类体外,则只需在声明函数时添加virtual关键字,定义时不加virtual关键字。
  6. 构造函数不能定义为虚函数。最好也不要将operator=定义为虚函数,因为使用时容易混淆。
  7. 不要在构造函数和析构函数中调用虚函数。在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
  8. 最好将基类的析构函数声明为虚函数

(3)通过基类指针实现多态

  • 声明虚函数后,派生类对象的地址可以赋值给基类指针,也就是基类指针可以指向派生类对象。
  • 对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时系统并不确定要执行的是基类还是派生类的虚函数;
  • 而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则调用基类的虚函数;
  • 如果基类指针指向的是一个派生类对象,则调用派生类的虚函数。

【示例】多态

【示例代码】演示了多态的特性,使用了基类和派生类的指针,在运行时调用了适当的虚函数:

#include <iostream>
using namespace std;

class A {
public:
    virtual void Print() // 虚函数
    {
        cout << "A::Print" << endl;
    }
};

class B : public A { // 公有继承
public:
    virtual void Print() // 虚函数
    {
        cout << "B::Print" << endl;
    }
};

class D : public A { // 公有继承
public:
    virtual void Print() // 虚函数
    {
        cout << "D::Print" << endl;
    }
};

class E : public B { // 公有继承
public:
    virtual void Print() // 虚函数
    {
        cout << "E::Print" << endl;
    }
};

int main()
{
    A a;
    B b;
    D d;
    E e;
    A* pa = &a;   // 基类pa指向基类对象a
    B* pb = &b;   // 派生类指针pa指向派生类对象b
    pa->Print();  // 多态,目前指向基类对象,调用a.Print(),输出A::Print
    pa = pb;      // 派生类指针赋给基类指针,pa指向派生类对象b
    pa->Print();  // 多态,目前指向派生对象,调用b.Print () ,输出B::Print
    pa = &d;      // 基类指针pa指向派生类对象d
    pa->Print();  // 多态,目前指向派生类对象,调用d.Print () ,输出D::Print
    pa = &e;      // 基类指针pa指向派生对象e
    pa->Print();  // 多态,目前指向派生类对象,调用e.Print () ,输出E::Print
    return 0;
}

【代码详解】

  • 多态是 C++ 的面向对象编程中的重要特性,它是指编译器在编译期不确定调用哪个函数,在运行期根据对象的实际类型确定使用哪个函数。
  • 在这段代码中,有基类 A 和它的两个派生类 B 和 D,以及派生类 B 的一个派生类 E。每一个类都有一个虚函数 Print()
  • 在 main()函数中,创建了四个对象 abde
  • 还声明了基类指针 pa 和派生类指针 pb。分别用指针 ​​​​​​​pa 指向每一个对象,然后调用虚函数 ​​​​​​​pa->Print() 。因为 ​​​​​​​Print() 是虚函数,所以在调用时会根据指针所指向的对象类型,调用相应的虚函数。这就实现了多态。在输出中,分别显示了针对不同对象打印的不同输出。
  • 值得注意的是,在派生类中实现虚函数时,需要保证函数签名(参数列表、返回类型、函数名)与基类中定义的虚函数完全相同。此外,在派生类中可以使用 ​​​​​​​override 关键字来标记虚函数的重载,这样可以让编译器检查是不是真的重载了虚函数。

【执行结果】

  • 这是由于程序中使用了多态,调用了虚函数 Print()
  • 在 C++ 中,虚函数通过指向派生类对象的基类指针来调用,实现了多态。
  • 当我们调用 ​​​​​​​pa->Print() 时,根据当前指向的实际对象的类型,调用相应的虚函数。
  • 在这个程序中,指针 ​​​​​​​pa 首先指向类 ​​​​​​​A 的对象 ​​​​​​​a ,因此调用 ​​​​​​​Print() 时输出 "A::Print"。
  • 接着,将指针 ​​​​​​​pa 指向类 ​​​​​​​B 的对象 ​​​​​​​b,调用 ​​​​​​​Print() 输出 "B::Print"。
  • 再将指针 ​​​​​​​pa 指向类 ​​​​​​​D 的对象 ​​​​​​​d,调用 ​​​​​​​Print() 时输出 "D::Print"。
  • 最后,将指针 ​​​​​​​pa 指向类E的对象 ​​​​​​​e,调用 ​​​​​​​Print() 时输出 "E::Print"。
  • 这些都是多态的表现,即同样的函数调用,根据不同的对象类型,表现出不同的行为。
A::Print
B::Print
D::Print
E::Print

(4)通过基类引用实现多态

  • 通过基类指针调用虚函数时可以实现多态,通过基类的引用调用虚函数的语句也是多态的。
  • 即通过基类的引用调用基类和派生类中同名、同参数表的虚函数时,若其引用的是一个基类的对象,则调用的是基类的虚函数;
  • 若其引用的是一个派生类的对象,则调用的是派生类的虚函数。

【示例】基类的引用和多态的特性

【示例代码】C++ 程序用到了基类的引用和多态的特性:

#include<iostream>
using namespace std;

class A // 基类
{
public:
    virtual void Print() // 虚函数
    {
        cout << "A::Print" << endl;
    }
};

class B : public A // 公有派生
{
public:
    virtual void Print() // 虚函数
    {
        cout << "B::Print" << endl;
    }
};

void PrintInfo(A& r) // 参数为基类引用
{
    r.Print(); // 多态,使用基类引用调用哪个Print()取决于r引用了哪个类的对象
}

int main()
{
    A a;
    B b;
    PrintInfo(a); // 使用基类对象,调用基类中的函数,输出A::Print
    PrintInfo(b); // 使用基类对象,调用派生类中的函数,输出B::Print
    return 0;
}

【代码详解】

  • 定义了一个基类 A 和一个公有派生类 ​​​​​​​B 。

  • 其中,类 ​​​​​​​A 中有一个虚函数 Print()

  • 定义了一个名为 ​​​​​​​PrintInfo() 的函数,它的参数是基类 ​​​​​​​A 的引用。

  • 在 ​​​​​​​main() 函数中,分别创建了一个 ​​​​​​​A 类型的对象 ​​​​​​​a 和一个 ​​​​​​​B 类型的对象 ​​​​​​​b 。

  • 然后,分别使用 ​​​​​​​PrintInfo() 函数来调用这两个对象的 ​​​​​​​Print() 方法。

  • 由于 ​​​​​​​Print() 方法是虚函数,所以实际上调用的是对象的实际类型所对应的虚函数。

  • 因此,在 ​​​​​​​PrintInfo(a) 中,虽然参数是基类的引用,但由于该引用实际指向的是 ​​​​​​​A 类的对象 ​​​​​​​a ,所以输出 "A::Print" 。

  • 而在 ​​​​​​​PrintInfo(b) 中,参数是基类的引用,但由于该引用指向 ​​​​​​​B 类的对象 ​​​​​​​b,所以输出 "B::Print"。

  • 这就展示了基类引用和多态的特性,这也是 C++ 面向对象编程中的一个重要概念。

【执行结果】

  • 在程序中,定义了基类 ​​​​​​​​​​​​​​A ​​​​​​​和派生类 ​​​​​​​​​​​​​​B,其中 ​​​​​​​​​​​​​​A ​​​​​​​中有一个虚函数 ​​​​​​​​​​​​​​Print()B ​​​​​​​重载了 ​​​​​​​​​​​​​​Print() ​​​​​​​虚函数。
  • 接着定义了一个函数 ​​​​​​​​​​​​​​PrintInfo(),该函数的参数为基类 ​​​​​​​​​​​​​​A ​​​​​​​的引用。
  • 在 ​​​​​​​​​​​​​​main() ​​​​​​​函数中,创建了一个基类对象 ​​​​​​​​​​​​​​a ​​​​​​​和一个派生类对象 ​​​​​​​​​​​​​​b ​​​​​​​,并调用函数 ​​​​​​​PrintInfo(),分别将这两个对象作为参数传入。
  • 由于函数 ​​​​​​​​​​​​​​PrintInfo() ​​​​​​​的参数是基类A的引用,并且 ​​​​​​​​​​​​​​Print() ​​​​​​​函数是虚函数,因此会根据参数对象的实际类型,分别调用基类 ​​​​​​​​​​​​​​A ​​​​​​​和派生类 ​​​​​​​​​​​​​​B ​​​​​​​中的 ​​​​​​​​​​​​​​Print() ​​​​​​​函数,输出 ​​​​​​​"A::Print" ​​​​​​​和 ​​​​​​​"B::Print"。
  • 这就展示了多态的特性,是 ​​​​​​​C++ ​​​​​​​面向对象编程的一个重要特性。
A::Print
B::Print

(5)* 多态的实现原理

  • 多态的关键在于通过基类指针或引用调用一个虚函数时,编译阶段不能确定到底调用的是基类还是派生类的函数,运行时才能确定。
  • 派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小。


二、多态实例

  • 定义一个基类 CShape 表示一般图形,然后派生 3 个子类分别表示矩形类、圆形类和三角形类。
  • 基类中定义了计算图形面积和输出信息的虚函数,3 个派生类中均继承了这两个虚函数,可以针对具体的图形计算各自的面积并输出结果。

【示例】类的继承、多态的实现

【示例代码】继承和多态的示例,分别使用指针和引用的 display 函数:

#include <iostream>
using namespace std;

const double PI = 3.14159; // 圆周率

class Point
{
private:
    double x, y; // 坐标点
public:
    Point(double i, double j) { x = i; y = j; } // 构造函数
    virtual double area() { return 0; } // 计算面积,虚函数
};

class Circle : public Point // 圆类继承自点类
{
private:
    double radius; // 半径
public:
    Circle(double a, double b, double r) : Point(a, b) { radius = r; } // 构造函数
    double area() { return PI * radius * radius; } // 计算面积,虚函数
};

void display(Point* p) { cout << p->area() << endl; } // 显示面积,通过指针调用虚函数
void display(Point& a) { cout << a.area() << endl; } // 显示面积,通过引用调用虚函数

int main()
{
    Point a(1.5, 6.7); // 定义点对象
    Circle c(1.5, 6.7, 2.5); // 定义圆对象
    Point* p = &c; // 派生类对象的地址赋给基类指针
    Point& rc = c; // 派生类对象初始化基类引用
    display(a); // 基类对象调用基类虚函数area,输出0
    display(p); // 指针调用派生类虚函数area,输出19.6349
    display(rc); // 引用调用派生类虚函数area,输出19.6349
    return 0;
}

【代码详解】

  • 该段代码是一个简单的继承和多态的示例,它定义了一个基类 Point 和它的一个派生类Circle。

  • Point 类具有两个私有成员变量 x 和 y,成员函数 Point(double i, double j) 表示该类的构造函数,虚函数 double area() 用于计算面积,返回值为0。

  • Circle 类继承于 Point 类,有一个私有成员变量 radius ,表示圆的半径。

  • 成员函数 Circle(double a, double b, double r) 是 Circle 类的构造函数,调用 Point 类的构造函数初始化基类的 x 和 y 。

  • 重载函数 double area() 用于计算面积,返回值为圆的面积。

  • 在主函数中定义了一个 Point 对象 a 和一个派生自 Point 对象的 Circle 对象 c,并用两种方式分别定义了一个基类指针 p 和基类引用 rc 。p 通过将派生类对象 c 的地址赋给基类指针,实现将一个指向基类的指针指向一个派生类的对象的功能,即将 Circle 对象命名为 Point 对象使用,这是多态性中的一种体现。

  • 显示函数有两个重载版本,参数分别是 Point 的指针和引用,均使用虚函数指针调用对象的虚函数 area() 。

  • 通过调用显示函数,输出三个对象(Point 对象、派生后的 Point 对象指针和引用)的面积,从输出结果可以看出,基类指针和基类引用调用了派生类的虚函数,实现了多态性。

  • 程序结束时,返回值 0 结束程序。

【执行结果】

  1. 调用display(a),输出0。此时p指针和rc引用指向不同的对象,p指向的是c的派生对象,而rc指向了同一个指针。因此,通过传入的参数为a,直接调用Point类的area()函数,输出0。

  2. 调用display§,输出19.6349。此时p指向的是Circle类的对象,而Circle类重写了Point类中的area()函数,因此调用时执行的是Circle类中的area()函数,输出的是圆的面积计算值。

  3. 调用display(rc),输出19.6349。与display§相同,rc引用指向的是Circle类中的对象,因此输出圆的面积计算值。

  4. 因此,通过指针和引用调用虚函数,实现了多态的效果:

0
19.6349
19.6349


三、多态的使用

在普通成员函数( 静态成员函数、构造函数和析构函数除外 )中调用其他虚成员函数也是允许的,并且是多态的。
  • 在构造函数中调用的,编译系统可以据此决定调用哪个类中的版本,所以它不是多态的;
  • 在析构函数中调用的,所以也不是多态的;
  • 实现多态时,必须满足的条件是:使用基类指针或引用来调用基类中声明的虚函数。
  • 派生类中继承自基类的虚函数,可以写virtual关键字,也可以省略这个关键字,这不影响派生类中的函数也是虚函数。 

【示例一】类的继承和多态

【示例代码】演示了 C++ 中类的继承和多态C++中 实现多态的主要方式是通过虚函数和基类的指针或者引用来实现。需要在基类中声明虚函数,在派生类中重写虚函数。在运行时,程序会根据实际对象类型来动态绑定调用,实现多态的效果:

#include <iostream>

using namespace std;

class CBase // 基类
{
public:
    void func1() // 不是虚函数
    {
        cout << "CBase::func1()" << endl;
        func2(); // 在成员函数中调用虚函数
        func3();
    }
    virtual void func2() // 虚函数
    {
        cout << "CBase::func2()" << endl;
    }
    void func3() // 不是虚函数
    {
        cout << "CBase::func3()" << endl;
    }
};

class CDerived: public CBase // 派生类
{
public:
    virtual void func2() override // 虚函数
    {
        cout << "CDerived:func2()" << endl;
    }
    void func3()
    {
        cout << "CDerived:func3()" << endl;
    }
};

int main()
{
    CDerived d;
    d.func1(); // 调用的基类中的成员函数
    return 0;
}

【代码详解】

  1. 定义基类:先定义了一个基类 CBase,其中定义了三个成员函数 func1()func2() 和func3()。其中 ​​​​​​​func1() 函数是一个非虚函数,在其中调用了基类的虚函数 ​​​​​​​func2() 和非虚函数 ​​​​​​​func3()func2() 函数是一个虚函数,输出了 "CBase::func2()"。
    class CBase // 基类
    {
    public:
        void func1() // 不是虚函数
        {
            cout << "CBase::func1()" << endl;
            func2(); // 在成员函数中调用虚函数
            func3();
        }
        virtual void func2() // 虚函数
        {
            cout << "CBase::func2()" << endl;
        }
        void func3() // 不是虚函数
        {
            cout << "CBase::func3()" << endl;
        }
    };
  2. 定义派生类:在 ​​​​​​​CBase 基础上定义了一个派生类 ​​​​​​​CDerived,其中重写了基类的虚函数func2(),输出了 "CDerived:func2()"。
    class CDerived: public CBase // 派生类
    {
    public:
        virtual void func2() override // 虚函数
        {
            cout << "CDerived:func2()" << endl;
        }
        void func3()
        {
            cout << "CDerived:func3()" << endl;
        }
    };
  3. 调用成员函数:在 ​​​​​​​​​​​​​​main() ​​​​​​​函数中,通过派生类 ​​​​​​​​​​​​​​CDerived ​​​​​​​的实例化对象 ​​​​​​​​​​​​​​d ​​​​​​​,调用d的成员函数 ​​​​​​​func1(),输出了基类和派生类中的成员函数。
    int main()
    {
        CDerived d;
        d.func1(); // 调用的基类中的成员函数
        return 0;
    }

【执行结果】

  • 在程序中,首先创建了一个派生类 CDerived 的实例化对象 ​​​​​​​d,然后调用了 ​​​​​​​d.func1() 进行函数执行。
  • 在执行过程中,先输出了 CBase::func1()
  • 在 ​​​​​​​func1() 调用了虚函数 ​​​​​​​func2(),由于 func2()是虚函数,因此程序会自动选择调用派生类中的实现,即输出 CDerived:func2()
  • 接着,func1() 又调用了非虚函数 ​​​​​​​func3(),输出 CBase::func3()
  • 所以最终的结果是输出了 ​​​​​​​CBase::func1()CDerived:func2() 和 CBase::func3()
CBase::func1()
CDerived:func2()
CBase::func3()

【示例二】虚函数、继承和多态

【示例代码】主要介绍了虚函数、继承和多态,主要演示了三个类之间的继承关系和多态,并演示了不同派生类之间的函数执行顺序和函数调用方式:

#include <iostream>
using namespace std;

class A // 基类
{
public:
    virtual void hello() // 虚函数
    {
        cout << "A::hello" << endl;
    } 
    virtual void bye() // 虚函数
    {
        cout << "A::bye" << endl;
    } 
};

class B: public A // 派生类
{
public:
    virtual void hello() // 虚函数
    { 
        cout << "B::hello" << endl; 
    }
    B()
    {
        hello(); // 调用虚函数,但不是多态
    }
    ~B()
    {
        bye(); // 调用虚函数,但不是多态
    }
};

class C: public B // 派生类
{
public:
    virtual void hello() // 虚函数
    {
        cout << "C::hello" << endl;
    }
};

int main()
{
    C obj;
    return 0;
}

【代码详解】

  1. 定义基类:定义了一个基类 A,其中定义了两个虚函数 hello 和 bye。虚函数是声明为 virtual 关键字且加上花括号{}的函数,派生类可以根据需要覆盖它们。
    class A // 基类
    {
    public:
        virtual void hello() // 虚函数
        {
            cout << "A::hello" << endl;
        } 
        virtual void bye() // 虚函数
        {
            cout << "A::bye" << endl;
        } 
    };
  2. 定义第一个派生类:在基类 A 的基础上,定义了一个派生类 B,其中重写了虚函数 hello,并输出了 B::hello。在 B 的构造函数中调用了 hello 函数,但由于此时对象还未被完全构造,因此无法使用多态。在 B 的析构函数中调用了 bye 函数,在析构函数中使用虚函数没有多态效果。
    class B: public A // 派生类
    {
    public:
        virtual void hello() // 虚函数
        { 
            cout << "B::hello" << endl; 
        }
        B()
        {
            hello(); // 调用虚函数,但不是多态
        }
        ~B()
        {
            bye(); // 调用虚函数,但不是多态
        }
    };
  3. 定义第二个派生类:在派生类 B 的基础上,定义了另一个派生类 C,其中重写了虚函数 hello。
    class C: public B // 派生类
    {
    public:
        virtual void hello() // 虚函数
        {
            cout << "C::hello" << endl;
        }
    };
  4. 主函数:在主函数中,创建了一个 C 类的对象 obj。在创建过程中,先创建基类,然后派生成 B 类对象,最后再派生成 C 类对象。在派生类对象创建时,类的构造函数的执行顺序是基类 -> 派生类 B -> 派生类 C。因此,当创建 C 类对象时,先调用了 A 类中的构造函数,然后在 B 类的构造函数中调用了 hello 函数,输出了 B::hello,最后完成 C 类的构造。
    int main()
    {
        C obj;
        return 0;
    }
  5. 程序执行完毕,自动释放内存,析构的顺序与构造的顺序相反,即先执行派生类 C 、B 的析构函数,然后执行 A 类的析构函数。
  6. 总之,该代码主要介绍了虚函数、继承和多态,并演示了不同派生类之间的函数执行顺序和函数调用方式。由于派生类构造函数和析构函数的特殊性,建议在这种情况下尽量避免使用虚函数,以免造成不必要的问题。

【执行结果】

  • 程序执行过程中,首先创建了一个 C 类的对象 obj,而 C 类继承了 B 类,B 类又继承了 A 类。
  • 在创建 obj 对象的过程中,按照基类 -> 派生类 B -> 派生类 C 的顺序执行构造函数。在 B 类中的构造函数中调用了 hello() 函数,输出了 B::hello。由于此时对象还未被完全构造,因此无法使用多态。
  • main 函数中没有显示调用任何函数,因此 main 函数执行完后,程序进入析构阶段,按照派生类 C -> 派生类 B -> 基类 A 的顺序执行析构函数。在 B 类的析构函数中调用了 bye() 函数,但由于同样无法使用多态,因此输出了 A::bye。
  • 因为 B 类中的 hello() 和 bye() 函数都没有使用多态,因此在创建 B 类的实例化对象时,这两个函数的调用都不是多态的。
B::hello
A::bye

【示例三】多态机制

【示例代码】实现了基于 C++ 中的多态机制的实例:

#include<iostream>
using namespace std;

// 定义类 A 
class A 
{
public:
    // 在类 A 中定义 func1() 函数
    void func1() 
    { 
        cout << "A::func1" << endl; 
    } 
    // 在类 A 中定义 func2() 函数并将其设置为虚函数
    virtual void func2() 
    {
        cout << "A::func2" << endl; 
    } 
};

// 定义类 B,继承自类 A
class B : public A 
{
public:
    // 在类 B 中重新定义 func1() 函数并将其设置为虚函数
    virtual void func1() 
    {
        cout << "B::func1" << endl;
    }
    // 在类 B 中重新定义 func2() 函数并将其设置为虚函数
    void func2() // func2 自动成为虚函数
    {
        cout << "B::func2" << endl;
    }
};

// 定义类 C,继承自类 B
class C : public B //类 C 以类 A 为间接基类
{
public:
    // 在类 C 中重新定义 func1() 函数并将其设置为虚函数
    void func1() // func1 自动成为虚函数
    {
        cout << "C::func1" << endl;
    }
    // 在类 C 中重新定义 func2() 函数并将其设置为虚函数
    void func2() // func2 自动成为虚函数
    {
        cout << "C::func2" << endl;
    }
};

// 程序入口函数
int main()
{
    // 创建 C 类的对象 obj
    C obj;
    // 使用 A 类型的指针指向 obj 对象
    A* pa=&obj;
    // 使用 B 类型的指针指向 obj 对象
    B* pb=&obj;
    // 通过调用 pa 所指向对象的 func2() 函数,触发多态
    pa->func2(); // 多态
    // 直接调用 pa 所指向对象的 func1() 函数,因为 func1() 不是虚函数,所以不会发生多态
    pa->func1(); // 不是多态
    // 通过调用 pb 所指向对象的 func1() 函数,触发多态
    pb->func1(); // 多态
    return 0;
}

【代码详解】

  1. #include<iostream>:引入 C++ 中标准输入输出库 iostream。

  2. using namespace std;:使用 std 命名空间。

  3. class A:定义了一个类 A。

  4. class B : public A:定义了一个类 B,它继承自类 A。

  5. class C : public B:定义了一个类 C,它继承自类 B。

  6. void func1():类 A、B、C 中都定义了一个名为 func1() 的函数,分别被重载并指定虚函数。

  7. virtual void func2():类 A、B、C 中都定义了一个名为 func2() 的函数,并将其声明为虚函数。其中 void func2() 可以被重载,而如果要使用基类里的同名函数,可以加上关键字 virtual,这样会在运行时确定身份,从而实现多态,这也是 c++ 中多态常用的实现方式。

  8. pa->func2();:使用指向类 A 对象的指针调用虚函数 func2(),触发了多态,因为指向对象的类型在运行时确定。

  9. pa->func1();:使用指向类 A 对象的指针调用非虚函数 func1(),不能触发多态。

  10. pb->func1();:使用指向类 B 对象的指针调用虚函数 func1(),触发了多态,因为指向对象的类型在运行时确定。

【执行结果】

  • C::func2 :表示使用指向 C 类对象的指针调用多态函数 func2(),触发了多态机制,因为该函数在 C 类中被重载;
  • A::func1 :表示使用指向 A 类对象的指针调用非虚函数 func1(),不能触发多态;
  • B::func1 :表示使用指向 B 类对象的指针调用多态函数 func1(),触发了多态机制,因为该函数在 B 类中被重载:
C::func2
A::func1
B::func1


四、虚析构函数

  • 如果一个基类指针指向的对象是用 new 运算符动态生成的派生类对象,那么释放该对象所占用的空间时,如果仅调用基类的析构函数,则只会完成该析构函数内的空间释放,不会涉及派生类析构函数内的空间释放,容易造成内存泄漏。
  • 声明虚析构函数的一般格式如下:
    virtual 〜类名( );
  • 虚析构函数没有返回值类型,没有参数,所以它的格式非常简单。
  • 虚析构函数没有返回值类型,没有参数,所以它的格式非常简单。
  • 如果一个类的析构函数是虚函数,则由它派生的所有子类的析构函数也是虚析构函数。使用虚析构函数的目的是为了在对象消亡时实现多态。
  • 可以看出,这次不仅调用了基类的析构函数,也调用了派生类的析构函数
  • 只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用 virtual 关键字声明,都自动成为虚析构函数
  • 一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。
  • 不过切记,构造函数不能是虚函数 

【示例一】虚析构函数

【示例代码】主要展示了一个基类 ABase 和一个派生类 DerivedDerived 继承自 ABase。在 main 函数中,使用一个基类指针指向 new 创建的派生类对象,并在使用完毕后释放指针:

#include<iostream>
using namespace std;     // 命名空间

class ABase {
public:
    ABase() {            // 基类构造函数
        cout << "ABase构造函数" << endl;
    }
    ~ABase() {           // 基类析构函数
        cout << "ABase::析构函数" << endl;
    }
};

class Derived: public ABase {   // 继承自基类
public:
    int w, h;      // 成员变量
    Derived() {    // 派生类构造函数
        cout << "Derived构造函数" << endl;
        w = 4;
        h = 7;
    }
    ~Derived() {  // 派生类析构函数
        cout << "Derived::析构函数" << endl;
    }
};

int main() {
    ABase* p = new Derived();   // 使用基类指针指向 new 创建的派生类对象
    delete p;                   // 删除基类指针
    return 0;
}

【代码详解】

  1. 首先,创建了一个派生类 Derived 对象,并分配了内存。
  2. 接着,ABase 基类的构造函数调用,输出 “ABase构造函数”。
  3. 然后,Derived 派生类的构造函数调用,输出 “Derived构造函数”。
  4. 运行结束后,调用了析构函数 ~Derived() 和 ~ABase(),分别输出 “Derived::析构函数” 和 “ABase::析构函数”。
  5. 最后释放内存。

【特别注意】

  • 由于基类 ABase 的析构函数是虚函数,因此在释放指针时会调用派生类的析构函数,而不是基类的析构函数。
  • 这里通过使用基类指针与派生类的对象进行绑定,实现了基类对派生类的控制。
  • 需要注意的是这里基类 ABase 的析构函数是虚函数,子类的析构函数应该也都是虚函数(C++11 规范已经定义,析构函数默认也都是虚函数),这样才能保证在释放内存时调用到正确的析构函数。

【执行结果】

  • 首先创建了一个派生类 Derived 的对象,并分配了内存;
  • 接着输出基类 ABase 的构造函数和派生类 Derived 的构造函数;
  • 然后通过使用基类指针指向 new 创建的派生类对象并进行操作;
  • 最后通过删除基类指针来释放内存。
  • 当程序结束时,会先调用派生类 Derived 的析构函数,然后再调用基类 ABase 的析构函数。
ABase构造函数
Derived构造函数
Derived::析构函数
ABase::析构函数

【示例二】虚析构函数

【示例代码】程序定义了一个基类 ABase 和一个派生类 Derived,其中 Derived 继承自基类 ABase。在 main 函数中,创建了一个派生类对象,并使用一个基类指针来指向该派生类对象,然后将其删除:

#include <iostream>

using namespace std;

class ABase {
public:
    ABase() {            // 基类构造函数
        cout << "ABase构造函数" << endl;
    }
    virtual ~ABase() {   // 基类析构函数(虚函数) ,与上一个示例代码唯一区别的代码行
        cout << "ABase::析构函数" << endl;
    }
};

class Derived: public ABase {   // 派生类,继承自基类
public:
    int w, h;      // 成员变量
    Derived() {    // 派生类构造函数
        cout << "Derived构造函数" << endl;
        w = 4;
        h = 7;
    }
    ~Derived() {  // 派生类析构函数
        cout << "Derived::析构函数" << endl;
    }
};

int main() {
    ABase* p = new Derived();   // 使用基类指针指向 new 创建的派生类对象
    delete p;                   // 删除基类指针
    return 0;
}

【代码详解】

  1. #include <iostream>:包含输入输出流库 iostream
  2. using namespace std:使用命名空间 std
  3. class ABase:声明一个基类 ABase
  4. public:公有的成员函数和成员变量。
  5. ABase():基类的构造函数。
  6. cout:输出 “ABase构造函数”。
  7. virtual ~ABase():基类的虚析构函数。
  8. cout:输出 “ABase::析构函数”。
  9. class Derived:声明一个派生类 Derived
  10. public:公有的成员函数和成员变量。
  11. int w, h:成员变量 w 和 h 定义整数类型。
  12. Derived():派生类的构造函数。
  13. cout:输出 “Derived构造函数”。
  14. w=4; h=7:为成员变量 w 和 h 赋初值。
  15. ~Derived():派生类的析构函数。
  16. cout:输出 “Derived::析构函数”。
  17. ABase* p = new Derived():使用基类指针来指向 new 创建的派生类对象。
  18. delete p:删除基类指针,执行析构函数。
  19. return 0:程序结束,返回成功的信号。
  20. 由于 ABase 的析构函数是虚函数,因此在删除 ABase 类型的指针时会自动调用派生类 Derived 的析构函数;这里使用了基类指针指向派生类对象,通过运行时角度来实现子类多态性。

【执行结果】

  • 程序首先创建了一个派生类 Derived 的对象,并将其地址存储在基类指针 p 中。
  • 接着打印出 “ABase构造函数” 和 “Derived构造函数”。
  • 然后通过调用 delete 删除了这个对象,并依次调用了派生类的析构函数和基类的析构函数。
  • 由于基类的析构函数是虚函数,因此调用的是派生类 Derived 的析构函数。
ABase构造函数
Derived构造函数
Derived::析构函数
ABase::析构函数


五、纯虚函数和抽象类

(1)纯虚函数 

  • 纯虚函数的作用相当于一个统一的接口形式,表明在基类的各派生类中应该有这样的一个操作,然后在各派生类中具体实现与本派生类相关的操作。
  • 纯虚函数是声明在基类中的虚函数,没有具体的定义,而由各派生类根据实际需要给出各自的定义。
  • 声明纯虚函数的一般格式如下:例如:virtual void fun( )=0;
    virtual 函数类型 函数名(参数表)=0;
  • 纯虚函数没有函数体,参数表后要写 “ =0” 。派生类中必须重写这个函数。
  • 按照纯虚函数名调用时,执行的是派生类中重写的语句,即调用的是派生类中的版本。

(2)抽象类 

  • 包含纯虚函数的类称为抽象类
  • 因为抽象类中有尚未完成的函数定义,所以它不能实例化一个对象。
  • 抽象类的派生类中,如果没有给出全部纯虚函数的定义,则派生类继续是抽象类。
  • 直到派生类中给出全部纯虚函数定义后,它才不再是抽象类,也才能实例化一个对象。
  • 虽然不能创建抽象类的对象,但可以定义抽象类的指针和引用。
  • 这样的指针和引用可以指向并访问派生类的成员,这种访问具有多态性。
纯虚函数不同于函数体为空的虚函数。
它们的不同之处如下:
  • 纯虚函数没有函数体,而空的虚函数的函数体为空。
  • 纯虚函数所在的类是抽象类,不能直接进行实例化;
  • 而空的虚函数所在的类是可以实例化的。
它们共同的特点是:
  • 纯虚函数与函数体为空的虚函数都可以派生出新的类,然后在新类中给出虚函数的实现,而且这种新的实现具有多态特征。

【示例】抽象类

【示例代码】该程序定义了一个抽象类 A 和一个继承自 A 的类 BA 中有一个纯虚函数 print(),意味着 A 类型的指针可以指向 B 类的对象。在 main 函数中,通过一个基类指针来使用派生类对象,并通过调用 print() 方法来展示多态性:

#include <iostream>
using namespace std;

class A {
private:
    int a;
public:
    virtual void print() = 0;  // 纯虚函数,抽象类
    void func1() {
        cout << "func1" << endl;
    }
};

class B : public A {
public:
    void print();
    void func1() {
        cout << "B_func1" << endl;
    }
};

void B::print() {
    cout << "B_Print" << endl;
}

int main() {
    // A a; // 错误,抽象类不能实例化
    // A *p = new A; // 错误,不能创建类 A 的实例
    // A b[2]; // 错误,不能声明抽象类的数组
    A* pa; // 正确,可以声明抽象类的指针
    A* pb = new B; // 使用基类指针指向派生类对象
    pb->print(); // 调用的是类 B 中的函数,多态,输出 B_Print
    B b;
    A* pc = &b;
    pc->func1(); // 因为不是虚函数,调用的是类 A 中的函数,输出 func1
    return 0;
}

【代码详解】

  1. #include <iostream>:包含输入输出流库 iostream
  2. using namespace std:使用命名空间 std
  3. class A:声明一个抽象类 A
  4. private:私有的成员函数和成员变量。
  5. int a:成员变量 a 定义整数类型。
  6. virtual void print() = 0:纯虚函数,抽象类。
  7. void func1():成员函数。
  8. cout:输出 “func1”。
  9. class B : public A:声明一个类 B,继承自 A
  10. void print():函数重载。
  11. cout:输出 “B_Print”。
  12. void func1():函数重载。
  13. cout:输出 “B_func1”。
  14. int main():程序入口。
  15. A* pa:声明一个基类指针 pa,可以指向抽象类 A 的对象。
  16. A* pb = new B:使用指向基类的指针定义一个派生类对象。
  17. pb->print():使用基类指针调用类 B 中重载的函数 print(),输出 “B_Print”。
  18. B b:创建一个派生类 B 的对象 b
  19. A* pc = &b:使用基类指针 pc 来指向派生类对象 b
  20. pc->func1():使用基类指针调用类 A 中的函数 func1(),输出 “func1”。
  21. return 0:程序结束,返回成功的信号。

【执行结果】

  • 这段代码定义了类 A 和它的派生类 BA 是一个抽象类,其中包含一个纯虚函数 print(),被声明为纯虚函数的函数没有默认实现,需要在派生类中实现。
  • B 公开了 print() 方法,而且将其实现为输出 “B_Print”。
  • B 的另一个成员方法 func1() 也被定义为输出 “B_func1”。
  • 在 main 函数中,尝试实例化抽象类 A 和其数组都是错误的,但是可以使用 A* 类型的指针指向派生类 B 的对象,这是因为 C++ 的多态机制允许基类指针指向派生类对象,这样就可以实现动态绑定,即根据对象的实际类型来调用方法。
  • 在这里,A* pb = new B; 实例化了一个指针 pb,指向派生类 B 的对象。
  • pb->print(); 通过基类 A 的指针来调用纯虚函数 print(),由于在 B 中实现了 print() 方法,所以调用的实际上是类 B 中的 print() 方法,输出 “B_Print”。
  • 接下来,实例化了一个 B 对象 b,将其地址赋值给指向 A 类型的指针 pc,然后调用 pc->func1(),虽然 func1() 函数是非虚函数,因此不依赖于对象类型,但是因为在 B 中重新定义了 func1() 函数,所以此处会调用 B 中的 func1() 函数,输出 “B_func1”。
B_Print
B_func1

(3)虚基类

【格式】定义虚基类的一般格式如下:
class 派生类名:virtual 派生方式 基类名
{
    派生类体
};

【说明】为了避免产生二义性,C++ 提供虚基类机制,使得在派生类中,继承同一个间接基类的成员仅保留一个版本。

【示例】如图所示的各类的继承关系如下:

class A
class B : virtual public A
class C : virtual public A
class D : public B, public C

 

【示例】虚基类

【示例代码】

#include <iostream>
using namespace std;

class A {
public:
    int a;
    void showa() {
        cout << "a=" << a << endl;
    } 
};

class B : virtual public A {  // 对类 A 进行了虚继承
public:
    int b;
}; 

class C : virtual public A {  // 对类 A 进行了虚继承
public:
    int c;
}; 

class D : public B, public C {  // 派生类 D 的两个基类 B、C 具有共同的基类 A
                                 // 采用了虚继承,从而使类 D 的对象中只包含着类 A 的1个实例
public:
    int d;
}; 

int main() {
    D Dobj;  // 说明派生类 D 的对象
    Dobj.a = 11;  // 若不是虚继承,此行会出错!因为 "D::a" 具有二义性
    Dobj.b = 22;
    Dobj.showa();  // 输出 a=11
                   // 若不是虚继承,此行会出错!因为 "D::showa" 具有二义性
    cout << "Dobj.b=" << Dobj.b << endl;  // 输出 Dobj.b=22
}

【代码详解】

  • 这段代码定义了 4 个类:基类 A,以及 3 个派生类:BC 和 DB 和 C 都使用了虚继承 AD 继承 B 和 C。这种虚继承方式可以避免 “二义性” 的问题,即在派生类中有多个基类含有相同的成员变量或函数,这带来了歧义,无法确定使用哪一个基类中的成员。
  • 在 main() 函数中,创建了一个 D 对象 Dobj,这里称为根对象或顶层对象。由于 D 是多重继承,因此构造时需要先构造基类,再构造派生类。具体来说,先构造虚基类 A,再分别构造 B 和 C,最后构造 D。这样可以保证 D 对象中只有一个 A 子对象。如果不使用虚继承,则 B 和 C 类各继承了 A 的拷贝,导致 D 对象中有两个 A 子对象,重复,浪费空间。
  • 接着设置了 a 和 b 的值,这里直接访问了 D 对象的成员变量 a 和 b,虽然存在多条路径可以到达成员变量 a,但由于使用了虚继承,因此只有一份变量 a,因此不存在二义性问题。同样地,调用 showa() 方法输出 Dobj.a 的值,由于是虚函数,因此只有一个 showa() 方法,也不会产生二义性。最后输出 Dobj.b 的值。
  • 需要注意的是,在派生类 D 中直接访问基类 A 的成员变量或成员函数,可能存在二义性或者歧义问题,必须清楚指定是哪个 A 子对象。使用虚继承可以有效解决这个问题。

【执行结果】

a=11
Dobj.b=22

猜你喜欢

转载自blog.csdn.net/qq_39720249/article/details/131386361