【C++】继承、虚函数、多态

详细地介绍C++的继承、虚函数、多态,以及虚函数表,并通过代码示例进行说明。

1. 继承 (Inheritance)

  • 概念: 继承是面向对象编程中的一个核心概念,它允许一个类(子类/派生类)从另一个类(父类/基类)那里获得属性和方法。子类可以重用父类的代码,并添加或修改自己的特性。

  • 语法:

    class BaseClass {
        // ... 父类成员 ...
    };
    
    class DerivedClass : public BaseClass {  // public 继承
        // ... 子类成员 ...
    };
    
    • public 继承: 父类的 public 成员在子类中仍然是 publicprotected 成员在子类中仍然是 protectedprivate 成员在子类中不可访问。
    • protected 继承: 父类的 publicprotected 成员在子类中都变成 protectedprivate 成员在子类中不可访问。
    • private 继承: 父类的所有成员在子类中都变成 privateprivate 成员在子类中不可访问。(一般不推荐使用)
  • 示例1:继承

    #include <iostream>
    #include <string>
    
    class Animal {
    public:
        Animal(const std::string& name) : name_(name) {}
    
        void eat() {
            std::cout << name_ << " is eating." << std::endl;
        }
        
        std::string getName() {
            return name_;
        }
    
    protected:
        std::string name_;
    };
    
    class Dog : public Animal {
    public:
        Dog(const std::string& name, const std::string& breed) : Animal(name), breed_(breed) {}
    
        void bark() {
            std::cout << name_ << " (a " << breed_ << ") is barking." << std::endl;
        }
    
    private:
        std::string breed_;
    };
    
    int main() {
        Animal animal("Generic Animal");
        animal.eat();
    //  animal.bark() //错误,Animal类没有bark函数
        
        Dog dog("Buddy", "Golden Retriever");
        dog.eat();    // 从 Animal 继承
        dog.bark();
        std::cout << "Dog's name using base class function: " << dog.getName() << std::endl; //访问父类的public函数
    //  std::cout << dog.name_ << std::endl; //错误,name_是protected,无法在main中访问。
    
        return 0;
    }
    

2. 虚函数 (Virtual Functions)

  • 概念: 虚函数是C++中实现多态的关键。在基类中声明为 virtual 的成员函数,可以在派生类中被重写(override)。当通过基类指针或引用调用虚函数时,实际调用的函数版本取决于指针或引用所指向的对象的实际类型,而不是指针或引用本身的类型。

  • 语法:

    class Base {
    public:
        virtual void someFunction() {
            // ... 基类实现 ...
        }
    };
    
    class Derived : public Base {
    public:
        void someFunction() override { // 使用 override 关键字(C++11 及以后)
            // ... 派生类实现 ...
        }
    };
    
    • override 关键字(可选,但强烈推荐):显式地告诉编译器,这个函数是重写基类中的虚函数。如果基类中没有对应的虚函数,编译器会报错,这有助于避免错误。
  • 纯虚函数 (Pure Virtual Functions)

    • 一个没有实现的虚函数,用 = 0 标记。

    • 包含纯虚函数的类称为抽象类 (Abstract Class)。

    • 抽象类不能被实例化(不能创建对象)。

    • 派生类必须实现(重写)所有纯虚函数,才能被实例化。

    • 纯虚函数用于定义接口,强制派生类实现特定的功能。

      class Shape { // 抽象类
      public:
          virtual double area() const = 0; // 纯虚函数
          virtual void draw() const = 0;      // 纯虚函数
      };
      
      // 派生类必须实现 area() 和 draw()
      class Circle : public Shape {
          // ...
          double area() const override { /* ... */ }
          void draw() const override { /* ... */ }
      
      };
      

3. 多态 (Polymorphism)

  • 概念: 多态是指“多种形态”。在C++中,多态允许我们使用基类指针或引用来操作派生类对象,并在运行时根据对象的实际类型来调用相应的函数。

  • 实现: 多态主要通过虚函数来实现。

  • 优点:

    • 代码灵活性: 可以编写通用的代码来处理不同类型的对象。
    • 可扩展性: 添加新的派生类时,无需修改现有的基类代码。
    • 代码复用: 可以使用相同的接口来处理不同类型的对象。
  • 示例1:多态

    #include <iostream>
    #include <vector>
    
    class Shape {
    public:
        virtual void draw() const {
            std::cout << "Drawing a generic shape." << std::endl;
        }
        virtual ~Shape() {} // 基类析构函数通常应为虚函数
    };
    
    class Circle : public Shape {
    public:
        void draw() const override {
            std::cout << "Drawing a circle." << std::endl;
        }
    };
    
    class Square : public Shape {
    public:
        void draw() const override {
            std::cout << "Drawing a square." << std::endl;
        }
    };
    
    int main() {
        std::vector<Shape*> shapes;
        shapes.push_back(new Circle());
        shapes.push_back(new Square());
        shapes.push_back(new Shape()); // 可以添加基类对象,但通常我们会使用抽象类
    
        for (const Shape* shape : shapes) {
            shape->draw(); // 多态:根据对象的实际类型调用 draw()
        }
        
        // 释放内存 (重要!)
        for (Shape* shape : shapes) {
            delete shape;
        }
    
        return 0;
    }
    

    输出结果:

    Drawing a circle.
    Drawing a square.
    Drawing a generic shape.
    
  • 示例2(结合虚函数和多态):

#include <iostream>
#include <vector>

class Shape {  // 抽象基类
public:
    virtual double area() const = 0; // 纯虚函数
    virtual void draw() const = 0;   // 纯虚函数
    virtual ~Shape() {} //虚析构函数
};

class Circle : public Shape {
public:
    Circle(double radius) : radius_(radius) {}

    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
    void draw() const override{
        std::cout << "Draw Circle" << std::endl;
    }

private:
    double radius_;
};

class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : width_(width), height_(height) {}

    double area() const override {
        return width_ * height_;
    }
    void draw() const override{
        std::cout << "Draw Rectangle" << std::endl;
    }

private:
    double width_;
    double height_;
};

int main() {
    std::vector<Shape*> shapes; // 存储基类指针的容器
    shapes.push_back(new Circle(5.0));
    shapes.push_back(new Rectangle(4.0, 6.0));

    for (const Shape* shape : shapes) {
        std::cout << "Area: " << shape->area() << std::endl; // 多态:调用实际对象的 area()
        shape->draw();
    }
    // 释放内存
    for (Shape* shape : shapes) {
        delete shape;
    }
    shapes.clear();

    return 0;
}

输出:

Area: 78.5397
Draw Circle
Area: 24
Draw Rectangle

代码解释:

  1. Shape 是一个抽象基类,定义了 area()draw() 纯虚函数。
  2. CircleRectangle 继承自 Shape,并分别实现了 area()draw() 函数。
  3. main 函数中,创建了一个 std::vector<Shape*>,它可以存储指向 Shape 及其派生类对象的指针。
  4. 通过循环,使用 shape->area()shape->draw() 调用虚函数。由于多态性,即使 shapeShape* 类型的指针,实际调用的也是 CircleRectangle 中对应的函数。
  5. 使用完后记得释放内存。

4. 虚函数表 (Virtual Function Table, vtable)

  • 概念: 虚函数表是C++编译器在幕后用来实现虚函数的一种机制。每个包含虚函数的类(以及其派生类)都有一个与之关联的虚函数表。虚函数表是一个函数指针数组,其中每个元素指向该类的一个虚函数的实际地址。

  • 工作原理:

    1. 创建对象: 当创建一个包含虚函数的类的对象时,编译器会在对象内存的开头(或某个固定位置)添加一个指向该类的虚函数表的指针(通常称为 vptr)。
    2. 调用虚函数: 当通过基类指针或引用调用虚函数时,编译器会:
      • 通过对象的 vptr 找到对应的虚函数表。
      • 在虚函数表中查找要调用的虚函数的索引(这个索引在编译时就确定了)。
      • 根据索引找到虚函数的实际地址,并调用该函数。
  • 示意图:

    // 假设有以下类:
    class Base {
    public:
        virtual void f1();
        virtual void f2();
    };
    
    class Derived : public Base {
    public:
        void f1() override; // 重写 f1
        virtual void f3(); // 新的虚函数
    };
    
    // 虚函数表结构可能如下:
    
    // Base 类的虚函数表 (Base::vtable)
    +-----------------+
    | &Base::f1       |  // 指向 Base::f1 的地址
    +-----------------+
    | &Base::f2       |  // 指向 Base::f2 的地址
    +-----------------+
    
    // Derived 类的虚函数表 (Derived::vtable)
    +-----------------+
    | &Derived::f1    |  // 指向 Derived::f1 的地址 (重写)
    +-----------------+
    | &Base::f2       |  // 指向 Base::f2 的地址 (继承)
    +-----------------+
    | &Derived::f3    |  // 指向 Derived::f3 的地址 (新增)
    +-----------------+
    
    // 对象内存布局:
    
    // Base 对象
    +--------+-----------------+
    | vptr  |--> Base::vtable |
    +--------+-----------------+
    | ...    |                 |  // 其他成员数据
    +--------+-----------------+
    
    // Derived 对象
    +--------+-----------------+
    | vptr  |--> Derived::vtable|
    +--------+-----------------+
    | ...    |                 |  // Base 类的成员数据
    +--------+-----------------+
    | ...    |                 |  // Derived 类的成员数据
    +--------+-----------------+
    
  • 虚函数表的存在会有很小的空间开销(每个对象一个vptr,每个类一个vtable),但带来的好处远大于这点开销。

总结

  • 继承提供了代码重用和层次化结构。
  • 虚函数和虚函数表是实现运行时多态的关键。
  • 多态提高了代码的灵活性、可扩展性和可维护性。
  • 纯虚函数和抽象类用于定义接口,强制派生类实现特定功能。
  • 虚析构函数对于防止内存泄漏至关重要,尤其是在涉及多态和动态内存分配时。 当你通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚函数,那么只有基类的析构函数会被调用,派生类的析构函数不会被调用,这可能导致资源泄漏(例如,派生类中分配的内存没有被释放)。
    把基类的析构函数声明为虚函数可以确保在删除对象时,首先调用派生类的析构函数,然后调用基类的析构函数,从而正确地释放所有资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值