简述C++虚函数

1、什么是虚函数

在 C++ 编程中,虚函数是实现多态性的关键机制,它为面向对象程序设计带来了极大的灵活性和可扩展性。

在类的成员函数声明前加上关键字 virtual,即可将该函数定义为虚函数。例如:

class Animal {
public:
    virtual void makeSound() {
        cout << "Some generic animal sound" << endl;
    }
};

虚函数允许在派生类中重新定义(覆盖)基类中的同名函数,并且通过基类指针或引用调用该函数时,能够根据指针或引用实际指向的对象类型来动态决定调用哪个类(基类或派生类)中的函数实现,从而实现多态性。

2、虚函数与多态性

假设有如下代码:

class Animal {
public:
    virtual void makeSound() {
        cout << "Some generic animal sound" << endl;
    }
};

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

class Cat : public Animal {
public:
    void makeSound() override {
        cout << "Meow!" << endl;
    }
};

以及对应的多态实现代码:

Animal* animal1 = new Dog();
Animal* animal2 = new Cat();

animal1->makeSound(); 
animal2->makeSound();

delete animal1;
delete animal2;
  • 当执行 Animal* animal1 = new Dog(); 时,我们创建了一个 Dog 类的对象,但通过一个 Animal* 类型的指针 animal1 来指向它。同样,Animal* animal2 = new Cat(); 创建了一个 Cat 类的对象并用 Animal* 类型的指针 animal2 指向它。
  • 接下来调用 animal1->makeSound(); 时,由于 makeSound 是虚函数,虽然 animal1 是 Animal* 类型的指针,但实际指向的是 Dog 类的对象,所以会动态绑定并调用 Dog 类中重新定义的 makeSound 函数,输出 Woof!
  • 同理,当调用 animal2->makeSound(); 时,因为 animal2 实际指向的是 Cat 类的对象,所以会调用 Cat 类中重新定义的 makeSound 函数,输出 Meow!
  • 所以我们创建类时,可以使用虚函数,声明基类类型,动态绑定派生类类型,指向派生类的成员,达到“一箭双雕”的效果。
  • 如果不使用虚函数,那上述代码只能这么创建了:
  • Dog* dog = new Dog();
    Cat* cat = new Cat();
    
    dog->makeSound();
    cat->makeSound();

    这就并没有体现c++的多态性。

3、虚函数的使用规则和注意事项

     1、函数签名一致性

  • 在派生类中重新定义虚函数时,函数签名(包括函数名、参数类型、参数个数、返回类型等)必须与基类中的虚函数基本一致。可以使用 override 关键字来明确表示是在派生类中重新定义基类的虚函数,这样有助于编译器检查函数签名是否正确,避免因疏忽导致的错误。
  • 2、虚函数的继承

  • 虚函数一旦在基类中被定义为虚函数,那么在派生类中,无论是否显式地使用 virtual 关键字再次声明该函数,它都依然是虚函数。也就是说,虚函数的 “虚” 特性是可以继承的。
  • 3、虚函数与构造函数、析构函数

  • 构造函数:构造函数不能是虚函数。因为在创建对象时,首先要调用的就是构造函数来初始化对象,此时对象还未完全创建完成,不存在多态性的概念。如果将构造函数定义为虚函数,会导致编译错误。
  • 析构函数析构函数可以是虚函数,而且在很多情况下应该将析构函数设置为虚函数。当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类中申请的资源(如动态分配的内存等)无法正确释放,从而造成内存泄漏等问题。例如以下代码:
  • #include 
    
    // 基类
    class Base {
    public:
        virtual ~Base() {
            std::cout << "Base destructor called." << std::endl;
        }
    };
    
    // 派生类
    class Derived : public Base {
    public:
        int* data;
    
        Derived() {
            data = new int(10);
        }
    
        ~Derived() {
            std::cout << "Derived destructor called." << std::endl;
            delete data;
        }
    };
    
    int main() {
        Base* basePtr = new Derived();
    
        // 因为Base的析构函数是虚函数,所以删除时会先调用Derived的析构函数
        // 再调用Base的析构函数,确保资源正确释放
        delete basePtr;
    
        return 0;
    }

    编译器会根据指针的类型来决定调用哪个类的析构函数。如果基类 Base 的析构函数不是虚函数,那么不管这个指针实际指向的是哪个派生类对象(在这里实际指向的是 Derived 类对象),编译器都只会调用基类 Base 的析构函数。(最起初的那段动物案例之所以没有写析构函数是因为类中没有动态分配内存等可能引发泄露空间的操作)

4、纯虚函数与抽象类

  • 纯虚函数的定义:在基类中,将虚函数声明为纯虚函数的方式是在函数声明的末尾添加 = 0。例如:
  • class Base {
    public:
        virtual void func() = 0;
    };
  • 抽象类的概念:只要含有纯虚函数的类被称为抽象类。抽象类不能直接被实例化,即不能创建。
  • 抽象类的对象。它的主要作用是作为其他派生类的一个基础模板,是一个抽象,要求强制派生类必须要重写虚函数,实现实例化限制和接口规范

5、虚函数表与动态绑定机制

  • 虚函数表的概念:虚函数表(V-Table,Virtual Function Table)是 C++ 实现虚函数动态绑定机制的关键底层结构。
  • 虚函数表本质上是一个存储了类中所有虚函数地址的表格(通常是一个指针数组)。每个类对象在内存中会有一个指针(通常称为虚指针,vptr),这个虚指针指向所属类的虚函数表。
  • 什么是动态绑定:动态绑定指的是在程序运行时,根据对象的实际类型来决定调用哪个类中的函数实现的一种机制。不像静态绑定一样,在程序编译阶段就确定了要调用的函数具体是哪个类中的实现。提高了代码灵活性和可拓展性。
  • 虚函数中的动态绑定机制:当通过基类指针或引用调用虚函数时,实际上是通过对象的虚指针找到所属类的虚函数表,然后在虚函数表中查找对应的虚函数地址,从而实现根据对象类型动态地调用相应的虚函数。

  • 6、结语:

  • 综上所述,C++ 虚函数是实现多态性的重要工具,掌握其定义、使用规则、与其他函数(如构造函数、析构函数)的关系以及相关概念(如纯虚函数、抽象类)和内部机制(如虚函数表、动态绑定机制)等知识点,对于编写高效、灵活、可扩展的面向对象程序至关重要。

  • 以上就是关于c++实现多态的过程中涉及到的虚函数的相关知识,如有不当还请多多指教。.

你可能感兴趣的:(c++)