虚函数和多态
成员函数重写
子类继承了父类的所有成员函数,但是父类的成员函数不一定适合子类,子类可以重载或者重写(覆盖)父类的成员函数。
重载我们在之前的章节中已有介绍,即函数名相同,参数不同。
重写(覆盖)的意思是函数名和参数表完全一样,即函数的原型完全一样。
比如,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)