多态
文章源地址:修能的博客
什么是多态?
多态(Polymorphism)
是面向对象编程中的一个重要概念,它允许以统一的方式处理不同类型的对象,即一个对象可以表现出多种形态。
在多态中,通过继承和函数重写,派生类可以重写基类的成员函数,使得同一函数名在不同的类中具有不同的实现。然后,通过基类的指针或引用指向派生类的对象,可以在运行时动态地选择调用哪个类的成员函数,而不需要在编译时就确定。
多态分为两类:
- 静态多态
- 动态多态
静态多态
静态多态性通过函数重载和模板机制实现,它在编译时根据参数的类型来确定调用哪个函数或模板实例。
静态多态性在编译时就确定了函数调用的具体实现。
动态多态
动态多态(动态多态性,Dynamic Polymorphism)
动态多态性通过继承、虚函数和运行时多态实现。
派生类可以重写基类的虚函数,当使用基类的指针或引用指向派生类的对象时,根据指针或引用的实际类型来调用相应的函数。
动态多态性在运行时确定了函数调用的具体实现
静态多态与动态多态的区别
静态多态(Static Polymorphism)和动态多态(Dynamic Polymorphism)是多态的两种不同形式,它们在实现机制和运行时行为上有一些区别。
-
实现机制:
-
静态多态:静态多态性通过函数重载和模板机制实现。
函数重载允许在同一作用域内定义多个同名函数,但它们的参数类型或参数个数不同。
在编译时,根据函数调用时**提供的参数类型(函数的签名)**来确定具体调用哪个函数,编译器会进行静态绑定。
-
动态多态:动态多态性通过继承和虚函数机制实现。
基类可以声明虚函数,派生类可以**重写(覆盖)**基类的虚函数。
在运行时,通过基类的指针或引用指向派生类的对象时,根据对象的实际类型来确定调用哪个函数,这是由编译器和运行时系统共同实现的动态绑定。
-
-
调用时机:
- 静态多态:函数重载的解析发生在编译时,根据参数的类型和数量确定具体调用哪个函数。编译器通过静态类型进行函数调用的决策。
- 动态多态:虚函数的调用发生在运行时,根据指针或引用的实际类型确定调用的具体函数。运行时系统在运行时根据对象的动态类型进行函数调用的决策。
-
继承关系:
- 静态多态:函数重载和模板机制没有直接依赖于继承关系。
- 动态多态:动态多态性通过继承和虚函数机制实现。基类声明了虚函数,派生类可以重写(覆盖)基类的虚函数,运用继承关系来实现多态性。
-
运行时开销:
- 静态多态:由于在编译时确定了函数的具体调用,静态多态性没有运行时开销。函数重载和模板的函数展开是在编译阶段完成的。
- 动态多态:动态多态性需要在运行时进行函数调用的决策,因此相对于静态多态性,会带来一定的运行时开销,包括虚函数指针和虚表的查找。
多态的底层原理
多态的底层原理涉及两个核心概念:虚函数表(vtable)
和虚函数指针(vptr)
。
当一个类中包含虚函数时,编译器会为该类创建一个虚函数表(vtable)。
虚函数表是一个存储了虚函数地址的表格,每个虚函数在表格中占据一个位置。这个表格是类的静态数据成员,与类的每个对象实例无关。
此外,编译器还会在类的每个对象实例中添加一个指向虚函数表的指针,即虚函数指针(vptr)。这个指针在对象的内存布局中的位置可能有所不同,但通常位于对象的起始位置或者附加在该对象的内存上。
当创建一个对象实例时,编译器会为虚函数表分配内存,并将虚函数地址填充到相应的位置上。同时,虚函数指针会被设置为指向该对象对应的虚函数表。
在使用对象指针或者引用调用虚函数时,编译器通过虚函数指针从虚函数表中获取对应的函数地址。这个过程被称为动态绑定(dynamic binding)或者后期绑定(late binding)。通过动态绑定,程序能够根据对象的实际类型来调用正确的函数实现,而不仅仅是根据指针或引用的类型。
总结起来,多态的底层原理可以概括为以下几个步骤:
- 编译器为包含虚函数的类创建虚函数表,其中存储了虚函数的地址。
- 每个对象实例中添加一个指向对应虚函数表的虚函数指针。
- 调用虚函数时,通过虚函数指针获取对应的虚函数地址。
- 根据虚函数地址调用正确的函数实现,实现动态绑定。
这种机制使得代码能够根据对象的实际类型来选择调用适当的函数实现,实现了多态性的特性。
父类指针指向子类对象
可以通过父类指针来告诉编译器所指定的子类对象。
#include <iostream>
// 基类 Animal
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound!" << std::endl;
}
};
// 派生类 Cat
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Cat says: Meow!" << std::endl;
}
};
// 派生类 Dog
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog says: Woof!" << std::endl;
}
};
int main() {
Animal* animal;
Cat cat;
Dog dog;
animal = &cat;
animal->makeSound(); // 输出:Cat says: Meow!
animal = &dog;
animal->makeSound();
return 0;
}
父类引用指向子类对象
将以上案例中的主函数修改成:
int main() {
Cat cat;
Dog dog;
Animal& animal1 = cat;
animal1.makeSound(); // 输出:Cat says: Meow!
Animal& animal2 = dog;
animal2.makeSound(); // 输出:Dog says: Woof!
return 0;
}
纯虚函数和抽象类
继承的出现使父类中的函数出现了一个问题,那就是一般程序调用的并不是父类中的函数,主要都是重写子类中的同名函数,所以父类中的函数一般是没有意义的,所以父类中的函数一般都会写成纯虚函数。
纯虚函数(Pure Virtual Function)是一个在基类中声明的虚函数,但没有提供默认的函数实现。纯虚函数的声明使用
=0
来指示,它告诉编译器无法在基类中对该函数提供具体的实现,而是要求派生类对该函数进行实现。纯虚函数通常用于定义抽象类的接口,它是一种约定,要求派生类必须提供该函数的具体实现来满足基类的接口要求。通过这种方式,基类可以定义一组函数接口,但不提供默认实现,将具体的实现留给派生类去完成。
纯虚函数的写法:
virtual returnType functionName(parameters) = 0;
只要有了纯虚函数的类,就是抽象类。
抽象类(Abstract Class)是一个包含纯虚函数的类,无法直接实例化,只能被其他类继承和使用。抽象类用于定义一个接口和基础行为,而不为这些行为提供具体实现。
抽象类的主要目的是作为其他相关类的基类,它定义了一组接口和纯虚函数,要求派生类必须实现这些接口和纯虚函数,从而完成特定的功能。
虚析构和纯虚析构
虚析构函数(Virtual Destructor)
和纯虚析构函数(Pure Virtual Destructor)
是在面向对象编程中用于对对象进行销毁和释放资源的特殊函数。
虚析构函数是在基类中声明为虚函数的析构函数。当通过基类的指针删除对象时,如果基类的析构函数没有声明为虚函数,可能会导致派生类的析构函数不被调用,从而造成资源泄露。通过将基类的析构函数声明为虚函数,可以确保在通过基类指针删除对象时,会调用正确的析构函数序列。
最大的作用就是让编译器正确的调用析构函数去析构对象。
适用于子类在堆区开辟数据的类。
#include <iostream>
class Base {
public:
// 虚析构函数
virtual ~Base() {
std::cout << "Base destructor called." << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor called." << std::endl;
}
};
int main() {
Base* obj = new Derived();
delete obj; // 输出:Derived destructor called. Base destructor called.
return 0;
}