目录
多态概念
多态(Polymorphism) 是 C++ 面向对象编程(OOP)的三大特性之一(封装、继承、多态)。多态指的是 相同的接口(函数名)在不同的类中表现出不同的行为。简单来说就是一个事物作用于不同的对象,实现的功能状态不一样。
✅ 多态的核心思想
父类指针(或引用)指向子类对象,调用子类的重写方法。
C++ 多态的两种类型
C++ 支持 两种多态:
- 编译时多态(静态多态 )
- 函数重载 (Function Overloading)
- 运算符重载 (Operator Overloading)
- 模板 (Templates)
- 绑定时机:编译期
- 运行时多态(动态多态 )
- 虚函数 (Virtual Functions)
- 抽象类 (Abstract Class)
- 绑定时机:运行期(通过 vtable 机制)
实现动态多态的特点和要求💥💥
1:必须要有继承,没有继承,就没有多态(父类的指针/引用可以指向不同的子类对象)
2:子类必须要重写父类的同名方法
3:父类的同名方法必须定义成虚函数
注意❗❗:父类的同名方法定义成了虚函数,所有子类中同名的方法全部都默认是虚函数
(子类加不加virtual都行)
-------------------------------------------------------------------------------
语法
virtual 返回值 函数名(参数)
{
}
动态多态应用场景举例
#include <iostream>
using namespace std;
// 基类,武器是什么武器根本不知道! 所以该武器的攻击是一种虚拟的行为
class wuqi
{
public:
// 武器攻击的虚方法 (虚构出来的)
virtual void pk()
{
cout << "不知道啥武器战斗" << endl;
}
};
// 实例化武器 -> 弓箭
class BowAndArrow : public wuqi
{
public:
virtual void pk() // 覆盖基类的攻击方法
{
cout << "弓箭发动会心一击!造成伤害 99999" << endl;
}
};
// 实例化武器 -> 魔法书
class MagicBook : public wuqi
{
public:
virtual void pk() // 覆盖基类的攻击方法
{
cout << "魔法书发动大火球,造成灼伤效果,每秒扣除 1000" << endl;
}
};
// 实例化武器 -> 魔法书
class LightSword : public wuqi
{
public:
virtual void pk() // 覆盖基类的攻击方法
{
cout << "光剑发动,旋风斩,群体伤害 88888" << endl;
}
};
// 创建一个角色
class role
{
public:
void attck(wuqi *p) // 角色的攻击接口,实现攻击的多态性
{
p->pk(); // 用武器战斗
}
};
int main()
{
// 创建一个角色
role r;
// 一个攻击函数,作用于不同的武器对象,攻击效果不一样!
r.attck(new BowAndArrow);
r.attck(new MagicBook);
r.attck(new LightSword);
}
多态(虚函数)底层原理
C++ 的 运行时多态(动态多态) 是通过 虚函数表(vtable) 和 虚指针(vptr) 来实现的。虚函数表是一种用于 动态绑定 的内部数据结构,它可以让 父类指针调用子类的方法。
什么是 vtable 虚函数表?
当一个类包含虚函数(virtual
关键字),编译器会为它创建一个虚函数表(vtable)。这个表包含了指向该类虚函数的指针。
- 每个包含虚函数的类都有一个
vtable
- 每个对象都有一个
vptr
(虚指针)指向vtable
- 调用虚函数时,程序会通过
vptr
查找vtable
,然后调用正确的函数
证明虚指针存在
#include <iostream>
using namespace std;
class Base {
public:
virtual void show()
{
cout << "Base::show()" << endl;
}
private:
int a;
};
class Derived : public Base
{
public:
void show() override
{
cout << "Derived::show()" << endl;
}
private:
double b;
};
int main()
{
cout << sizeof(Base) << endl; // 16
cout << sizeof(Derived) << endl; // 24
return 0;
}
C++隐藏-重载-重写区别👍👍
重写(覆盖,复写)
重写必须满足 3 个核心条件:
条件 | 说明 | 示例 |
---|---|---|
1. 基类函数必须是 virtual | 只有基类虚函数才能被重写,否则是隐藏 | ✅ virtual void func(); |
2. 子类函数的签名必须完全相同 | 函数名、返回类型、参数列表必须一致 | ❌ void func(int) (参数不同,变成重载) |
3. 访问权限必须 兼容 | 子类方法的访问权限不能低于基类方法 | ✅ public → public ❌ public → private |
#include <iostream>
using namespace std;
class Animal {
public:
virtual void eat() { // ✅ 虚函数(必须有 virtual 关键字)
cout << "Animal is eating" << endl;
}
};
class Cat : public Animal {
public:
void eat() override { // ✅ 重写(override)
cout << "Cat is eating fish" << endl;
}
};
int main() {
Animal* a = new Cat(); // ✅ 基类指针指向子类对象
a->eat(); // 🔥 调用 Cat::eat(),而不是 Animal::eat()
a->Animal::eat(); // ✅ 调用基类函数
delete a;
return 0;
}
隐藏
子类和父类的函数名相同
- 如果派生类函数与基类函数同名,但参数不同,无论基类函数前是否有virtual修饰,基类函数被隐藏.
- 如果派生类函数与基类函数同名,参数也相同(不关心返回值类型),但是基类函数前无virtual修饰,基类函数被隐藏。
#include <iostream>
using namespace std;
class Base {
public:
void show() { // 基类方法
cout << "Base::show()" << endl;
}
};
class Derived : public Base {
public:
void show(int x) { // ❌ 这是 "隐藏",不是 "重写"
cout << "Derived::show(" << x << ")" << endl;
}
};
int main() {
Derived d;
d.show(10); // ✅ 调用 Derived::show(int)
// d.show(); // ❌ 编译错误!Base::show() 被隐藏,无法访问
d.Base::show(); // ✅ 仍然可以手动访问基类的方法
Base* p = &d; // ✅利用基类指针,显示隐藏后的接口
p->show();
return 0;
}
注意:❗❗
- 子类
void show(int x)
前面加上virtual
修饰也还是隐藏,因为基类和派生类的参数不同,重写要求参数一致 - 子类
void show()
也是隐藏,因为父类前面必须加上virtual
修饰
重载
当一个函数具有多个相同的函数名,不同的参数列表,不关心返回值,且要在同一个类内,这就是函数重载
#include <iostream>
using namespace std;
class OverloadExample {
public:
void show() { // ❶ 第一个 show()
cout << "show() without parameters" << endl;
}
void show(int x) { // ❷ 第二个 show()
cout << "show(int x): " << x << endl;
}
void show(double y) { // ❸ 第三个 show()
cout << "show(double y): " << y << endl;
}
};
int main() {
OverloadExample obj;
obj.show(); // ✅ 调用 show()
obj.show(10); // ✅ 调用 show(int)
obj.show(3.14); // ✅ 调用 show(double)
return 0;
}
父类的同名函数是虚函数(virtual
)和普通函数(非虚函数)的区别
在 C++ 中,函数的调用方式 取决于它是 普通函数 还是 虚函数(virtual
)。
- 普通函数(非虚函数) → 静态联编(Static Binding)
- 虚函数(
virtual
) → 动态联编(Dynamic Binding)
静态联编 vs 动态联编
静态联编(Static Binding)
- 发生在 编译阶段,编译器在编译时就决定调用哪个函数。
- 函数的调用取决于“左侧变量的类型”(即“指针/引用的静态类型”),而不是右侧对象的实际类型。
- 普通(非
virtual
)成员函数采用静态联编。
#include <iostream>
using namespace std;
class Animal {
public:
void eat() // ❌ 不是虚函数,采用静态联编
{
cout << "动物吃食物" << endl;
}
};
class Cat : public Animal {
public:
void eat() // ❌ 这不是重写,而是隐藏(Hiding)
{
cout << "猫吃鱼" << endl;
}
};
int main()
{
Animal a;
Animal *animalPtr;
Cat c;
Cat *catPtr;
// 1️⃣ 父类指针指向父类对象
animalPtr = &a;
animalPtr->eat(); // ✅ 调用 Animal::eat()
//Animal &animalRef1 = a; // 换成引用也是一样的
//animalRef1.eat();
// 2️⃣ 父类指针指向子类对象(不需要强制转换)
animalPtr = &c;
animalPtr->eat(); // ❌ 调用 Animal::eat(),不会调用 Cat::eat()
// 3️⃣ 子类指针指向父类对象(⚠️ 需要强制转换)
catPtr = (Cat *)&a;
catPtr->eat(); // ❌ 可能导致未定义行为!
//Cat &catRef1 = (Cat &)a; // 换成引用也是一样的
//catRef1.eat();
// 4️⃣ 子类指针指向子类对象
catPtr = &c;
catPtr->eat(); // ✅ 调用 Cat::eat()
return 0;
}
动态联编(Dynamic Binding)
- 发生在 运行时,编译器会在运行时确定调用哪个函数。
- 函数的调用取决于“右侧对象的实际类型”(即“赋值给指针/引用的对象类型”)。
virtual
成员函数采用动态联编,它依赖于虚表(vtable) 机制。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void eat() // ✅ 变成虚函数,采用动态联编
{
cout << "动物吃食物" << endl;
}
};
class Cat : public Animal {
public:
void eat() override // ✅ 正确重写(Override)
{
cout << "猫吃鱼" << endl;
}
};
int main()
{
Animal a;
Animal *animalPtr;
Cat c;
Cat *catPtr;
// 1️⃣ 父类指针指向父类对象
animalPtr = &a;
animalPtr->eat(); // ✅ 调用 Animal::eat()
// 2️⃣ 父类指针指向子类对象(不需要强制转换)
animalPtr = &c;
animalPtr->eat(); // ✅ 调用 Cat::eat()(动态绑定)
// 3️⃣ 错误情况(父类对象尝试转换为子类指针)
catPtr = dynamic_cast<Cat*>(&a);
if (catPtr) {
catPtr->eat(); // ❌ 这个不会执行,因为 catPtr 是 nullptr
} else {
cout << "转换失败,catPtr 为空" << endl;
}
// 4️⃣ 子类指针指向子类对象
catPtr = &c;
catPtr->eat(); // ✅ 调用 Cat::eat()
return 0;
}
虚析构
问题: 父类的指针指向子类对象的时候,如果delete释放父类的指针,那么正常情况下只会调用父类的析构函数,不会调用子类的析构函数(释放不彻底)
虚析构作用: 把基类与派生类的所有析构函数都放入虚表,这样基类和派生类的析构都会执行。
虚析构原理:
不加virtual,此时采用静态联编(只调用赋值运算左边的类(父类)析构函数)
Animal *p=c1;
delete p;
加virtual,此时采用动态联编(依据赋值运算右边的类型(子类),先调用子类析构,再调用父类析构)
Animal *p=c1;
delete p;
语法:
virtual ~析构函数 //写了虚析构就不能同时再去写普通析构
{
}
示例:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void eat() { // ✅ 虚函数
cout << "动物吃食物" << endl;
}
virtual ~Animal() { // ✅ 变成虚析构
cout << "Animal 虚析构函数" << endl;
}
};
class Cat : public Animal {
public:
void eat() override {
cout << "猫吃鱼" << endl;
}
~Cat() {
cout << "Cat 析构函数" << endl;
}
};
int main() {
Animal* animalPtr = new Cat();
animalPtr->eat(); // ✅ 调用 Cat::eat()
delete animalPtr; // ✅ 现在会先调用 Cat::~Cat(),再调用 Animal::~Animal()
return 0;
}
纯虚函数和抽象类
纯虚函数
概念: 纯虚函数是基类中不能直接实现的函数,它的目的是让子类必须重写这个函数,否则子类就会变成抽象类,无法实例化。
语法:
virtual 返回类型 函数名(参数) = 0; //空架子,没有函数体,也不能有函数体
其中,= 0 表示这是一个纯虚函数,在基类中不会提供实现,必须由子类重写
抽象类(abstract base class 抽象基类)
👉 定义
1.抽象类是至少包含一个纯虚函数的类。
2.不能直接创建对象(实例化),只能作为基类被继承。
3.子类继承了抽象类,子类必须把抽象类中所有的纯虚函数都实现,
如果有任何一个纯虚函数没有实现,那么子类依然是抽象类。
4.子类实现抽象类的纯虚函数,必须同名同参,同返回值。
5.作用:提供接口,让子类实现具体行为。
示例:
#include <iostream>
using namespace std;
// 🐾 1. 抽象类(含有纯虚函数)
class Animal {
public:
virtual void makeSound() = 0; // ✅ 纯虚函数,子类必须实现
virtual ~Animal() {} // ✅ 虚析构,防止内存泄漏
};
// 🐱 2. 具体子类(继承抽象类)
class Cat : public Animal {
public:
void makeSound() override { // ✅ 必须实现纯虚函数
cout << "喵喵喵!" << endl;
}
};
// 🐶 3. 另一个子类
class Dog : public Animal {
public:
void makeSound() override { // ✅ 必须实现纯虚函数
cout << "汪汪汪!" << endl;
}
};
int main() {
// ❌ 不能创建抽象类对象
// Animal a; // 错误:抽象类不能实例化
// ✅ 但可以用基类指针指向子类对象(多态)
Animal* cat = new Cat();
Animal* dog = new Dog();
cat->makeSound(); // 输出:喵喵喵!
dog->makeSound(); // 输出:汪汪汪!
delete cat;
delete dog;
return 0;
}
总结