多态
1、基本概念
多态是C++面向对象的三大特点之一。
多态分为两类: 静态多态: 函数重载和运算符属于静态多态,服用函数名; 动态多态: 派生类和虚函数实现运行时多态。
静态多态和动态多态区别: 静态多态的函数地址早绑定 - 编译阶段确定函数地址。 动态多态的函数地址晚绑定 - 运行阶段确定函数地址。
#include <iostream>
using namespace std;
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//执行说话的函数
void doSpeak(Animal& animal) //Animal& animal = cat;
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
}
int main()
{
test01();
system("pause");
return 0;
}
上面的代码并不能达到预期的效果,它打印的并不是“小猫在说话”,而是“动物在说话”。 原因就是地址早绑定,在编译阶段确定函数地址。
如果想要打印“猫在说话”,需要使用动态多态。
class Animal
{
public:
//虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
添加关键字“virtual”,将函数变为“虚函数”,实现动态多态。
动态多态的满足条件:
1、有继承关系;
2、子类重写父类的虚函数;
动态多态如何使用呢? 总结下来,就是父类的指针或者引用,执行子对象。
void doSpeak(Animal& animal) //Animal& animal = cat;
{
animal.speak();
}
那么,动态多态的底层是怎样实现的呢?
首先我们删除虚函数关键字“virtual”,看一看Animal类占了多大内存。
void test02()
{
cout << "sizeof Animal = " << sizeof(Animal) << endl;
}
打印的结果是 1,它是一个空类。
那加上“virtual”关键字后有什么变化呢? 结果变成了 4。 也就是说类的内部发生了变化。
class Animal
{
public:
//虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
当写了这样一段代码后,Animal类的内部会有一个大小为4个字节的指针,名叫vfptr,也叫虚函数指针。 这个指针指向虚函数表vftable,表的内部记录一个虚函数的地址 &Animal::speak。
class Cat : public Animal
{
public:
};
对于小猫的类,如果不发生重写,仅仅只是继承,那Cat的内部结构中继承一份虚指针,指向子类的虚函数表&Animal::speak。
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
当发生重写后,子类中的虚函数表会被覆盖,替换成子类的虚函数地址&Cat::speak。
所以,多态的原理就是父类的指针和引用指向子类对象。
可以使用开发人员命令工具验证。
案例1:计算器类
#include <iostream>
#include <string>
using namespace std;
//分别利用普通写法和多态技术实现计算器
//普通写法
class Calculator
{
public:
int gerResult(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;
}
}
int m_Num1; //操作数1
int m_Num2; //操作数2
};
void test01()
{
//创建计算器对象
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.gerResult("+") << endl;
cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.gerResult("-") << endl;
cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.gerResult("*") << endl;
}
//如果要改进计算器,要修改源码。
//在实际开发中,提倡 开闭原则:对扩展进行开放,对修改进行关闭。
//利用多态实现计算器
//实现计算器的抽象类
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 test02()
{
//多态使用条件:父类的指针或引用指向子类对象
//加法计算器
abstractCalculator* ptr = new addCalculator;
ptr->m_Num1 = 100;
ptr->m_Num2 = 100;
cout << ptr->m_Num1 << "+" << ptr->m_Num2 << "=" << ptr->getResult() << endl;
//用完后销毁
delete ptr;
//减法
ptr = new subCalculator;
ptr->m_Num1 = 100;
ptr->m_Num2 = 100;
cout << ptr->m_Num1 << "-" << ptr->m_Num2 << "=" << ptr->getResult() << endl;
delete ptr;
//乘法
ptr = new mulCalculator;
ptr->m_Num1 = 100;
ptr->m_Num2 = 100;
cout << ptr->m_Num1 << "*" << ptr->m_Num2 << "=" << ptr->getResult() << endl;
delete ptr;
}
//使用多态后代码量变大了,
//但是,组织结构清晰;可读性强;对于前期和后期的扩展以及维护性高;
int main()
{
test01();
test02();
system("pause");
return 0;
}
2、纯虚函数和抽象类
在上面的代码中,父类中的虚函数下的代码块是没有任何意义的,主要是在调用子类中的重写内容。 因此,可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数类型) = 0;
当类中有了纯虚函数,这个类称为抽象类。
抽象类特点: 无法实例化对象; 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
#include <iostream>
using namespace std;
class Base
{
public:
//纯虚函数
virtual void func() = 0;
};
//子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
class Son : public Base
{
public:
virtual void func()
{
cout << "func函数调用" << endl;
}
};
void test01()
{
/*抽象类无法实例化对象
Base b;
new Base;*/
Base* base = new Son;
base->func();
}
int main()
{
test01();
system("pause");
return 0;
}
案例二:制作饮品
#include <iostream>
using namespace std;
//抽象类
class drink
{
public:
//煮水
virtual void boil() = 0;
//洗茶具
virtual void wash() = 0;
//放茶叶
virtual void put() = 0;
//冲泡
virtual void brew() = 0;
//静置
virtual void standing() = 0;
void makeDrink()
{
boil();
wash();
put();
brew();
standing();
}
};
//制作过程
class Tea : public drink
{
public:
//煮水
virtual void boil()
{
cout << "煮山泉" << endl;
}
//洗茶具
virtual void wash()
{
cout << "清洗茶具" << endl;
}
//放茶叶
virtual void put()
{
cout << "放茶叶" << endl;
}
//冲泡
virtual void brew()
{
cout << "冲泡" << endl;
}
//静置
virtual void standing()
{
cout << "静置等待" << endl;
}
};
void doWork(drink* ptr)
{
ptr->makeDrink();
delete ptr;
}
void test1()
{
cout << "---泡茶---" << endl;
//泡茶
doWork(new Tea);
}
int main()
{
test1();
system("pause");
return 0;
}
3、虚析构和纯虚析构
在多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,堆区的数据会因此造成数据的泄露。 所以,要将父类中的析构函数改为虚析构或纯虚析构。
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal构造函数调用" << endl;
}
/*virtual ~Animal()
{
cout << "Animal析构函数调用" << endl;
}*/
//纯虚析构,需要声明也需要实现
//有了纯虚析构后, 这个类属于抽象类,无法实例化对象
virtual ~Animal() = 0;
virtual void speak() = 0;
};
Animal::~Animal()
{
cout << "Animal纯析构函数调用" << endl;
}
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "cat的构造函数调用" << endl;
m_Name = new string(name);
}
virtual void speak()
{
cout << "小猫" << *m_Name << "在说话" << endl;
}
~Cat()
{
if (m_Name != NULL)
{
cout << "cat的析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
string* m_Name;
};
void test1()
{
Animal* ptr = new Cat("Tom");
ptr->speak();
//父类指针析构的时候不会调用子类的析构函数,堆区的数据会因此造成数据的泄露。
delete ptr;
}
int main()
{
test1();
system("pause");
return 0;
}