多态概念
对于同一个行为,对于不要的对象,有不同的表现
具体就是去完成某个行为,当不同的对象去完成时会产生不同的状态
通俗就是多种形态
多态分类
-
静态多态(静态绑定)
在程序编译结束后就已经明确了需要调用的函数
-
动态多态(动态绑定)
在运行时才确定具体需要调动的函数
绑定:奖变量和函数名转换成地址的过程
绑定作用:把不同的派生类对象都当作基类对象看,可以屏蔽不同子类对象之间的差异;提高程序的通用性来适应需求的不断变化
虚函数
目的:通过基类访问派生类定义的函数
语法:
- 使用virtual关键字声明的函数
- 非类的成员函数不能定义为虚函数
- 类的静态成员函数不能定义为虚函数
- 构造函数不能定义为虚函数
- 当将基类的某一成员函数声明成虚函数后,派生类的同名函数自动成为虚函数
class Person
{
public:
//虚函数
//virtual 在类中使用,类外定义不能加
virtual void getTicket()
{
cout << "100%票价" << endl;
}
};
class Student : public Person
{
public:
//继承之后也是虚函数
//override关键字明确表示该函数覆盖了基类的虚函数
//override只能在类中声明使用,类外定义不能加
void getTicket() override
{
cout << "80% 票价" << endl;
}
};
说明符final 与 override
final:
功能:指定某个虚函数不能在子类中被重写,或者某个类不能被子类继承
使用方法:
-
在类上使用:希望某个类不能被其他类继承
class Base final { public: void show() { cout << "Base class" << endl; } }; //编译出错,因为Base是final,不能被继承 //class Derived : public Base{}; -
在类上使用:希望某个虚函数不能在派生类中被重写
class Base { public: virtual void show() final { cout << "Bash show" << endl; } }; class Derived : public Base { public: //会编译报错,因为show在Base中被标记未funal,不能被重写 //void show() override //{} };
使用场景:final关键字主要用于那些你不希望被进一步修改或扩展的类或成员函数
override
功能:明确指示一个成员函数覆盖(override)基类中的虚函数。有助于提高代码的可读性和安全性,因为可以避免在子类中意外地隐藏基类中的虚函数
使用:
class Base
{
public:
virtual void show()
{
//基类
cout << "Base show" << endl;
}
};
class Derived : public Base
{
public:
//使用override关键字:覆盖Base中的show()
void show() override
{
//派生类实现
cout << "Derived show" << endl;
}
};
注意点:
如果在基类中的函数不是虚函数,即使在派生类中使用override关键字,编译器也不会报错。这是因为C++允许非虚函数的隐藏(hiding),而不是覆盖。为了避免这种情况,应确保基类中的函数是虚函数
动态多态
实现:
- 创建两个类,并且是继承关系
- 基类中的函数声明未virtual函数,也就是虚函数
- 派生类继承基类并重写基类中的虚函数
- 通过基类的指针或者引用访问派生类的对象
例外:
-
析构
如果不构成多态,那么指针什么类型就会调用什么类型的析构函数,这也就导致:如果派生类的析构函数中有资源要释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题
-
协变
重写的虚函数,返回值可以不同,但是返回值必须是父、子类的指针或引用类型, 即为协变
#include <iostream>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person constructor" << endl;
}
virtual ~Person()
{
cout << "Person destructor" << endl;
}
//虚函数
//virtual 在类中使用,类外定义不能加
virtual void getTicket();
};
class Student : public Person
{
public:
Student()
{
cout << "Student constructor" << endl;
}
~Student()
{
cout << "Student destructor" << endl;
}
//继承之后也是虚函数
//override关键字明确表示该函数覆盖了基类的虚函数
//override只能在类中声明使用,类外定义不能加
void getTicket() override;
};
void Person::getTicket()
{
cout << "100%票价" << endl;
}
void Student::getTicket()
{
cout << "80%票价" << endl;
}
int main(int argc, const char *argv[])
{
Person p;
p.getTicket();
cout << "-----------" << endl;
Student s;
s->getTicket();
cout << "-----------" << endl;
//通过基类的指针访问派生类对象:调用的是派生类的getTicket()函数
Person *pp = &s;
pp->getTicket();
cout << "-----------" << endl;
//通过基类的引用访问派生类对象:调用的是派生类的getTicket()函数
Person &rs = s;
rs.getTicket();
return 0;
}
成员函数的覆盖(重写)
覆盖指的是在派生类中重新定义基类中已经定义的函数。
在派生类中,使用相同的函数名、参数列表和返回类型来重新定义基类的函数
这样做可以使派生类对象在调用这个函数的时候执行自己的版本
特点:
- 不同的作用域(分别位于派生类与基类)
- 函数名相同
- 参数相同
- 返回值相同
- 基类必须有virtual关键字,不能有static
- 重写函数的权限访问限定符可以不同
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func()
{
cout << "Base func" << endl;
}
};
class Derived : public Base
{
public:
//重写
void func() override
{
cout << "Derived func" << endl;
}
};
int main(int argc, const char *argv[])
{
Base *ptr = new Derived();
ptr->func();
return 0;
}
成员函数的隐藏
隐藏是指当派生类中的函数与基类中的函数名称相同,但参数列表不同(不论是否使用virtual),或者即使参数列表相同但基类中的函数没有被声明为虚函数时,派生类中的函数会隐藏(Hide)所有基类中同名的函数
特点:
- 基类与派生类有同名函数,调用的时候总是调用子类的函数,此时父类成员函数被隐藏
- 不在同一作用域(分别位于派生类与基类)
- 函数名相同
- 返回值可相同可不同
- 参数不同时,无论有无virtual关键字,基类函数将被隐藏(注意与重载的区别)
- 参数相同时,但基类没有virtual关键字,基类的函数将被隐藏(注意与覆盖的区别)
#include <iostream>
using namespace std;
class Base
{
public:
void func()
{
cout << "Base func" << endl;
}
};
class Dervied : public Base
{
public:
//隐藏了Base中的func函数
void func()
{
cout << "Dervied func" << endl;
}
};
int main(int argc, const char *argv[])
{
Dervied obj;
obj.func();
return 0;
}
成员函数的重载
重载是指在同一作用域内,使用相同的函数名,但是参数列表不同的多个函数
C++允许通过参数列表的不同来定义多个同名函数,这样可以根据传入的参数类型或数量来决定调用哪个函数
特点:
- 同一个作用域
- 函数名相同
- 返回值可以相同也可以不同
- 参数列表不同
#include <iostream>
using namespace std;
class Base
{
public:
void func()
{
cout << "Base func" << endl;
}
};
class Dervied : public Base
{
public:
//隐藏了Base中的func函数
void func()
{
cout << "Dervied no arg func" << endl;
}
//重载了Dervied类中的func函数
void func(int a)
{
cout << "Dervied one arg func" << endl;
}
};
int main(int argc, const char *argv[])
{
Dervied obj;
obj.func();
return 0;
}
重写与隐藏的区别
子类对象:子类对象在调用的时候,总是调用子类实现的成员函数
父类对象:
重写:用父类的指针或引用指向子类对象的时候,覆盖的情况下,父类指针或引用调用的函数是子类实现的函数
隐藏:用父类的指针或引用指向子类对象的时候,隐藏的情况下,父类指针或引用调用的函数是父类实现的函数
多态原理
C++中多态的核心原理是通过虚函数表(vtable) 和虚函数指针(vptr) 实现的动态绑定机制。这种机制允许在运行使根据对象的实际类型调用正确的函数实现
虚函数表(vtable)
每个包含虚函数的类(或继承自包含虚函数的类)在编译时会生成一个虚函数表
该表是一个函数指针数组,储存该类中所有虚函数的实际地址(包括继承和重写的虚函数)
虚函数指针(vptr)
每个对象在内存中会包含一个隐藏的指针(vptr),指向其所属类的虚函数表
vptr的初始化:在对象构造时,有构造函数隐式设置
单继承:
class Base {
public:
virtual void func1() {}
virtual void func2() {}
int a;
};
class Derived : public Base {
public:
void func1() override {} // 重写 func1
virtual void func3() {} // 新增虚函数
int b;
};
内存布局示例
Base对象:
----------------- -----------------
| vptr(Base) | --> | &Base::fun1 |
| int a | | &Base::fun2 |
----------------- -----------------
Derived对象:
----------------- ---------------------
| vptr(Deriver) | --> | &Derived::fun1 |
| int a | | &Base::fun2 |
| int b | | &Dervied::fun2 |
----------------- ---------------------
Derived的虚函数表包含重写的fun1、继承的fun2和新增的fun3
Derived对象中的vptr指向自己的虚函数表
动态绑定过程
当通过基类指针或引用调用虚函数时,编译器生成代码实现动态绑定:
- 通过对象的vptr找到对应的虚函数表
- 根据函数在虚函数表中的偏移量(由编译时确定)找到函数地址
- 调用该地址对应的函数
Base *obj = new Derived();
obj->func1();//实际调用Derived::func1()
obj的vptr指向Derived的虚函数表
虚函数表中func1的偏移量为0,对应Derived::func1的地址
多继承下的虚函数表
对于多继承,虚函数表的组指更为复杂
每个基类可能拥有独立的虚函数表,派生类会合并这些表,并处理可能的冲突
class Base1 {
public:
virtual void f1() {}
};
class Base2 {
public:
virtual void f2() {}
};
class Derived : public Base1, public Base2 {
public:
void f1() override {} // 重写 Base1::f1
virtual void f3() {} // 新增虚函数
};
对象内存布局
Derived 对象:
------------------ -------------------
| vptr(Base1) | --> | &Deriver::f1 | //Base1 的虚函数表
| Base1 members | -------------------
------------------
| vptr(Base2) | --> | &Base2::f2 | //Base2 的虚函数表
| Base2 members | -------------------
| Derived members|
------------------
Deriver对象包含两个vptr,分别对应Base1和Base2的虚函数表
Deriver::f1替换Base1虚函数表中的f1
Deriver::f3可能添加到Base1或Base2的虚函数表中,具体由编译器决定
虚函数表的实现细节
-
虚函数表的创建时机
编译时:编译器为每个包含虚函数的类生成虚函数表,存储在程序的只读数据段(.rodata)
运行时:对象构造时,构造函数负责初始化vptr指向正确的虚函数表
-
虚函数表的内容
虚函数表中按声明顺序存储虚函数地址
若派生类未重写某个虚函数,表中保留基类函数的地址
-
虚函数的调用代价
直接调用开销:需通过vptr查找虚函数表,再通过偏移量获取函数地址
缓存不友好:虚函数可能分散在内存中,导致缓存未命中
虚函数与普通成员函数的区别
| 特性 | 虚函数 | 普通成员函数 |
|---|---|---|
| 调用方法 | 动态绑定(通过vptr查询) | 静态绑定(直接地址调用) |
| 内存开销 | 每个对象多一个vptr | 无额外开销 |
| 性能 | 略低(间接调用) | 更高 |
| 覆盖行为 | 可被派生类覆盖(override) | 可隐藏(hide),不可覆盖 |
纯虚函数(Pure Virtual Function)
纯虚函数是一种特殊的虚函数,他在基类中声明但不提供实现,强制要求派生类必须提高
语法:
virtual 返回类型 函数名(参数) = 0;
特征:
- 纯虚函数没有函数体,仅提供接口声明
- 派生类必须实现所有纯虚函数,否则派生类也会成为抽象类
抽象类(Abstract Class)
抽象类是包含至少一个纯虚函数的类
特征:
- 不能直接实例化,只能通过派生类创建对象
- 用于定义接口规范,强制派生类实现特定行为
抽象类的内存布局与虚函数表
class Base1 {
public:
virtual void f1() = 0; // 纯虚函数
};
class Base2 {
public:
virtual void f2() = 0; // 纯虚函数
};
class Derived : public Base1, public Base2 {
public:
void f1() override {} // 实现 Base1 的纯虚函数
void f2() override {} // 实现 Base2 的纯虚函数
};
对象内存布局
Derived 对象:
------------------ -------------------
| vptr(Base1) | --> | &Deriver::f1 | // 实现纯虚函数
| Base1 members | -------------------
------------------
| vptr(Base2) | --> | &Dervied::f2 | // 实现纯虚函数
| Base2 members | -------------------
| Derived members|
------------------
纯虚函数在虚函数表中的表现:
若基类的虚函数是纯虚函数,派生类必须提供实现,否则派生类仍为抽象类
虚函数条目:
纯虚函数在基类虚函数表中通常指向一个占位符(如编译器生的错误处理函数),直到派生类覆盖它
抽象类的核心规则
-
无法实例化
-
派生类必须实现所有纯虚函数
-
抽象类可以由非虚函数和成员变量
-
抽象类的构造函数和析构函数
构造函数:可以定义,用于初始化成员变量
析构函数:必须声明为虚函数,确保正确释放资源
抽象类的应用场景
- 定义接口规范
- 多态容器
- 模板方法模式
纯虚函数 vs 普通虚函数
| 特征 | 纯虚函数 | 普通虚函数 |
|---|---|---|
| 实现要求 | 派生类必须覆盖 | 派生类可选覆盖 |
| 基类是否提供实现 | 否(C++11后允许提供默认实现) | 是 |
| 类是否可实例化 | 包含纯虚函数的类是抽象类 | 可以实例化 |
| 典型用途 | 强制接口规范 | 提供默认行为,允许派生类扩展 |
1388



被折叠的 条评论
为什么被折叠?



