Part3: 多态 3.1 虚函数与函数覆盖 使用 virtual 关键字修饰普通成员函数就是虚函数 虚函数的主要用途是函数覆盖 函数覆盖的主要用途是多态 虚函数的使用需要注意: 静态成员函数不能
Part3: 多态
3.1 虚函数与函数覆盖
使用virtual
关键字修饰普通成员函数就是虚函数
虚函数的主要用途是函数覆盖
函数覆盖的主要用途是多态
虚函数的使用需要注意:
- 静态成员函数不能设置为虚函数
- 构造函数不能设置为虚函数
- 析构函数可以设置为虚函数
- 如果声明和定义分离,只需要在声明时使用
virtual
关键字修饰 - 函数覆盖
虚函数具有传递性当基类中某一个成员函数设置为虚函数后,派生类中的同名函数(函数名称相同、参数列表完全相同、返回值类型相关)会自动变为虚函数。此时使用派生类对象调用此函数的效果类似于函数隐藏,但相比于函数隐藏,函数覆盖支持多态、在C++11中可以使用override关键字验证是否覆盖成功
#include <iostream>
using namespace std;
class Animal
{
public:
// 加上virtual后,所有eat变斜体——传递性
virtual void eat() // 虚函数,QTcreater将其变为斜体字
{
cout << "吃东西" << endl;
}
};
class Dog:public Animal
{
public:
// 函数覆盖检验方式:override自我查错,可读性提示
void eat() override // 加上override不错——成功了,报错——失败
{
cout << "狗吃屎" << endl;
}
};
int main()
{
Animal a;
a.eat();
Dog d;
// 类似函数隐藏
d.Animal::eat();
d.eat();
return 0;
}
3.2 多态
字面意思:
多种状态,可认为是"一个接口多种状态"
接口在运行期间,根据传入的参数来决定具体调用的函数,最终采取不同的执行策略
多态与模板的区别:
多态虽然支持也多种数据类型,但是不同类型的处理逻辑可能不同
模板对所以类型的处理方式是相同的
使用多态的条件:
- 要使用公有继承
- 要有函数覆盖
- 基类引用/指针指向派生类对象
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat() // Qt Creator中虚函数使用斜体字
{
cout << "动物吃东西" << endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗吃骨头" << endl;
}
void guard() // 狗类新增非虚函数
{
cout << "狗能看门" << endl;
}
};
class Wolf:public Animal
{
public:
void eat()
{
cout << "狼吃肉" << endl;
}
};
class Husky:public Dog
{
public:
void eat() override // 可以验证覆盖的有效性
{
cout << "哈士奇吃家具" << endl;
}
};
// 基于引用的多态函数
void test_eat1(Animal& a)
{
a.eat();
}
// 基于指针的多态函数
void test_eat2(Animal* a)
{
a->eat();
}
int main()
{
Animal a;
Dog d;
Wolf w;
Husky h;
test_eat1(a); // 动物吃东西
test_eat1(d); // 狗吃骨头
test_eat1(w); // 狼吃肉
test_eat1(h); // 哈士奇吃家具
Animal* ap = new Animal;
Dog* dp = new Dog;
Wolf* wp = new Wolf;
Husky* hp = new Husky;
test_eat2(ap); // 动物吃东西
test_eat2(dp); // 狗吃骨头
test_eat2(wp); // 狼吃肉
test_eat2(hp); // 哈士奇吃家具
delete ap;
delete dp;
delete wp;
delete hp;
// 多态的本质
Animal& a0 = d;
a0.eat(); // 狗吃骨头
// a0.guard(); 使用多态时,不支持派生类独有的功能
return 0;
}
3.3 多态原理
动态类型绑定
拥有虚函数的类,会生成一个记录所有虚函数的虚函数表
同时此类所有对象中会增加一个隐藏的成员变量:虚函数表指针,指向本类的虚函数表
当派生类继承拥有虚函数的基类的同时也会继承虚函数表
如果派生类覆盖了基类中的虚函数,会更新这张表的内容
若派生类新增了新的虚函数,会在表的尾部新增此虚函数
每个类型对象的虚函数表指针都指向自己类的虚函数表
代码运行阶段可通过查询虚函数表找到对应类型应该调用的函数地址
多态与继承一样
- 提高了代码的编程效率
- 牺牲了一部分执行效率
3.4 虚析构函数
warning: deleting object of polymorphic class type 'Animal'
which has non-virtual destructor might cause undefined behaviour
警告:使用非虚拟析构函数删除多态类型的对象会出现问题
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat() // Qt Creator中虚函数使用斜体字
{
cout << "动物吃东西" << endl;
}
~Animal()
{
cout << "Animal destructor" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "狗吃骨头" << endl;
}
~Dog()
{
cout << "Dog destructor" << endl;
}
};
// 基于引用是在栈内存不会出现内存泄漏
// 基于指针的多态函数
//void test_eat2(Animal* a) // 相当于将dp赋值给基类的指针,可在主函数定义
//{
// a->eat();
//}
int main()
{
Dog* dp = new Dog;
Animal* ap = dp; // 指针的赋值只是将保存地址赋值
// 对象是Dog,但是类型被Animal所限制
ap->eat(); // 狗吃骨头
delete ap; // 按照Animal方式析构,实际上是Dog对象
// Dog对象会产生内存泄漏
return 0;
}
狗吃骨头
Animal destructor
// 未调用派生类析构函数
使用虚析构函数就可以解决该问题
虚析构函数可以把析构函数加入到虚函数表中
在对象销毁时查询虚函数表,依次调用各个继承层次的析构函数
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void eat() // Qt Creator中虚函数使用斜体字
{
cout << "动物吃东西" << endl;
}
virtual ~Animal()
{
cout << "Animal destructor" << endl;
}
};
class Dog:public Animal
{
public:
void eat() override
{
cout << "狗吃骨头" << endl;
}
~Dog()
{
cout << "Dog destructor" << endl;
}
};
// 基于引用是在栈内存不会出现内存泄漏
// 基于指针的多态函数
//void test_eat2(Animal* a) // 相当于将dp赋值给基类的指针,可在主函数定义
//{
// a->eat();
//}
int main()
{
Dog* dp = new Dog;
Animal* ap = dp;
ap->eat(); // 狗吃骨头
delete ap;
return 0;
}
狗吃骨头
Dog destructor
Animal destructor
在设计一个类时,编译器自动生成的析构函数不是虚函数
此类应用多态时可能造成内存泄漏的问题
建议把一个类的析构函数设置为虚函数
除非这个类确定不会有派生类