目录
多态是C++面向对象三大特性之一;
多态分为两类:
静态多态:函数重载和运算符重载;
动态多态:派生类和虚函数实现运行时多态;
静态多态和动态多态的区别:
静态多态的函数地址早绑定-编译阶段确定函数地址;
动态多态的函数地址晚绑定-运行阶段确定函数地址;
我们一般说的多态指的是动态多态。
1,多态的基本语法
满足动态多态的条件:
(1),有继承关系;
(2),子类重写父类的虚函数;
动态多态的使用:
父类的指针或者引用,指向子类的对象。
注意:
虚函数是在函数前边加上virtual关键字,子类在重写父类虚函数时virtual关键字可写可不写;
函数的重写与函数重载不同,重写是函数返回值类型、函数名、参数列表完全一致。
如下代码中,父类Animal中有动物的叫声的成员函数,子类有Cat和Dog也分别有它们各自叫声的成员函数。
#include <iostream>
using namespace std;
#include <string>
class Animal
{
public:
virtual void Speak()
{
cout << "Animal在说话" << endl;
}
};
class Cat : public Animal
{
public:
virtual void Speak()
{
cout << "Cat在说话" << endl;
}
};
class Dog : public Animal
{
public:
virtual void Speak()
{
cout << "Dog在说话" << endl;
}
};
void doSpeak(Animal& animal)
{
animal.Speak();
}
void test()
{
Cat cat;
Dog dog;
doSpeak(cat);
doSpeak(dog);
}
int main()
{
test();
system("pause");
return 0;
}
运行结果:
Cat在说话
Dog在说话
请按任意键继续. . .
运行代码可见,得到了子类不同动物的叫声,发生了多态。
该代码中需要注意的是,C++允许父类的引用或指针指向子类的对象,也就是下边代码是成立的:
Animal& animal = cat;
Animal* animal = &cat;
2,多态原理剖析
通过VS开发人员命令提示工具,分别观察上边代码中父类Animal和子类Cat的数据结构:
父类Animal的数据结构:
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::Speak
子类Cat的数据结构:
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Cat::Speak
可见在父类Animal中有一个vfptr(虚函数指针),这个指针指向Animal::Speak函数;
子类Cat中也有一个vfptr,这个指针指向Cat::Speak函数。
当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址,当父类的指针或引用指向子类对象时,发生多态。
3,多态案例1-计算器
使用多态的方法,设计一个计算器。
#include <iostream>
using namespace std;
#include <string>
class AbstractCal
{
public:
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
class AddCal:public AbstractCal //有继承关系
{
public:
virtual int getResult() //重写父类的虚函数
{
return m_Num1 + m_Num2;
}
};
class SubCal :public AbstractCal
{
public:
virtual int getResult()
{
return m_Num1 - m_Num2;
}
};
class MulCal :public AbstractCal
{
public:
virtual int getResult()
{
return m_Num1 * m_Num2;
}
};
void test()
{
AbstractCal* cal = new AddCal; //父类的指针指向子类对象
cal->m_Num1 = -20;
cal->m_Num2 = 10;
cout <<"m_Num1" <<"+"<<"m_Num2" <<"="<<cal->getResult() << endl;
delete cal;
cal = new SubCal;
cal->m_Num1 = -20;
cal->m_Num2 = 10;
cout << "m_Num1" << "-" << "m_Num2" <<"=" <<cal->getResult() << endl;
delete cal;
cal = new MulCal;
cal->m_Num1 = -20;
cal->m_Num2 = 10;
cout << "m_Num1" << "*" << "m_Num2" << "=" << cal->getResult() << endl;
}
int main()
{
test();
system("pause");
return 0;
}
注意:在使用delete关键字释放父类指针指向的子类对象时,下次重新使用这个指针时,需要重新对成员变量赋值。
结合上述计算器的例子,利用多态的方法有以下好处:
组织结构清晰;
可读性强;
对于前期和后期扩展及维护性高。
4, 纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类中重写的虚函数内容,因此可以将虚函数改为纯虚函数。
纯虚函数的语法:
virtual 返回值类型 函数名(参数列表)= 0;
当类中有了纯虚函数,这个类也称为抽象类;
抽象类的特点:
无法实例化对象;
子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
纯虚函数的应用见下章节的示例。
5,多态案例2——制作饮品
#include <iostream>
using namespace std;
#include <string>
class AbstactDrinking
{
public:
virtual void Boil() = 0;
virtual void ChongPao() = 0;
virtual void DaoChu() = 0;
virtual void JiaFuLiao() = 0;
void doWorkDrink()
{
Boil();
ChongPao();
DaoChu();
JiaFuLiao();
}
};
class Coffee:public AbstactDrinking
{
public:
virtual void Boil()
{
cout << "煮纯净水" << endl;
}
virtual void ChongPao()
{
cout << "加入咖啡" << endl;
}
virtual void DaoChu()
{
cout << "导入纸杯" << endl;
}
virtual void JiaFuLiao()
{
cout << "加入糖和牛奶" << endl;
}
};
class Tea:public AbstactDrinking
{
public:
virtual void Boil()
{
cout << "煮矿泉水" << endl;
}
virtual void ChongPao()
{
cout << "加入茶叶" << endl;
}
virtual void DaoChu()
{
cout << "导入玻璃杯" << endl;
}
virtual void JiaFuLiao()
{
cout << "加入枸杞" << endl;
}
};
void test()
{
AbstactDrinking* drink = new Coffee;
drink->doWorkDrink();
delete drink;
cout << "------------------------" << endl;
drink = new Tea;
drink->doWorkDrink();
}
int main()
{
test();
system("pause");
return 0;
}
运行结果:
煮纯净水
加入咖啡
导入纸杯
加入糖和牛奶
------------------------
煮矿泉水
加入茶叶
导入玻璃杯
加入枸杞
请按任意键继续. . .
咖啡和茶的父类是一个抽象类,类中的虚函数为纯虚函数,子类咖啡和茶需要重写纯虚函数。
6,虚析构和纯虚析构
多态在使用时,如果子类中有属性开辟在堆区,那么父类指针在释放时是无法调用到子类的析构代码,也就是无法释放干净子类对象在堆区的数据。
下边的代码,体现了上述问题。
#include <iostream>
using namespace std;
#include <string>
class Base
{
public:
Base()
{
cout << "父类构造函数调用" << endl;
}
~Base()
{
cout << "父类析构函数调用" << endl;
}
virtual void func() = 0;
};
class Son :public Base
{
public:
Son(int a)
{
cout << "子类构造函数调用" << endl;
m_A = new int(a);
}
~Son()
{
cout << "子类析构函数调用" << endl;
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
virtual void func()
{
cout << "子类func()函数调用" << endl;
}
int* m_A;
};
void test()
{
Base* b = new Son(10);
b->func();
delete b;
}
int main()
{
test();
system("pause");
return 0;
}
运行结果:
父类构造函数调用
子类构造函数调用
子类func()函数调用
父类析构函数调用
请按任意键继续. . .
代码中虽然释放了指向子类对象的父类指针,但通过运行结果可以看到,子类对象的析构函数是没有运行的,也就是子类对象在堆区的数据是没有释放干净的。
将父类中的析构函数改为虚析构或者纯虚析构解决父类指针释放时无法释放干净子类对象在堆区数据的问题。
#include <iostream>
using namespace std;
#include <string>
class Base
{
public:
Base()
{
cout << "父类构造函数调用" << endl;
}
virtual ~Base()
{
cout << "父类析构函数调用" << endl;
}
virtual void func() = 0;
};
class Son :public Base
{
public:
Son(int a)
{
cout << "子类构造函数调用" << endl;
m_A = new int(a);
}
~Son()
{
cout << "子类析构函数调用" << endl;
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
virtual void func()
{
cout << "子类func()函数调用" << endl;
}
int* m_A;
};
void test()
{
Base* b = new Son(10);
b->func();
delete b;
}
int main()
{
test();
system("pause");
return 0;
}
运行结果:
父类构造函数调用
子类构造函数调用
子类func()函数调用
子类析构函数调用
父类析构函数调用
请按任意键继续. . .
将父类中的析构函数改为虚析构就可以调用到子类的析构函数,也就是在父类的析构函数前加virtual关键字:
virtual ~Base()
{
cout << "父类析构函数调用" << endl;
}
同样也可以将父类的构造函数改为纯虚析构:
#include <iostream>
using namespace std;
#include <string>
class Base
{
public:
Base()
{
cout << "父类构造函数调用" << endl;
}
virtual ~Base() = 0;
virtual void func() = 0;
};
Base::~Base()
{
cout << "父类析构函数调用" << endl;
}
class Son :public Base
{
public:
Son(int a)
{
cout << "子类构造函数调用" << endl;
m_A = new int(a);
}
~Son()
{
cout << "子类析构函数调用" << endl;
if (m_A != NULL)
{
delete m_A;
m_A = NULL;
}
}
virtual void func()
{
cout << "子类func()函数调用" << endl;
}
int* m_A;
};
void test()
{
Base* b = new Son(10);
b->func();
delete b;
}
int main()
{
test();
system("pause");
return 0;
}
父类中的构造函数写为纯虚析构的关键代码:
virtual ~Base() = 0;
Base::~Base()
{
cout << "父类析构函数调用" << endl;
}
需要注意的是,纯虚析构需要在类内声明,类外定义,并且也需要有具体的函数实现。
虚析构和纯虚析构共性:
可以解决父类指针释放子类对象;
都需要有具体的函数实现。
虚析构和纯虚析构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象。
总结:
虚析构和纯虚析构就是用来解决通过父类指针释放子类对象;
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;
拥有纯虚析构函数的类也属于抽象类,无法实例化对象。
7,禁止重写虚函数
C++11里新的规则,可以禁止重写虚函数,一般发生在多重继承中。