Skip to content

1. 多态

1.1 多态的定义

多态(Polymorphism)是面向对象编程中的一个核心概念,它描述的是同一种类型的对象在不同情况下表现出不同的行为。简单来说,多态意味着不同的对象对相同的消息或函数调用会作出不同的响应。

多态的实现主要基于继承和方法的重写(override)。子类通过继承父类并使用或重写父类中的方法,可以表现出与父类不同的行为。当使用父类类型的引用或指针来调用一个被子类重写的方法时,会根据实际对象的类型来执行相应的方法,从而实现多态。

1.2 多态的实现

多态主要是通过虚函数来实现的。
如果将基类中某个成员函数声明为虚函数,那么其子类中与该函数具有相同原型(函数名称、参数列表和返回类型)的成员函数就也是虚函数,并且对基类中的版本形成覆盖。
这时,通过指向子类对象的基类指针,或者通过引用于类对象的基类引用,去调用函数,实际被执行的将是子类中的覆盖版本,而不是在基类中的原始版本,这种语法现象被称为多态。

1.2.1 虚函数的定义

在基类中,虚函数是通过在函数声明前加上 virtual 关键字来定义的。
注意:虚函数是在基类的函数声明前加 virtual 关键字,与子类是否加 virtual 关键字无关。换句话说,子类对应的相同原型的函数加不加 virtual 关键字都无所谓。
C++11也提供了一个保留字(override:重写),用于说明某一个函数是覆盖了基类的虚函数(重写了基类的虚函数),可写可不写。
目的:

  1. 在成员函数比较多的情况下,可以提示用户某一个函数是重写了基类的虚函数
    • 提高代码的可读性
  2. 编译器会强制检查这个函数是不是重写了基类的虚函数
    • 如果不是则报错(防止程序员粗心)
    • override修饰的函数的函数原型必须和父类的虚函数相同!
cpp
#include<iostream>
// 假设的坐标点结构  
struct Point  
{
    int x;  
    int y;  
};
// 定义每个形状的最大顶点数  
#define SIZE 10  

class Shape
{
protected:
    Point arr[SIZE]; // 结构体数组来存放顶点
    int numArr;    // 当前形状的顶点数

public:
    Shape() : numArr(0) {}

    // 添加顶点,注意这里需要确保不会超过数组的最大容量
    void addVertex(const Point& p)
    {
        if (numArr < SIZE)
        {
            arr[numArr++] = p;
        }
        else
        {
            std::cout << "Error: Too many vertices for this shape." << std::endl;
        }
    }
    virtual void draw() const//const也要加上,否则不构成多态
    {
        std::cout << "draw--by Shape" << std::endl;
    }
};

class Triangle : public Shape
{
public:
    Triangle()
    {
        // 三角形的三个顶点
        addVertex({0, 0});
        addVertex({0, 5});
        addVertex({5, 0});
    }
    // 绘制三角形的方法
    void draw() const override
    {
        std::cout << "Drawing a triangle with vertices:" << std::endl;
        for (int i = 0; i < numArr; ++i)
        {
            std::cout << "("<< arr[i].x << ", " << arr[i].y << ")" << std::endl;
        }
    }
};

// 四边形子类
class Quadrilateral : public Shape
{
public:
    Quadrilateral()
    {
        // 四边形的四个顶点
        addVertex({0, 0});
        addVertex({0, 5});
        addVertex({5, 5});
        addVertex({5, 0});
    }

    // 绘制四边形的方法
    void draw() const /*override*/ //override可写可不写
    {
        std::cout << "Drawing a quadrilateral with vertices:" << std::endl;
        for (int i = 0; i < numArr; ++i)
        {
            std::cout << "(" << arr[i].x << ", " << arr[i].y << ")";
        }
    }
};
  • 问题1: 成员函数的隐藏、重写、重载分别是什么意思,有什么区别?

    • 重载: 是指同一可访问区内被声明的几个具有不同参数的(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数。重载不关心函数返回类型。
    • 隐藏: 是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
    • 重写(覆盖): 是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
  • 问题2: 虚函数可以设置默认参数吗?
    虚函数也可以设置默认参数。
    如果虚函数设置了默认参数,则基类和派生类中设置的默认值最好保持一致。如果不一致,则以基类中设置的默认值为准。
    当虚函数的声明和定义分开的时候,只需要在声明的地方加上 virtual 的说明即可。

1.2.2 多态的表现

需要使用一个指向子类对象的基类指针,或者通过引用子类对象的基类引用,去调用这个虚函数。
实际被执行的将是子类中的覆盖版本,而不是在基类中的原始版本,这种语法现象被称为多态。

cpp
int main()
{
    Triangle triangle;
    triangle.draw(); // 显式调用Triangle的draw方法

    Quadrilateral quadrilateral;
    quadrilateral.draw(); // 显式调用Quadrilateral的draw方法

    // 如果要处理Shape数组,则需要手动检查每个对象的类型
    Shape* shapes[2] = { &triangle, &quadrilateral };
    for (int i = 0; i < 2; ++i)
    {
        shapes[i]->draw();
    }
    return 0;
}
  • 调用虚函数的指针也可以是 this 指针。当通过一个子类对象调用基类中的成员函数时,该函数里面的 this 指针就是一个指向子类对象的基类指针,再通过它去调用虚函数,同样可以表现多态的语法特性:
cpp
#include<iostream>
using namespace std;

class Base{
public:
    virtual int cal(int x,int y)
    {
        return x + y;
    }

    void func()
    {
        //this是一个基类类型的指针,当子类类型对象未调用时,此时this就是一个指向子类对象的基类指针,因此可以表现出多态
        cout << this->cal(200,300) << endl;
        //cout << cal(200,300) << endl;
    }
};

class Derived: public Base
{
public:
    int cal(int x,int y)
    {
        return x * y;
    }
};

int main()
{
    Derived d;
    //Base &b = d;
    //cout << b.cal(100,200) << endl;
    d.func();//当子类对象未调用基类函数
    return 0;
}
  • 虚函数回跳机制
    在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是执行某一个特定的函数版本,此时使用作用域运算符就可以达到这个目的:

    cpp
    shapes[i]->Shape::draw(); //显示调用Shape中的draw()函数
  • 动态类型转换
    语法:

    cpp
    目标类型变量 = dynamic_cast<目标类型>(源类型变量);

    运用场景:
    用于具有多态继承关系的父子类指针或引用的显示转换。
    注:在转换过程中会检查目标对象的类型和需要转换的类型是否一致,如果一致转换成功,否则转换失败。如果转换的是指针,返回 NULL 表示失败;如果转换时引用,抛出 "bad_cast" 异常表示失败。

    cpp
    Triangle* ptr = dynamic_cast<Triangle*>(shapes[0]); //ok ptr->draw();
    Quadrilateral* pqu = dynamic_cast<Quadrilateral*>(shapes[1]); //ok pqu->draw();
    ptr = dynamic_cast<Triangle*>(shapes[1]); //error 转换失败,空指针
  • typeid操作符
    头文件:#include<typeinfo>
    格式:

    cpp
    typeid(类型/对象/变量) //类似sizeof(...)

    返回的是 typeinfo 的对象,用于描述类型信息,里面包含一个 name 的成员函数,可以将类型信息转换为字符串形式。
    typeinfo 提供了对 ==!= 操作符重载的支持,通过它们可以直接进行类型之间的比较:

    cpp
    #include<iostream>
    #include<typeinfo>
    using namespace std;
    
    class X
    {
        virtual void foo() {}
    };
    class Y :public X
    {
        void foo() {}
    };
    class Z :public X
    {
        void foo() {}
    };
    
    void func(X& x) {
        if (typeid(x) == typeid(Y))
        {
            cout << "针对Y对象处理" << endl;
        }
        else if (typeid(x) == typeid(Z))
        {
            cout << "针对Z对象处理" << endl;
        }
        else {
            cout << "针对X对象处理" << endl;
        }
    }
    
    int main()
    {
        int a;
        cout << typeid(int).name() << endl; //i
        cout << typeid(a).name() << endl; //i
    
        int arr[10];
        cout << typeid(arr).name() << endl; //A10_i
    
        int* p;
        cout << typeid(p).name() << endl; //Pi
    
        int* arr1[10];
        int(*arr2)[10];
        cout << typeid(arr1).name() << endl; //A10_Pi
        cout << typeid(arr2).name() << endl; //PA10_i
    
        cout << typeid(Y).name() << endl; //1Y
    
        Y y;
        Z z;
        X x;
        func(y);
        func(z);
        func(x);
        return 0;
    }

1.3 纯虚函数与抽象类

  • 纯虚函数
    格式:

    cpp
    virtual 返回值类型 函数名(参数列表) = 0;

    纯虚函数不需要定义,只需要在函数声明时,在形参列表后面加上 =0 即可。
    =0 不表示函数的值为 0,仅仅是一种写法,用来告诉编译器,这个函数基类没有定义,需要派生类去实现它。

    如果一个类中含有纯虚函数,这种类叫做抽象类(Abstract class)
    抽象类不能用来实例化对象,但是可以创建基类引用绑定到派生类对象。
    如果一个类继承自一个抽象类,但是本身依然没有实现那个纯虚函数,则这个派生类依然是一个抽象类。

    cpp
    #include<iostream>
    using namespace std;
    
    class Base //抽象类
    {
    public:
        void fun1()
        {
            cout << "Base::fun1()" << endl;
        }
        virtual void fun2() = 0;//纯虚函数
    };
    
    class Derived : public Base
    {
    public:
        void fun2()
        {
            cout << "Derived::fun2()" << endl;
        }
    };
    
    int main()
    {
        Derived d;
        Base& b = d; //抽象类可以创建基类引用绑定到派生类对象
        b.fun1();
        b.fun2();
    
        //Base bl; //error 抽象类不能用来实例化对象
        return 0;
    }
  • 如果一个类中的成员函数都是纯虚函数,那么该类就是纯抽象类

    cpp
    class Base //纯抽象类
    {
    public:
        virtual void fun2() = 0; //纯虚函数
    };

1.4 虚函数实现原理(了解)

虚函数的实现原理主要依赖于 C++ 中的虚函数表和虚指针(vptr)机制。这个机制允许在运行时确定应该调用哪个类的虚函数实现,从而实现动态多态性。

  • 虚函数表(vtable)
    每个包含虚函数的类(或者说,有虚函数表的类)都会有一个与之关联的虚函数表。这个表是一个函数指针数组,其中每个指针都指向类中的一个虚函数的实现。虚函数表是在编译时创建的,并存储在程序的只读数据段中。

  • 虚指针(vptr)
    每个包含虚函数的类的对象都会有一个指向其类的虚函数表的虚指针(vptr)。这个指针在对象创建时由编译器自动设置,并存储在对象的内存布局中。vptr 通常作为对象的第一个成员变量存在(尽管具体实现可能因编译器和平台而异)。

  • 动态绑定
    当通过基类的指针或引用调用虚函数时,编译器生成的代码不会直接调用函数,而是首先通过 vptr 找到虚函数表,然后在这个表中找到对应函数的地址,最后调用这个函数。这个过程被称为动态绑定或跳跃绑定,因为它是在运行时根据对象的实际类型来确定的。

  • 继承与重写
    当派生类重写基类的虚函数时,派生类的虚函数表会包含一个新的函数指针,指向派生类中的虚函数实现。如果派生类没有重写某个虚函数,那么它的虚函数表中对应的位置将包含基类虚函数的地址。

1.5 虚析构

  • 问题:构造函数可以是虚函数吗?
    不可以。
    原因:

    1. 调用时机:构造函数是在对象创建时自动调用的,而虚函数的机制依赖于对象的 vptr(虚指针),这个 vptr 是在构造函数执行过程中被初始化的。因此,在构造函数执行期间,vptr 还没有被设置,所以无法实现动态绑定。
    2. 继承关系:在对象创建时,首先调用的是基类的构造函数,然后是派生类的构造函数(如果有的话)。如果构造函数是虚函数,那么调用基类的构造函数就需要确定实际调用的派生类构造函数,这在逻辑上是不合理的。
    3. 设计原则:构造函数的主要目的是初始化对象的状态,而不是实现对象的行为。虚函数主要用于实现对象的行为多态性,因此将构造函数设计为虚函数并不符合面向对象的设计原则。
  • 析构函数可以是虚函数吗?
    可以。析构函数在调用前对象肯定存在了。
    问题场景:
    基类的析构函数不会自动调用子类的析构函数。如果对一个指向子类对象的基类指针使用 delete 操作符,实际被执行的仅是基类的析构函数,子类的析构函数执行不到,有内存泄露的风险:

    cpp
    #include<iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(void) : m_i(0)
        {
            cout << "Base::Base(void)" << endl;
        }
        ~Base()
        {
            cout << "Base::~Base()" << endl;
        }
        int m_i;
    };
    
    class Derived : public Base
    {
    public:
        Derived(void)
        {
            cout << "Derived::Derived(void)" << endl;
            // 动态分配资源
            m_dynamicArray = new int[10];
        }
        ~Derived()
        {
            cout << "Derived::~Derived()" << endl;
            //释放动态分配的资源
            delete[] m_dynamicArray;
        }
        int* m_dynamicArray; // 动态分配的整数数组
        int m_il;
    };
    
    int main()
    {
        Base* pb = new Derived; // 分配Derived对象
        delete pb; // 通过基类指针释放对象,但由于Derived的析构函数没有释放资源,发生内存泄露
        return 0;
    }
  • 解决方案
    将基类的析构函数声明为虚函数。子类的析构函数会自动成为虚函数,并覆盖基类的虚析构函数:

    cpp
    #include<iostream>
    using namespace std;
    
    class Base
    {
    public:
        Base(void) : m_i(0)
        {
            cout << "Base::Base(void)" << endl;
        }
        virtual ~Base()
        {
            cout << "Base::~Base()" << endl;
        }
        int m_i;
    };
    
    class Derived : public Base
    {
    public:
        Derived(void)
        {
            cout << "Derived::Derived(void)" << endl;
            m_dynamicArray = new int[10];
        }
        ~Derived()
        {
            cout << "Derived::~Derived()" << endl;
            delete[] m_dynamicArray;
        }
        int* m_dynamicArray;
        int m_il;
    };
    
    int main()
    {
        Base* pb = new Derived;
        delete pb; // 正确调用子类析构函数
        return 0;
    }

知识如风,常伴吾身