1.概念
多态分为编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。示例代码如下所示:
1)函数重载
void print(int x) {std::cout << "Integer: " << x << std::endl;
}void print(double x) {std::cout << "Double: " << x << std::endl;
}void print(const std::string& x) {std::cout << "String: " << x << std::endl;
}
2)函数模板
template <typename T>
void print(T x) {std::cout << "Value: " << x << std::endl;
}
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。也就是说,是在程序运行时才确定函数调用的具体实现形式,主要通过虚函数和继承机制实现。
2.虚函数
虚函数是实现运行时多态的关键。通过在基类中用virtual声明虚函数,派生类可以重写这些函数。调用虚函数时,程序会根据对象的实际类型动态选择调用的函数版本。
2.1 虚函数的重写/覆盖:
派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。例如:
class Base {
public:virtual void show() {std::cout << "Base::show" << std::endl;}
};class Derived : public Base {
public:void show() override { // 重写虚函数std::cout << "Derived::show" << std::endl;}
};
在运行时,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用对应的函数版本:
Base* ptr = new Derived();Base* ptr1 = new Base();ptr->show(); // 输出 "Derived::show"ptr1->show();//输出"Base::show"
2.2 析构函数的重写
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。
下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
class A
{public :virtual ~A(){std::cout << "~A()" << std::endl;}
};
class B : public A {
public:~B(){std::cout << "~B()->delete:" << _p << std::endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;
}
这是运行结果
有些人可能会好奇为什么输出结果是这样的,这里我解释一下:第一句delete p1 执行后,调用A的析构函数,输出~A(),之后执行delete p2 命令,先调用B的析构函数,输出
~B()->delete:0000027202410FB0,之后由于B继承了A,所以还要调用A的析构函数,输出~A()
3.override关键字
作用:1)明确告诉编译器,当前函数是用于覆盖基类中的虚函数。
2)避免错误:
如果没有使用 override
,编译器不会检查当前函数是否正确覆盖了基类的虚函数。如果基类中的虚函数签名发生变化(例如参数类型或数量改变),派生类中的函数可能无法正确覆盖,但编译器不会报错。这种情况下,可能会导致运行时错误或逻辑问题。而使用 override
后,如果派生类中的函数无法正确覆盖基类的虚函数,编译器会报错,从而避免潜在的错误。例如
class Base {
public:virtual void display(int x) { // 基类虚函数的参数改变了std::cout << "Base::display" << std::endl;}
};class Derived : public Base {
public:void display() override { // 编译错误,无法覆盖基类的虚函数std::cout << "Derived::display" << std::endl;}
};
在这种情况下,编译器会报错,提示 display
函数无法覆盖基类中的虚函数,从而避免了潜在的错误。
4.final关键字
作用:如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
例如:
// error C3248 : “Car::Drive” : 声明为“final”的函数⽆法被“Benz::Drive”重写
class Car
{public :virtual void Drive() final {}
};
class Benz :public Car
{public :virtual void Drive() { std::cout << "Benz-舒适" << std::endl; }
};
5.纯虚函数和抽象类
如果希望在基类中声明一个虚函数,但不提供具体实现,可以将其定义为纯虚函数(在后面加=0)。包含纯虚函数的类称为抽象类,不能直接实例化。例如:
class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing a Circle" << std::endl;}
};class Square : public Shape {
public:void draw() override {std::cout << "Drawing a Square" << std::endl;}
};
通过抽象类的指针或引用,可以统一调用派生类的实现:
Shape* shape1 = new Circle();
Shape* shape2 = new Square();shape1->draw(); // 输出 "Drawing a Circle"
shape2->draw(); // 输出 "Drawing a Square"
6.小结
综上所述,实现多态的两个必须重要条件是
1)是基类的指针或者引用调用虚函数
2)被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
7.原理
1)虚函数表(V-Table):编译器为每个包含虚函数的类生成一个虚函数表(V-Table),表中存储了类中所有虚函数的地址。
2)对象的V-Pointer:每个对象都包含一个指向其类的V-Table的指针(V-Pointer)。当通过基类指针或引用调用虚函数时,程序会通过对象的V-Pointer找到对应的V-Table,再通过V-Table找到实际的函数地址