MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++父类虚函数的继承规则

2024-12-162.1k 阅读

C++ 父类虚函数的继承规则

虚函数基础概念

在 C++ 中,虚函数(virtual function)是一种特殊的成员函数,它允许在派生类中被重新定义。虚函数主要用于实现多态性(polymorphism),这是面向对象编程的重要特性之一。

当一个函数被声明为虚函数时,它在基类和派生类中都可以有不同的实现。通过基类指针或引用调用虚函数时,会根据指针或引用实际指向的对象类型来决定调用哪个类的函数版本,这一过程被称为动态绑定(dynamic binding)。

例如,假设有一个基类 Animal,其中包含一个虚函数 speak

class Animal {
public:
    virtual void speak() {
        std::cout << "I am an animal" << std::endl;
    }
};

这里,speak 函数被声明为虚函数。现在我们可以定义一个派生类 Dog 并重新定义 speak 函数:

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog 类中,speak 函数重写(override)了基类 Animalspeak 函数。这里使用了 override 关键字,它是 C++11 引入的,用于显式表明该函数是重写基类的虚函数,有助于编译器检查重写的正确性。如果不小心拼写错误函数名,编译器会报错,而不是创建一个新的函数。

虚函数的继承规则

  1. 自动继承虚属性
    • 当派生类从基类继承时,基类中的虚函数会自动成为派生类的虚函数,即使在派生类中没有再次使用 virtual 关键字声明。例如:
class Base {
public:
    virtual void print() {
        std::cout << "Base::print" << std::endl;
    }
};
class Derived : public Base {
public:
    void print() {
        std::cout << "Derived::print" << std::endl;
    }
};

在上述代码中,Derived 类的 print 函数虽然没有再次声明为 virtual,但它依然是虚函数,因为它继承自 Base 类的虚函数 print。这是 C++ 虚函数继承的一个重要特性,使得在派生类层次结构中,虚函数的多态行为能够自然延续。

  1. 重写的规则
    • 函数签名必须一致:派生类中重写虚函数时,函数签名(函数名、参数列表、常量性)必须与基类中的虚函数完全一致。例如:
class Shape {
public:
    virtual double area() const {
        return 0.0;
    }
};
class Circle : public Shape {
public:
    double area() const override {
        // 假设半径为 5
        return 3.14 * 5 * 5;
    }
};

这里,Circle 类的 area 函数重写了 Shape 类的 area 函数。注意,两个函数都是 const 成员函数,且参数列表和函数名完全相同。如果在 Circle 类的 area 函数中不小心遗漏了 const,编译器会将其视为一个新的函数,而不是重写 Shape 类的 area 函数。

  • 返回类型的协变:在 C++ 中,重写虚函数时,返回类型可以是基类虚函数返回类型的派生类型,这被称为返回类型的协变(covariant return types)。例如:
class BaseObject {
};
class DerivedObject : public BaseObject {
};
class Base {
public:
    virtual BaseObject* createObject() {
        return new BaseObject();
    }
};
class Derived : public Base {
public:
    DerivedObject* createObject() override {
        return new DerivedObject();
    }
};

在这个例子中,Derived 类的 createObject 函数返回 DerivedObject*,它是 BasecreateObject 函数返回类型 BaseObject* 的派生类型。这种返回类型的协变特性增强了多态性的灵活性,使得派生类能够返回更具体类型的对象。

  1. 访问控制与虚函数继承
    • 基类虚函数的访问控制属性在派生类中重写时会有所影响。例如,如果基类的虚函数是 public 的,派生类重写的虚函数可以是 publicprotected,但不能是 private。因为如果派生类将重写的虚函数设为 private,那么通过基类指针或引用就无法访问到该函数,这与多态性的初衷相违背。例如:
class A {
public:
    virtual void func() {
        std::cout << "A::func" << std::endl;
    }
};
class B : public A {
protected:
    void func() override {
        std::cout << "B::func" << std::endl;
    }
};

在上述代码中,B 类重写的 func 函数是 protected 的。虽然通过 B 类的对象不能直接调用 func,但通过 A 类指针或引用指向 B 类对象时,仍然可以调用 B 类重写的 func 函数。

  • 相反,如果基类虚函数是 protected,派生类重写的虚函数可以是 protectedprivate,但不能是 public。因为派生类不能扩大基类成员的访问权限。例如:
class C {
protected:
    virtual void anotherFunc() {
        std::cout << "C::anotherFunc" << std::endl;
    }
};
class D : public C {
private:
    void anotherFunc() override {
        std::cout << "D::anotherFunc" << std::endl;
    }
};

在这个例子中,D 类重写的 anotherFunc 函数是 private 的,这是符合访问控制规则的。

  1. 虚函数与多重继承 在多重继承的情况下,虚函数的继承规则会变得稍微复杂一些。假设有多个基类,每个基类都有虚函数,派生类继承自这些基类并可能重写虚函数。例如:
class Base1 {
public:
    virtual void print1() {
        std::cout << "Base1::print1" << std::endl;
    }
};
class Base2 {
public:
    virtual void print2() {
        std::cout << "Base2::print2" << std::endl;
    }
};
class Derived : public Base1, public Base2 {
public:
    void print1() override {
        std::cout << "Derived::print1" << std::endl;
    }
    void print2() override {
        std::cout << "Derived::print2" << std::endl;
    }
};

在这个多重继承的例子中,Derived 类继承自 Base1Base2 两个基类,并分别重写了它们的虚函数 print1print2。通过 Derived 类对象或指向 Derived 类对象的 Base1Base2 指针/引用,可以调用相应的重写函数。

然而,如果不同基类中有相同签名的虚函数,可能会导致命名冲突。例如:

class BaseA {
public:
    virtual void commonFunc() {
        std::cout << "BaseA::commonFunc" << std::endl;
    }
};
class BaseB {
public:
    virtual void commonFunc() {
        std::cout << "BaseB::commonFunc" << std::endl;
    }
};
class DerivedAB : public BaseA, public BaseB {
public:
    void commonFunc() override {
        std::cout << "DerivedAB::commonFunc" << std::endl;
    }
};

在这种情况下,DerivedAB 类重写了两个基类中相同签名的 commonFunc 函数。通过 DerivedAB 类对象调用 commonFunc 不会有问题,但如果通过 BaseABaseB 指针/引用调用,就需要明确指定使用哪个基类的版本,例如:

DerivedAB obj;
BaseA* ptrA = &obj;
ptrA->BaseA::commonFunc(); // 调用 BaseA 的 commonFunc
BaseB* ptrB = &obj;
ptrB->BaseB::commonFunc(); // 调用 BaseB 的 commonFunc
  1. 虚函数与菱形继承 菱形继承是多重继承中的一种特殊情况,它可能会导致数据冗余和歧义问题。当涉及虚函数时,同样需要注意一些规则。例如:
class GrandParent {
public:
    virtual void print() {
        std::cout << "GrandParent::print" << std::endl;
    }
};
class Parent1 : public GrandParent {
public:
    void print() override {
        std::cout << "Parent1::print" << std::endl;
    }
};
class Parent2 : public GrandParent {
public:
    void print() override {
        std::cout << "Parent2::print" << std::endl;
    }
};
class Child : public Parent1, public Parent2 {
public:
    void print() override {
        std::cout << "Child::print" << std::endl;
    }
};

在这个菱形继承结构中,Child 类从 Parent1Parent2 继承,而 Parent1Parent2 又都从 GrandParent 继承。Child 类重写了 GrandParent 类的 print 虚函数。

为了避免数据冗余和歧义,通常使用虚继承(virtual inheritance)。例如:

class GrandParent {
public:
    virtual void print() {
        std::cout << "GrandParent::print" << std::endl;
    }
};
class Parent1 : virtual public GrandParent {
public:
    void print() override {
        std::cout << "Parent1::print" << std::endl;
    }
};
class Parent2 : virtual public GrandParent {
public:
    void print() override {
        std::cout << "Parent2::print" << std::endl;
    }
};
class Child : public Parent1, public Parent2 {
public:
    void print() override {
        std::cout << "Child::print" << std::endl;
    }
};

在这种情况下,通过虚继承,GrandParent 的数据成员在 Child 类中只会有一份拷贝,并且虚函数的调用也能正确解析,确保多态行为的一致性。

  1. 纯虚函数与抽象类 纯虚函数(pure virtual function)是一种特殊的虚函数,它在基类中没有实现,仅提供函数声明,并要求派生类必须重写它。纯虚函数通过在函数声明后加上 = 0 来定义。例如:
class Shape {
public:
    virtual double area() const = 0;
};

包含纯虚函数的类被称为抽象类(abstract class)。抽象类不能实例化对象,它主要作为其他派生类的基类,为派生类提供一个通用的接口。例如,Shape 类是一个抽象类,因为它包含纯虚函数 area

派生类必须重写基类中的纯虚函数,否则派生类也会成为抽象类。例如:

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override {
        return width * height;
    }
};

Rectangle 类中,重写了 Shape 类的纯虚函数 area,因此 Rectangle 类不再是抽象类,可以实例化对象。

虚函数表与动态绑定

  1. 虚函数表(vtable) C++ 实现虚函数的动态绑定是通过虚函数表(vtable)机制。每个包含虚函数的类都有一个虚函数表,该表是一个函数指针数组,其中每个元素指向类中的一个虚函数。

当创建一个包含虚函数的类对象时,对象的内存布局中会有一个指向虚函数表的指针(vptr)。例如,对于前面定义的 Animal 类:

class Animal {
public:
    virtual void speak() {
        std::cout << "I am an animal" << std::endl;
    }
};

当创建 Animal 类的对象时,对象的内存布局大致如下:

[ vptr | other data members ]

vptr 指向 Animal 类的虚函数表,该虚函数表中包含 speak 函数的地址。

当派生类继承自包含虚函数的基类时,派生类会继承基类的虚函数表,并根据需要修改虚函数表中的指针。例如,对于 Dog 类:

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog 类对象的内存布局为:

[ vptr | other data members ]

Dog 类的虚函数表与 Animal 类的虚函数表结构相似,但 speak 函数的指针指向 Dog 类重写的 speak 函数版本。

  1. 动态绑定过程 当通过基类指针或引用调用虚函数时,动态绑定就会发生。例如:
Animal* animalPtr = new Dog();
animalPtr->speak();

在上述代码中,animalPtr 是一个 Animal 类指针,但实际指向一个 Dog 类对象。在运行时,首先通过 animalPtr 找到 Dog 类对象的 vptr,然后通过 vptr 找到 Dog 类的虚函数表,最后在虚函数表中找到 speak 函数的地址并调用它,从而实现了动态绑定,调用的是 Dog 类重写的 speak 函数。

虚函数继承规则的应用场景

  1. 图形绘制系统 在一个图形绘制系统中,可以定义一个基类 Shape,并将 draw 函数定义为虚函数:
class Shape {
public:
    virtual void draw() const = 0;
};
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};
class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

然后可以通过一个 Shape 指针数组来存储不同类型的图形对象,并调用它们的 draw 函数:

Shape* shapes[2];
shapes[0] = new Circle();
shapes[1] = new Rectangle();
for (int i = 0; i < 2; ++i) {
    shapes[i]->draw();
}

这样,通过虚函数的继承和动态绑定,实现了不同图形对象的统一绘制接口,使得系统具有良好的扩展性和灵活性。

  1. 游戏开发中的角色行为 在游戏开发中,假设有一个基类 Character,包含虚函数 performAction
class Character {
public:
    virtual void performAction() {
        std::cout << "Character is performing a default action" << std::endl;
    }
};
class Warrior : public Character {
public:
    void performAction() override {
        std::cout << "Warrior is attacking" << std::endl;
    }
};
class Mage : public Character {
public:
    void performAction() override {
        std::cout << "Mage is casting a spell" << std::endl;
    }
};

在游戏逻辑中,可以通过 Character 指针或引用来调用不同角色的 performAction 函数,实现游戏角色行为的多态性。

总结虚函数继承规则的要点

  1. 基类虚函数自动成为派生类虚函数,派生类重写虚函数时函数签名(除返回类型协变外)必须一致。
  2. 访问控制在重写虚函数时需遵循一定规则,不能随意扩大或缩小访问权限。
  3. 多重继承和菱形继承中虚函数的继承需要注意命名冲突和数据冗余问题,可通过适当的方式解决。
  4. 纯虚函数使基类成为抽象类,派生类必须重写纯虚函数才能实例化。
  5. 虚函数的动态绑定通过虚函数表机制实现,理解这一机制有助于深入掌握虚函数的工作原理。

虚函数的继承规则是 C++ 面向对象编程中实现多态性的关键,熟练掌握这些规则对于编写高效、可维护和可扩展的 C++ 程序至关重要。无论是在大型系统开发还是小型项目中,合理运用虚函数继承规则都能提升代码的质量和灵活性。