
多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在C++中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。
核心概念
多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。 实现多态还有两个必须重要条件:
说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类 对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派 ⽣类之间才能有不同的函数,多态的不同形态效果才能达到。
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
class A
{
public:
virtual void() {}
};虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)
vptr指针指向vtable
override关键字明确重写意图
virtual
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
核心概念
协变的条件
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:"<<_p<< endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为 ~B()中在释放资源。
只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象
纯虚函数:在基类中声明为virtual,并且没有实现的函数,形式为virtual void func() = 0;。
抽象类:包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被派生。
示例:
class Shape {
public:
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing Circle" << endl;
}
};Shape是一个抽象类,Circle是派生类,重写了draw()函数。
多态的使用:
Shape* shape = new Circle();
shape->draw(); // 输出:Drawing Circle在C++中,多态主要通过虚函数(virtual functions)和继承机制来实现。以下是多态实现的详细过程和原理:
class Base {
public:
virtual void func1() {}
protected:
int a = 1;
};
class Derive : public Base
{
public:
virtual void func1() {}
protected:
int b = 2;
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Base* p3 = &b;
printf("虚表地址:%p\n", *(int*)p3);
return 0;
}
//通过比较虚表地址,找到其最近的内存片段来确定在类中使用virtual关键字声明函数为虚函数。
Base为基类,Derived为继承类。
class Base {
public:
virtual void show() {
cout << "Base show()" << endl;
}
};
class Derived : public Base {
public:
void show() override { // override表示重写基类的虚函数
cout << "Derived show()" << endl;
}
};Base类的show()函数被声明为虚函数,Derived类重写了这个虚函数。创建对象:
Derived d;Derived类的对象d时,对象中会包含一个vptr,指向Derived类的vtable。Derived类的vtable中存储了Derived类重写的虚函数show()的地址。通过基类指针或引用调用虚函数:
Base* ptr = &d;
ptr->show();ptr是一个指向Base类的指针,但它指向的是Derived类的对象。ptr->show()时,程序会通过ptr找到对象的vptr,然后通过vptr找到Derived类的vtable,最终调用Derived类的show()函数。• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。