深入解析C++多态机制
目录
一.多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。
1. 静态多态(编译时多态)
- 概念:在编译阶段确定函数调用,通过函数重载或模板实现。
- 特征:
- 编译器根据参数类型、数量或顺序匹配具体函数。
- 无运行时开销,性能高。
- 典型应用:函数重载、运算符重载、模板。
2. 动态多态(运行时多态)
- 概念:在运行时根据对象实际类型确定函数调用,通过虚函数和继承实现。
- 特征:
- 需满足三条件:继承关系、虚函数声明、基类指针/引用指向派生类对象。
- 通过虚函数表(vtable)实现动态绑定,有轻微运行时开销。
- 典型应用:接口抽象、行为扩展。
二.定义和实现多态
多态是继承关系下的类,去调用同一个函数,产生不同的行为,比如狗(dog)继承了动物(animal)叫声
1.实现多态还有两个必须重要条件:
•必须是基类的指针或者引⽤调⽤虚函数
• 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。
注意:如果派生类没有virtual,基类有virtual也构成多态不推荐使用。
如果俩个都没有就是重定义(不是多态)派生类会隐藏基类。
class animal
{
public:
virtual void call()
{
std::cout << "动物叫声" << std::endl;
}
};
class dog : public animal
{
public:
virtual void call()
{
std::cout << "汪汪" << std::endl;
}
};
void func(animal& A)
{
A.call();
}
int main()
{
animal a;
dog d;
func(a);//动物叫声
func(d);//汪汪
return 0;
}
2.虚函数
- 虚函数:用
virtual关键字修饰的类成员函数 - 核心目的:实现运行时多态(动态绑定),允许基类指针/引用调用派生类的重写函数
3.析构函数重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写。
如果析构函数不加virtual的话可能会产生析构。下面代码如果没有加,b会产生内存泄漏,~B不会运行。
class A
{
public:
virtual ~A()
{
std::cout << "~A()" << std::endl;
}
};
class B : public A
{
public:
virtual ~B()
{
std::cout << "~B()" << std::endl;
}
};
int main()
{
A* a = new A;
A* b = new B;
delete a;// ~A()
delete b;// ~B() + ~A()
return 0;
}
4.协变(了解即可)
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。
class Base {};
class Derived : public Base {};
class Creator {
public:
virtual Base* create() { // 基类返回 Base*
return new Base();
}
};
class DerivedCreator : public Creator {
public:
virtual Derived* create() override { // 协变返回:Derived*
return new Derived();
}
};
三.override和final关键字
1.override
- 编译器检查:确保函数签名与基类虚函数完全匹配
- 防止意外隐藏:检测拼写错误/参数类型错误
- 代码可读性:显式表达设计意图
class animal { public: virtual void call() { std::cout << "动物叫声" << std::endl; } int a; }; class dog : public animal { public: virtual void call() override //明确重写关系 { std::cout << "汪汪" << std::endl; } int b; };2.final
- 安全加固:防止关键功能被意外修改
- 性能优化:提示编译器可去虚拟化(devirtualization)
- 接口锁定:在框架设计中明确终止扩展点
class animal //如果放在这里就是不能被继承 { public: virtual void call() final//声明不能重写 { std::cout << "动物叫声" << std::endl; } int a; }; class dog : public animal { public: virtual void call()//这里报错 { std::cout << "汪汪" << std::endl; } int b; };四.重载/重写/隐藏的对⽐
-
注意:这个概念对⽐经常考,⼤家得理解记忆⼀下
| 特性 | 重载(Overload) | 重写/覆盖(Override) | 重定义/隐藏(Redefine) |
|---|---|---|---|
| 作用域 | 同一作用域 | 继承体系 | 继承体系 |
| 函数签名要求 | 参数列表必须不同 | 签名完全一致(协变返回除外) | 函数名相同,参数可不同 |
| virtual关键字 | 不需要 | 基类必须声明virtual |
不需要 |
| 多态类型 | 编译时多态(静态) | 运行时多态(动态) | 无多态(静态绑定) |
| 关键字支持 | 无 | override(C++11) |
无 |
| 基类函数可见性 | 不影响 | 不隐藏其他重载 | 隐藏基类所有同名函数 |
五.纯虚函数和抽象类
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象
class animal
{
public:
virtual void call() = 0;
int a;
};
class dog : public animal
{
public:
virtual void call()
{
std::cout << "汪汪" << std::endl;
}
int b;
};
class cat : public animal
{
public:
virtual void call()
{
std::cout << "喵喵" << std::endl;
}
};
void func(animal& A)
{
A.call();
}
int main()
{
animal c1;//这里抽象类不能实例化
cat* c2 = new animal;//这样子也不行
animal* c3 = new cat;//但是可以这样子
dog d;
cat c;
func(c);
func(d);
c3->call();//输出喵喵
delete c3;
delete c2;
return 0;
}
六.底层实现机制
class animal
{
public:
virtual void call()
{
std::cout << "动物叫声" << std::endl;
}
int a;
};
int main()
{
printf("%zu\n", sizeof(animal));//如果是x64环境是 8(因为这里有虚函数指针+int)
return 0;
}
1. 虚函数表(vtable)
- 每个包含虚函数的类都有一个隐藏的虚函数表
- 存储该类所有虚函数的函数指针
- 编译器自动生成,在编译期确定
2. 虚指针(vptr)
- 每个对象包含一个指向vtable的隐藏指针
- 位于对象内存布局首部(32位系统4字节,64位系统8字节)
- 构造函数中初始化,析构函数中销毁
#include <iostream>
class Base {
public:
virtual void display() {
std::cout << "Base::display()" << std::endl;
}
};
class Derived : public Base {
public:
void display() override {
std::cout << "Derived::display()" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->display(); // 运行时动态绑定
delete ptr;
return 0;
}
3.底层细节
-
- 当创建
Derived对象时,对象内部会有一个虚指针vptr,它指向Derived类的虚函数表。 - 当通过基类指针
ptr调用display函数时,编译器会通过ptr找到对象的虚指针vptr。 - 然后通过
vptr找到Derived类的虚函数表。 - 在虚函数表中找到
display函数的地址,并调用该函数,从而实现了动态绑定。
- 当创建
4.多继承情况下的虚函数表
在多继承的情况下,每个派生类可能会有多个虚函数表,分别对应不同的基类。派生类对象的虚指针会根据基类的顺序指向不同的虚函数表,具体的实现会更加复杂,但基本原理仍然是通过虚函数表和虚指针来实现动态绑定。
总之,C++ 的静态多态在编译阶段确定函数调用,而动态多态在运行时根据对象的实际类型确定函数调用,通过虚函数表和虚指针实现了灵活的多态性。
更多推荐
所有评论(0)