多态
通常说的多态,是指发生在类之间的多态。即相同的代码,实现不同的功能。
函数重载 ------ 静态多态/编译时多态。
类之间的多态 ------ 动态多态/运行时多态。
前提
继承、虚函数、函数重写。
函数重写(override)
在子类中重写父类的虚函数就是函数重写的过程,可以实现多态。
1、必须有继承关系;
2、父类中必须有虚函数。
虚函数(virtual)
只要基类中的某函数是虚函数,则所有继承自此基类的子类中该函数都是虚函数。
只有 普通成员函数 与 析构函数 可以声明为虚函数。
常规来说,给父类中的函数加上 virtual 关键字,定义成一个虚函数,那么在子类中,可以对父类的虚函数进行函数重写(override)。在 C++11中,可以在派生类的新覆盖的函数上使用一个关键字 override 来验证覆盖是否成功。
只要有虚函数的类,都会有一个虚函数表和一个虚(函数表)指针。
虚指针:指向虚函数表的指针;
虚函数表:存储所有的虚函数的信息(每个有虚函数的类都有专属的虚函数表)
虚函数表 :保存所有虚函数的入口地址,每一个包含虚函数的类都会有一张虚函数表。
如果发生继承关系,子类会先复制父类的虚函数表,如果子类对某个虚函数重写,就会更新虚函数表中该函数的入口地址。未被重写的虚函数,保持原样(和父类的虚函数一样)。
虚函数表指针:指向虚函数表的指针,父类中有一个虚函数表指针,子类中的虚函数表指针是从父类中继承下来的虚函数表指针,指向子类的虚函数表(虚函数表指针存在类中的第一个位置,占 4 字节)。
cpp
#include <iostream>
using namespace std;
class Father
{
public:
string secret;
virtual void act_1()
{
cout << "Earn money. " << endl;
}
};
class Son:public Father
{
public:
string secret;
void act_1() override // 覆盖基类中的虚函数,virtual 可写可不写,override 验证覆盖是否成功
{
cout << "Save money. " << endl;
}
};
int main()
{
Father f;
cout << &f << endl; // 类中存在虚函数时,起始位置存放虚指针
cout << &f.secret << endl; // 类中第一个成员的地址,偏移首地址 4 个字节
Son s;
cout << &s << endl; // 子类继承父类的虚指针(1 个)
cout << &s.secret << endl; // 类中第一个成员的地址,偏移首地址 8 个字节
return 0;
}
虚析构函数
问题:由于实现多态 需要使用父类的指针 指向 子类的空间,父类指针可以操作的空间只能是父类自己的那部分,所以,在 delete 父类指针时,并不会释放掉子类的空间。
解决方法:给基类(父类)的析构函数前面加上 virtual 关键字,只要基类是虚析构函数,后面继承的所有子类都是虚析构函数,虚析构函数会引导父类的指针释放掉子类的空间。
cpp
#include <iostream>
using namespace std;
class Father
{
string secret;
public:
virtual void act_1(); // 虚函数声明
virtual ~Father()
{
cout << "Destructor of father class. " << endl;
}
};
void Father::act_1() // 虚函数声明与定义分离时,virtual 只需要修饰在声明处
{
cout << "Earn money. " << endl;
}
class Son:public Father
{
string secret;
public:
void act_1() // 前面可以加 virtual,后面可以加 override
{
cout << "Save money. " << endl;
}
void act_2() // 这里不能加 virtual 和 override,此函数非虚函数
{
cout << "Inherit money. " << endl;
}
virtual ~Son() // 此 virtual 可以不加
{
cout << "Destructor of son class. " << endl;
// 父类的析构前,不加 virtual,则 delete p 时,此函数不执行
}
};
int main()
{
Father *p = new Son;
p->act_1();
p->Father::act_1();
delete p;
return 0;
}
多态的实现
多态可以理解为"一种接口,多种状态"。只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的使用时需要具有三个前提条件:
● 公有继承
● 函数覆盖
● 基类的 引用/指针 指向派生类对象
在代码运行时,通过对象的虚函数指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。
使用时会产生一些额外的开销,优点是代码的编写更加灵活高效,缺点是会降低代码的执行速度。
cpp
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat();
};
// 声明与定义分离。virtual 只需要修饰声明处
void Animal::eat()
{
cout << "动物爱吃坤柳。" << endl;
}
class Dog:public Animal
{
public:
// 覆盖基类中虚函数,派生类中 virtual 关键字可写可不写
void eat() override // 验证虚函数覆盖是否成功
{
cout << "狗吃骨头。" << endl;
}
};
class Cat:public Animal
{
public:
void eat() // override
{
cout << "猫吃鱼。" << endl;
}
};
void test_eat1(Animal &al) // 基类的引用指向派生类对象
{
al.eat();
}
void test_eat2(Animal *al) // 基类的指针指向派生类对象
{
al->eat();
}
int main()
{
Animal a1;
Dog d1;
Cat c1;
test_eat1(a1); // 动物爱吃坤柳,基类的引用指向了基类的对象
test_eat1(d1); // 狗吃骨头,基类的引用指向了 Dog类 的对象
test_eat1(c1); // 猫吃鱼,基类的引用指向了 Cat类 的对象
Animal *a2 = new Animal; // 基类的指针指向了基类的对象
Dog *d2 = new Dog;
Cat *c2 = new Cat;
test_eat2(a2); // 动物爱吃坤柳
test_eat2(d2); // 狗吃骨头
test_eat2(c2); // 猫吃鱼
return 0;
}
💡 练习
全局变量:int monster = 10000;
定义英雄类 hero,受保护的属性:string name,int hp,int attck;
公有的无参构造,有参构造,虚成员函数 void Atk(){blood-=0;};
法师类继承自英雄类,私有属性 int ap_atk=50;重写虚成员函数 void Atk(){blood-=(attck+ap_atk);};射手类继承自英雄类,私有属性 int ac_atk = 100;重写虚成员函数 void Atk(){blood-=(attck+ac_atk);}实例化类对象,判断怪物何时被杀死。
cpp
#include <iostream>
using namespace std;
int He_Yanwei = 10000;
class Hero
{
protected:
string name;
int hp;
int attack;
public:
Hero(string name, int hp, int attack):name(name), hp(hp), attack(attack) { }
virtual void atk()
{
He_Yanwei -= 0;
}
};
class Mages:public Hero
{
int ap_atk = 50;
public:
Mages():Hero("You_Changzhi", 10000000000, 1000) { }
Mages(string name, int hp, int attack, int ap_atk):Hero(name, hp, attack), ap_atk(ap_atk) { }
void atk()
{
He_Yanwei -= (attack + ap_atk);
cout << "-" << (attack + ap_atk) << endl;
}
};
class Shooters:public Hero
{
int ac_atk = 100;
public:
Shooters():Hero("Zou_Jinqi", 100000000, 1000) { }
Shooters(string name, int hp, int attack, int ac_atk):Hero(name, hp, attack), ac_atk(ac_atk) {}
void atk()
{
He_Yanwei -= (attack + ac_atk);
cout << "-" << (attack + ac_atk) << endl;
}
};
int main()
{
Mages mage("You_Changzhi", 10000000000, 1000, 50);
Shooters shooter("Zou_Jinqi", 100000000, 1000, 100);
int i = 1;
cout << "He_Yanwei: " << He_Yanwei << endl;
cout << "-----------------------" << endl;
while (He_Yanwei >= 0)
{
cout << "Round " << i++ << ": " << endl;
shooter.atk();
if (He_Yanwei <= 0)
break;
cout << "He_Yanwei: " << He_Yanwei << endl;
mage.atk();
if (He_Yanwei <= 0)
break;
cout << "He_Yanwei: " << He_Yanwei << endl;
shooter.atk();
if (He_Yanwei <= 0)
break;
cout << "He_Yanwei: " << He_Yanwei << endl;
cout << "-----------------------" << endl;
}
// cout << "-----------------------" << endl; // 加上就有问题,离谱
cout << "You killed He Yanwei! " << endl;
return 0;
}
纯虚函数和抽象类
纯虚函数
virtual 返回值 函数名(参数列表) = 0;
抽象类
包含纯虚函数的类就是抽象类,抽象类不允许实例化类对象。抽象类的析构函数必须是虚析构函数。
抽象类通常作为一个类型的模板(作为基类存在),不能实例化类对象(没有意义)。在继承后的子类中,加上具体的属性后,重写父类中的纯虚函数就可以实例化类对象了;如果子类中不重写父类的纯虚函数,那么子类也不能实例化类对象。
抽象类的子类重写纯虚函数:该子类不再是抽象类。(水果 ------> 苹果、香蕉、草莓...)
抽象类的子类不重写纯虚函数:该子类依然是抽象类。(水果 ------> 热带水果、温带水果...)
● 抽象类支持多态,可以存在引用或指针的声明格式。
● 因为抽象类的作用是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。
cpp
#include <iostream>
using namespace std;
class Test
{
public:
virtual void show() = 0; // show 函数是一个纯虚函数
};
class T:public Test
{
public:
void show()
{
cout << "T" << endl;
}
};
int main()
{
T t1; // 可以定义了;若不对 父类的纯虚函数 重写,则此行报错
return 0;
}
💡 练习
定义抽象类 Animal,私有成员 string name,string colour,公有纯虚函数 void sound(),
定义 Cat 类,继承自 Animal 类,重写 sound 函数,
定义 Dog 类,继承自 Animal 类,重写 sound 函数,实现多态现象的测试。
(定义一个全局函数,可以实现两个子类 sound 功能的测试,要求:全局函数只有一个参数)
cpp
#include <iostream>
using namespace std;
class Animal
{
string species;
string color;
public:
virtual void sound() = 0;
};
class Cat:public Animal
{
public:
void sound()
{
cout << "喵~喵~" << endl;
}
};
class Dog:public Animal
{
public:
void sound()
{
cout << "汪~汪~" << endl;
}
};
class Sheep:public Animal // 1、公有继承
{
public:
void sound() // 2、重写父类的虚函数
{
cout << "咩~咩~" << endl;
}
};
void choice(Animal *ani)
{
ani->sound(); // 3、基类的 指针或引用 指向 派生类对象
}
int main()
{
Cat c;
choice(&c);
Dog d;
choice(&d);
Sheep sh;
choice(&sh);
return 0;
}