一、多态的概念
通俗来说,多态就是多种形态。分为编译时多态(静态多态)和运行时多态(动态多态)。这篇文章主要讲述运行时多态。
编译时多态(静态多态)主要是函数重载与函数模板,传不同类型的参数就可以调用不同的函数,通过参数达到多种形态,之所以叫做编译时多态是因为实参传给形参的参数匹配是在编译时完成的。
运行时多态,具体来说就是完成某个行为。可以传不同的对象完成不同的行为达到多种形态。比如买票这个行为,当普通人买票时是全价买票;学生买票可以优惠买学生票;特殊职业买票可以优先买票。再比如同样是动物叫的一个行为(函数),传猫对象得到的就是"喵喵",传狗对象过去就是"汪汪"。
二、多态的定义与实现
2.1 多态的构成条件
多态是一个继承关系下的类对象调用同一个函数产生不同的行为。比如Student 继承了Person ,Person 对象全价买票,Student对象优惠买票。
实现多态的两个必须条件:
- 必须是基类的指针或者引用调用。
- 被调用的函数必须是虚函数 并且完成虚函数重写/覆盖。
2.2 虚函数
类成员函数前面加virtual
关键字修饰,这个成员函数被称为虚函数。
cpp
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
2.3 虚函数的重写/覆盖
派生类中有一个和基类完全相同的虚函数(派生类虚函数与基类虚函数的返回类型、函数名字、参数列表完全相同),即为派生类的虚函数重写了基类的虚函数。
cpp
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
// 重写
virtual void BuyTicket() { cout << "买票-打折 " << endl; }
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
cpp
class Animal
{
public:
virtual void talk() const {};
};
class Dog :public Animal
{
public:
virtual void talk() const
{
cout << "汪汪" << endl;
}
};
class Cat :public Animal
{
public:
virtual void talk() const
{
cout << "喵喵" << endl;
}
};
void func(const Animal& a)
{
a.talk();
}
int main()
{
Dog d;
Cat c;
func(d);
func(c);
return 0;
}
2.4 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时叫做协变。协变的实际意义并不大,了解一下即可。
cpp
class Animal {
public:
virtual Animal* clone() const {
return new Animal(*this);
}
};
class Dog : public Animal {
public:
// 协变:返回类型是 Dog*(Animal* 的派生类指针)
virtual Dog* clone() const override {
return new Dog(*this);
}
};
Dog::clone()
返回Dog*
,而基类Animal::clone()
返回Animal*
。- 协变允许这种返回类型的"窄化",因为
Dog*
可以隐式转换为Animal*
。
2.5 析构函数的重写
当基类的析构函数为虚函数,此时派生类析构函数只需定义,无论是否加virtual 关键字,都可与基类的析构函数构成重写,这是因为在上一篇继承文章中提到过,实际上编译器对析构函数名称做了特殊处理,编译后析构函数名称统一处理为destructor ,所以只要基类的析构函数加了virtual修饰,派生类的析构函数就构成重写。
下面代码中可以看到,如果~A()
不加virtual
,那么delete p2
时只调用A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题。
cpp
class A
{
public:
virtual ~A()
{
// 基类虚析构函数
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()"<< 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;
}
2.6 override 和 final
在有些情况下由于疏忽,比如函数名写错参数写错等无法构成重写,并且这种错误编译期间不会报出,只是在程序运行时没有得到期望结果,因此C++11 提供了override,可以帮助检测是否重写。
如果我们不想让派生类重写这个虚函数,就可以用final去修饰。
cpp
class Car
{
public:
virtual void Driver() {}
};
class Benz :public Car
{
public:
virtual void Driver()override { cout << "Benz" << endl; }
};
int main()
{
return 0;
}
cpp
class Car
{
public:
virtual void Driver() final {}
};
class Benz :public Car
{
public:
virtual void Driver() { cout << "Benz" << endl; }
};
int main()
{
return 0;
}

2.7 重载/重写/隐藏的对比

三、纯虚函数和抽象类
在虚函数后加上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,无法实例化处对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。某种程度上强制派生类重写虚函数,因为不重写实例化不出对象。
cpp
class Car
{
public:
virtual void Driver() = 0;
};
class Benz :public Car
{
public:
virtual void Driver() { cout << "Benz" << endl; }
};
四、多态的原理
C++ 多态的底层实现主要依赖于 虚函数表(vtable) 和 虚函数指针(vptr) 的机制。这种机制在编译器和运行时共同协作下,实现了动态绑定,从而支持多态行为。
4.1 虚函数表
每个包含虚函数的类 都有一个对应的虚函数表(vtable)。
-
vtable 的作用:存储该类所有虚函数的实际入口地址(函数指针)。
- 若派生类未覆盖基类虚函数,则 vtable 中保留基类虚函数地址。
- 若派生类覆盖了基类虚函数,则 vtable 中替换为派生类的函数地址。
比如类A 和派生类 B
cpp
class A {
public:
virtual void func1() {} // 虚函数
virtual ~A() {} // 虚析构函数
};
class B : public A {
public:
void func1() override {} // 覆盖基类 func1
~B() override {} // 覆盖基类析构函数
};
类 A 的 vtable:
css
| A::func1() 地址 |
| A::~A() 地址 |
类 B 的 vtable:
arduino
| B::func1() 地址 | // 覆盖基类 func1
| B::~B() 地址 | // 覆盖基类析构函数
| A::~A() 地址 | // 基类析构函数(若有其他未覆盖的虚函数,也会保留)
4.2 虚函数指针
当一个类包含虚函数时,编译器会隐式地为该类的每个对象添加一个隐藏成员------虚函数指针(vptr) ,vptr 指向所属类的vtable。
css
| vptr | --> 指向 B 的 vtable
| A 的成员变量 |
| B 的成员变量 |
4.3 动态绑定的实现
当通过 基类指针或引用调用虚函数 时,编译器会生成以下步骤的代码:
- 通过对象的 vptr 找到 vtable。
- 从 vtable 中查找虚函数的地址(偏移量固定,由编译器决定)。
- 跳转到该地址执行函数。
cpp
A* obj = new B();
obj->func1(); // 动态绑定到 B::func1()
delete obj; // 动态绑定到 B::~B() 和 A::~A()
obj
指向 B
的实例,其 vptr 指向 B
的 vtable。调用 func1()
时,通过 vptr 找到 B
的 vtable,并调用 B::func1()
,析构时同理,先调用 B::~B()
,再调用 A::~A()
。