首先,什么是多态,多态是怎么来的?
多态指的是 父类和子类在执行相同行为的时候,出现了不同的结果。
成人和学生执行买票这种行为的时候,成人的结果是全票,学生的结果是半票。实际上为了从语言的角度达到上述效果,其实就是父类Person 和子类Student 有着相同的成员函数 BuyTicket,父类去调用 BuyTicket 的结果是买全票,子类去调用 BuyTicket 的结果是买半票。我们的第一想法可能是这样
Person p;
p.BuyTicket(); // 成人买票
Student s;
s.BuyTicket(); // 儿童买票
但是这样的灵活性不强,于是引入了多态的概念,仅仅使用一个对象,就可以达到两种效果
void Func(Person& p)
{
p.BuyTicket();
}
int main(){
Person ps;
Student st;
Func(ps); // 传递Person对象买成人票
Func(st); // 传递Student对象买半票
}
在实现c++多态时会用到虚函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。一般格式:
class base
{
public:
base();
virtual void test(); //定义的一个虚函数
private:
char *basePStr;
};
上述代码在基类中定义了一个test的虚函数,所有可以在其子类重新定义父类的做法这种行为成为覆盖(override),或者为重写。
常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关的虚函数,动态绑定,由于编写代码时不能确定被调用的是基类函数还是那个派生类函数,所以被称为“”虚“”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。
例如下面这个例子:
#include<iostream>
using namespace std;
class A
{
public:
A() {};
~A() {};
void show(void)
{
cout << "i am a" << endl;
}
};
class B :public A
{
public:
B() {};
~B() {};
void show(void)
{
cout << "i am b" << endl;
}
};
int main()
{
A atr,*ptr;//A atr;
B btr; //B btr,*ptr;这样写会报错,就好比你可以说人是动物却不能说动物是人一样。
ptr = &atr;
ptr->show();
ptr = &btr;
ptr->show();
return 0;
}
运行结果如下:
这并不是我们需要的结果。(基类指针可以指向子类和基类,而子类指针只能指向子类)
下面的例子就是使用虚函数的好处
#include<iostream>
using namespace std;
class A
{
public:
void foo()
{
printf("1\n");
}
virtual void fun()
{
printf("2\n");
}
};
class B : public A
{
public:
void foo() //隐藏:派生类的函数屏蔽了与其同名的基类函数
{
printf("3\n");
}
void fun() //多态、覆盖
{
printf("4\n");
}
};
int main(void)
{
A a;
B b;
A *p = &a;
p->foo(); //输出1
p->fun(); //输出2
p = &b;
p->foo(); //取决于指针类型,输出1
p->fun(); //取决于对象类型,输出4,体现了多态
return 0;
}
虚函数的两个常见错误:无意的重写、虚函数签名不匹配。
继承中要构成多态条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
无意的重写
什么是重写?
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数和基类虚函数的返回值类型,函数名,参数列表完全相同)称子类的虚函数重写了基类的虚函数,如下所示
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
// 子类的 virtual 可以不写,但是父类的必须写
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
无意的重写 示例如下,在派生类中声明了一个与基类的某个虚函数具有相同的签名的成员函数,不小心重写了这个虚函数。
class Base {
public:
virtual void Show(); // 虚函数
};
class Derived : public Base {
public:
void Show(); // 无意的重写
};
隐藏
五、成员函数的隐藏
隐藏指在某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下几种情况
两个函数参数相同,但是基类不是虚函数。和重写的区别在于基类函数是否是虚函数
两个函数参数列表不同,无论基类函数是否虚函数,基类函数都将被覆盖。和重载的区别在于两个函数不在同一个类中
注意:父类指针指向子类实例对象,调用普通重写方法时,会调用父类中的方法。而调用被子类重写虚函数时,会调用子类中的方法。
#include <iostream>
using namespace std;
class Base
{
public:
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
int main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
//没加关键字virtual,所以就调用指针所代表的对象
pb->g(3.14f);//调用基类,Base::g(float)
pd->g(3.14f);//调用派生类,尽管派生类没有float的函数,派生类屏蔽基类Derived::g(int)
// Bad : behavior depends on type of the pointer
pb->h(3.14f);
pd->h(3.14f);
}
虚函数重写的两种特例情况
协变
析构函数的重写
2.1.2、虚函数签名不匹配
函数的签名包括:函数名,参数列表,const属性。
虚函数签名不匹配的错误通常是因为 函数名、参数列表 或 const 属性不一样,导致意外创建了一个新的虚函数,而不是重写一个已存在的虚函数。
class Base {
public:
virtual void Show(int x); // 虚函数
};
class Derived : public Base {
public:
virtual void Sh0w(int x); // o 写成了 0,新的虚函数
virtual void Show(double x); // 参数列表不一样,新的虚函数
virtual void Show(int x) const; // const 属性不一样,新的虚函数
};
上述三种写法,编译器并不会报错,因为它不知道你的目的是重写虚函数,而是把它当成了新的虚函数。
2.2如何避免这些错误?
针对上述情况,C++ 11 增加了两个继承控制关键字:override 和 final,两者的作用分别为:
override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名;
final:阻止类的进一步派生 和 虚函数的进一步重写。
2.2.1、override
比如下面的代码,加了override,明确表示派生类的这个虚函数是重写基类的,如果派生类与基类虚函数的签名不一致,编译器就会报错。
class Base {
public:
virtual void Show(int x); // 虚函数
};
class Derived : public Base {
public:
virtual void Show(int x) const override; // const 属性不一样,新的虚函数
};
报错信息如下
因此,为了减少程序运行时的错误,重写的虚函数都建议加上 override。