虚函数和多态
来源:互联网 发布:淘宝花呗店铺出售 编辑:程序博客网 时间:2024/05/17 22:49
- 虚函数和多态
- 成员函数重写
- 继承遇到函数重写
- 虚函数和多态的含义
- 多态的应用
- 多态的作用
- 什么时候父类成员函数加virtual关键字
- 还支持引用类型
- 父类类型本身没有多态效果
- 抽象基类和纯虚函数
- 语法细节
- 其它参考
虚函数和多态
成员函数重写
子类继承了父类的所有成员函数,但是父类的成员函数不一定适合子类,子类可以重载或者重写(覆盖)父类的成员函数。
重载我们在之前的章节中已有介绍,即函数名相同,参数不同。
重写(覆盖)的意思是函数名和参数表完全一样,即函数的原型完全一样。
比如,void Human::introduce()函数的功能是输出人类的各成员变量的值。但是它并不合适Student对象,因为该函数没有输出Student类增加的年级成员变量grade的值。因此Student类可以新增一个更适合自己的版本,即重写(覆盖)该函数。
class Student : public Human{ public: void set_grade(int g){ grade = g; } int get_grade(){ return grade; } int grade; void introduce(); // 新增introduce成员函数};void Student::introduce(){ // 与Human::introduce()函数实现相同的功能 cout << "大家好,我是" << name << endl; if(is_male){ cout << "男性" << endl; }else{ cout << "女性" << endl; } cout << age << "岁" << endl; if(id.length() == 0){ cout << "身份证号未知" << endl; }else{ cout << "身份证号:" << id << endl; } // 新增grade成员的输出 cout << "年级:" << grade << endl;}
注意,与重载不同,重写(覆盖)只能发生在继承关系中。单独的一个类中的两个成员函数,或者类外部的两个函数是不能有相同的函数原型的。
class foo{public: void member(){}; void member(){}; // 错误,一个类中不能出现原型完全一样的成员函数};void fun(int x){}void fun(int x){} // 错误,不能出现原型完全一样的函数
重写(覆盖)的意义在于子类可以增加一些成员函数,这些成员函数与父类的成员函数有相同的接口(函数原型),不同的实现。使它们更适合子类。换句话说即成员函数功能的细化。
重写(覆盖)的意义在于子类的”新版本“的成员函数优化了/细化了父类的一些”老版本“成员函数。
注意,父类的”老版本“的被重写的函数也被子类继承了,在子类中存在”老版本“和”新版本“的两种版本。在子类中要使用”老版本“的函数需要使用范围运算符::。否则,默认是”新版本“的。即重写和重载一样,在子类中都会自动隐藏父类的版本的函数。
下面是void Student::introduce()另一种正确的写法。
void Student::introduce(){ // 与Human::introduce()函数实现相同的功能,因此可以调用它 Human::introduce(); // 新增grade成员的输出 cout << "年级:" << grade << endl;}
而下面的void Student::introduce()的实现会造成void Student::introduce()递归调用。
void Student::introduce(){ introduce(); // 错误,调用了Student::introduce自己,形成了递归调用,函数无法结束 // 新增grade成员的输出 cout << "年级:" << grade << endl;}
继承遇到函数重写
前面说过,继承反映的是子类与父类之间“是一个”的关系,比如说对象赋值:
Human p;Student s;s.init("周润发", true, 30, "123456", 1);p = s; // 把Student类对象s中包含的Human部分的各成员的值赋给p的各成员
再比如说,指针操作
Human *p_human = NULL;Student s;s.init("周润发", true, 30, "123456", 1);p_human = &s;p_human->introduce();
图
因为,定义p_human时指定它的类型是Human *,因此它认为自己指向的是Human对象。于是,p_human->introduce()调用的是void Human::introduce()方法。
不过,等等,这里我们是不是忽略了什么。
显然,p_human实际上指向的是Student类对象s,而Student重写了introduce方法,为什么p_human->introduce()调用的不能是”新版本“的introduce呢,它更适合Student对象啊。
在这种场景下,要想p_human->introduce()调用的是”新版本“的void Student::introduce(),那么introduce必须是虚函数。
虚函数和多态的含义
如果想在父类中定义一个成员函数留待子类中进行细化,我们必须在它前面加关键字virtual ,以便可以使用指针对指向相应的对象进行操作。
class Human{ public: void init(string n, int a, bool m, string i){ name = n; age = a; is_male = m; id = i; } virtual void introduce(); // 虚函数 private: string name; int age; bool is_male; string id;};void Human::introduce(){ cout << "大家好,我是" << name << endl; if(is_male){ cout << "男性" << endl; }else{ cout << "女性" << endl; } cout << age << "岁" << endl; if(id.length() == 0){ cout << "身份证号未知" << endl; }else{ cout << "身份证号:" << id << endl; } }class Student : public Human{ public: void init(string n, int a, bool m, string i, int g){ Human::init(n, a, m, i); grade = g; } void introduce(); private: int grade; // 年级};void Student::introduce(){ Human::introduce(); cout << "年级:" << grade << endl;}class Soldier : public Human{ public: void init(string n, int a, bool m, string i, string r){ Human::init(n, a, m, i); rank = r; } void introduce(); private: string rank; // 军衔};void Soldier::introduce(){ Human::introduce(); cout << "军衔:" << rank << endl;}int main(){ Human *p_human = NULL; Student s; s.init("周润发", true, 30, "12345", 1); Soldier s2; s2.init("刘伯承", true, 30, "23456", "元帅"); p_human = &s; p_human->introduce(); p_human = &s2; p_human->introduce(); system("pause"); return 0;}
父类成员函数是虚函数,那么通过父类指针来调用,调用的版本由指针指向的对象来决定。这就体现了多态的含义。即一种接口,多种实现。
- 一种调用形式:p_human->introduce()
- 多种调用结果:实际被调用的可能是下面三个版本的introduce之一
void Human::introduce()
void Student::introduce()
void Soldier::introduce()
大家可以去掉Human类中的virtual关键字,对比一下程序运行结果。
多态的应用
多态的作用
多态有什么作用呢?它帮助我们达到“软件复用”。
那在程序里,可能编写了很多处理Human类的代码,比如下面的函数,
... // Human、Student、Soldier类的定义void introduce_triple(Human *p){ int i; for(i = 0; i < 3; i++){ p->introduce(); }}
会让Human的进行3遍自我介绍。
那这个函数能否处理学生对象呢,即当p指向学生对象,p->introduce();是否可以调用学生类的introduce。在introduce函数是虚函数的情况下是可以的。
int main(){ Student s; s.init("周润发", true, 30, "12345", 1); Soldier s2; s2.init("刘伯承", true, 30, "23456", "元帅"); introduce_triple(&s); introduce_triple(&s2); system("pause"); return 0; }
举一个生活中的例子,TCL出了某款型号的彩电比如“栩栩如生”系列第一代产品,还有附带的遥控器可以来遥控电视。随后,后续又推出了该系列第二代、第三代产品。这时,大家可能会想,控制第一代的遥控器也可以控制它们就好了。如果可以,那遥控器就可以复用了。
什么时候父类成员函数加virtual关键字
什么时候需要将父类的成员函数修饰为虚函数,即加virtual关键字呢?就看这个函数是否是适应于子类的,
如果是成员函数适合子类的就不需要加virtual关键字。
如果子类可能会细化这个函数,那就需要在父类中加virtual关键字。
比如void Human::set_age(int age)
和int Human::get_age()
这两个函数是用来设置和读取年龄的。显然,Human类的子类不需要重写这个函数。那么这两个函数就不需要设置为虚函数。
还支持引用类型
多态必须通过父类指针调用才可以起作用,或者引用也可以,如下面的代码
... // Human、Student、Soldier类的定义void introduce_triple(Human &p) // 父类的引用{ int i; for(i = 0; i < 3; i++){ p.introduce(); }}int main(){ Student s; s.init("周润发", true, 30, "12345", 1); Soldier s2; s2.init("刘伯承", true, 30, "23456", "元帅"); introduce_triple(s); introduce_triple(s2); system("pause"); return 0; }
父类类型本身没有多态效果
如果不是指针和引用,而是父类类型,那么最终调用的是父类的版本,没有多态的效果。如下面代码:
... // Human、Student、Soldier类的定义void introduce_triple(Human p) // 父类类型本身{ int i; for(i = 0; i < 3; i++){ p.introduce(); }}int main(){ Student s; s.init("周润发", true, 30, "12345", 1); Soldier s2; s2.init("刘伯承", true, 30, "23456", "元帅"); introduce_triple(s); introduce_triple(s2); system("pause"); return 0; }
抽象基类和纯虚函数
shape类的例子
语法细节
虚函数特性是被继承的,子类重写虚函数,在类的定义里可加virtual关键字也可不加。当然加上更明显的说明此函数是虚函数。
...class Student : public Human{ public: void init(string n, int a, bool m, string i, int g){ Human::init(n, a, m, i); grade = g; } virtual void introduce(); // 正确,也可以不加virtual关键字 private: int grade; // 年级};
类定义中指出是某个成员函数是虚函数,如果该函数的实现写在类定义外边,不能加virtual关键字了。
virtual void Student::introduce() // 错误,不能在这里加virtual关键字{ Human::introduce(); cout << "年级:" << grade << endl;}
其它参考
多态介绍可参考4.4 多态 (Polymorphism)
- 多态和虚函数
- 虚函数和多态
- 多态和虚函数
- 多态和虚函数
- 多态和虚函数
- 虚函数和多态
- 虚函数和多态
- 虚函数和多态
- 虚函数和多态
- 虚函数和多态
- 虚函数和多态
- 虚函数,纯虚函数和多态
- 虚函数、纯虚函数和多态
- 第四章 虚函数和多态
- 什么是虚函数和多态
- 学习了虚函数和多态
- C++虚函数和多态
- C++虚函数和多态学习
- android_替代麻烦的异步任务 解决网络访问必须在子线程
- 利用Logistic回归预测疝气病症的病马的死亡率
- JBoss7/WildFly配置数据源:mysql
- Dockerfile中ENTRYPOINT 和 CMD的区别
- 事件冒泡相关处理
- 虚函数和多态
- 考试篇(5.2) NSE4 题库 02. 日志与监控 ❀ 飞塔 (Fortinet) 网络安全专家
- C++中std::reverse和std::reverse_copy的使用
- ubuntu16.04输入密码后登入黑屏
- scrapy 登录
- R语言向量_NA与NULL值
- 用vue手脚架生成的项目修.vue文件后,保存编译报错,缺少2个空格
- leetcode_303. Range Sum Query
- 【Bzoj3668】起床困难综合症