17 多态
多态是 C++ 面向对象三大特性之一,它让同一接口可以表现出多种不同的形态,底层原理是虚函数。多态分两类:
- 静态多态:函数重载和运算符重载,属于编译阶段就确定的绑定,又称编译时多态(早绑定)
- 动态多态:派生类和虚函数实现,属于运行阶段才确定的绑定,又称运行时多态(晚绑定)
17.1 多态的基本概念
17.1.1 动态多态满足的条件
- 有继承关系
- 子类重写父类的虚函数(重写:函数返回值类型、函数名、参数列表完全相同)
使用场景:父类指针或引用指向子类对象
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream> using namespace std;
class Animal { public: virtual void speak() { cout << "动物在说话" << endl; } };
class Cat : public Animal { public: void speak() { cout << "小猫在说话" << endl; } };
void doSpeak(Animal& animal) { animal.speak(); }
int main() {
Cat cat; doSpeak(cat);
system("pause");
return 0; }
|
输出结果为「小猫在说话」。如果去掉 virtual 关键字,输出就会变成「动物在说话」——因为此时 animal.speak() 在编译阶段就被绑定到了 Animal::speak,属于静态绑定。
17.1.2 多态的原理
当类中声明了虚函数后,类内部会多出一个虚函数指针 vfptr,它指向一张虚函数表 vftable,表中记录了各个虚函数的入口地址。
- 子类重写父类虚函数后,子类虚函数表中记录的地址被替换为子类函数的地址
- 当父类指针或引用指向子类对象时,调用虚函数会通过表中的地址去调用,于是调用到了子类函数
示例:用 sizeof 验证虚函数表指针的存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> using namespace std;
class Animal { public: virtual void speak() { cout << "动物在说话" << endl; } };
class Cat : public Animal { public: void speak() { cout << "小猫在说话" << endl; } };
int main() {
cout << "sizeof(Animal) = " << sizeof(Animal) << endl; cout << "sizeof(Cat) = " << sizeof(Cat) << endl;
system("pause");
return 0; }
|
注意:示例中多了虚函数指针,空类原本只占 1 字节,所以判断类的大小时要把虚函数指针也算进去。
17.2 多态案例一:计算器类
分别使用普通写法和多态技术,设计两个操作数进行运算的计算器类。
- 普通写法:需要为每个运算写独立的类的成员函数,后期维护扩展困难
- 多态写法:先写一个抽象计算器基类,再派生出各运算类,后期新增运算只需加一个子类,符合开闭原则(对扩展开放,对修改关闭)
示例:普通写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream> using namespace std; #include <string>
class Calculator { public:
int getResult(string oper) { if (oper == "+") { return m_Num1 + m_Num2; } else if (oper == "-") { return m_Num1 - m_Num2; } else if (oper == "*") { return m_Num1 * m_Num2; } return 0; }
int m_Num1; int m_Num2; };
void test01() { Calculator c; c.m_Num1 = 10; c.m_Num2 = 10;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl; cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl; cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl; }
int main() {
test01();
system("pause");
return 0; }
|
示例:多态写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| #include <iostream> using namespace std; #include <string>
class AbstractCalculator { public:
virtual int getResult() { return 0; }
int m_Num1; int m_Num2; };
class AddCalculator : public AbstractCalculator { public:
int getResult() { return m_Num1 + m_Num2; } };
class SubCalculator : public AbstractCalculator { public:
int getResult() { return m_Num1 - m_Num2; } };
class MulCalculator : public AbstractCalculator { public:
int getResult() { return m_Num1 * m_Num2; } };
void test01() { AbstractCalculator* abc = new AddCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc;
abc = new SubCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc;
abc = new MulCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; }
int main() {
test01();
system("pause");
return 0; }
|
总结:多态的优点——组织结构清晰、可读性强、前期和后期扩展以及维护性高。
17.3 纯虚函数和抽象类
纯虚函数:在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。
语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <iostream> using namespace std;
class Base { public:
virtual void func() = 0; };
class Son : public Base { public:
void func() { cout << "Son 中 func 的调用" << endl; } };
void test01() {
Son s; s.func(); }
int main() {
test01();
system("pause");
return 0; }
|
17.4 多态案例二:制作饮品
制作饮品的流程大体为:煮水 → 冲泡 → 倒入杯中 → 加入辅料。
利用多态技术实现,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| #include <iostream> using namespace std;
class AbstractDrinking { public:
virtual void Boil() = 0;
virtual void Brew() = 0;
virtual void PourInCup() = 0;
virtual void PutSomething() = 0;
void MakeDrink() { Boil(); Brew(); PourInCup(); PutSomething(); } };
class Coffee : public AbstractDrinking { public:
void Boil() { cout << "煮农夫山泉" << endl; }
void Brew() { cout << "冲泡咖啡" << endl; }
void PourInCup() { cout << "倒入杯中" << endl; }
void PutSomething() { cout << "加入糖和牛奶" << endl; } };
class Tea : public AbstractDrinking { public:
void Boil() { cout << "煮矿泉水" << endl; }
void Brew() { cout << "冲泡茶叶" << endl; }
void PourInCup() { cout << "倒入杯中" << endl; }
void PutSomething() { cout << "加入枸杞" << endl; } };
void doWork(AbstractDrinking* abs) { abs->MakeDrink(); delete abs; }
void test01() { doWork(new Coffee);
cout << "----------------------" << endl;
doWork(new Tea); }
int main() {
test01();
system("pause");
return 0; }
|
17.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,会造成内存泄漏。
解决办法:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0;,且需要类外实现 类名::~类名(){}
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #include <iostream> using namespace std; #include <string>
class Animal { public:
Animal() { cout << "Animal 构造函数调用" << endl; }
virtual ~Animal() = 0; };
Animal::~Animal() { cout << "Animal 纯虚析构函数调用" << endl; }
class Cat : public Animal { public:
Cat(string name) { cout << "Cat 构造函数调用" << endl; m_Name = new string(name); }
~Cat() { if (m_Name != NULL) { cout << "Cat 析构函数调用" << endl; delete m_Name; m_Name = NULL; } }
string* m_Name; };
void test01() { Animal* animal = new Cat("Tom"); cout << *animal->m_Name << endl;
delete animal; }
int main() {
test01();
system("pause");
return 0; }
|
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象的问题
- 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类