多态概念
在面向对象中,多态就是指不同对象收到相同消息时,产生不同的行为。
一个源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编在一起的过程。其中在运行之前就完成的联编称为静态联编,而在程序运行时才完成的联编叫动态联编。
静态联编是指系统在编译时就决定如何实现某一动作。静态联编要求在程序编译时就知道调用函数的全部信息。因此,这种联编类型的函数调用速度很快。效率高是静态联编的主要优点。
动态联编是指系统在运行时动态实现某一动作。采用这种联编方式,一直到程序运行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性和程序易维护性。
静态联编支持的多态性成为编译时多态性,也叫静态多态。在C++中,编译时多态性是通过函数重载和模板实现的。
动态联编所支持的多态性为运行时多态性,也叫动态多态。在C++中,运行时多态是通过虚函数实现的。
下面重点介绍动态多态。
虚函数
动态多态是通过虚函数来实现的,为了引入虚函数我们先来看一段代码。
class A
{
public:
A()
{
}
void show()
{
cout << "基类函数" << endl;
}
};
class B :public A
{
public:
B()
{
}
void show()
{
cout << "派生类函数" << endl;
}
};
int main()
{
B obj_b;
A* a = &obj_b;
a->show();
return 0;
}
虽然我们将派生类对象的地址赋给基类的指针,通过基类指针调用show()函数时,得到的是基类函数,这并不是我们想要的。所以需要我们在基类成员函数前面加上 virtual 将该函数声明为虚函数,然后在派生类中将虚函数重写。虚函数的重写是指:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数。另外虚函数的重写也叫作虚函数的覆盖。这样我们就能通过指向派生类对象的基类的指针调用派生类的虚函数。
仅仅将函数声明为虚函数并调用它就实现多态了吗?并不是的。只有通过使用基类的指针或引用并且指向或引用的是派生类的对象时,才会体现出多态行为。
#include<iostream>
using namespace std;
class A
{
public:
A(int _a) :a(_a)
{
}
virtual void fun()
{
cout << "基类虚函数" << endl;
}
protected:
int a;
};
class B :public A
{
public:
B(int _a, int _b) :A(_a), b(_b)
{}
virtual void fun() override
{
cout << "派生类虚函数" << endl;
}
protected:
int b;
};
int main()
{
A obj_a(10);
B obj_b(20, 30);
A aa = obj_b;
aa.fun();
A* paa = &obj_b;
paa->fun();
A& raa = obj_b;
raa.fun();
paa = &obj_a;
B* pbb = (B*)paa;
pbb->fun();
paa = &obj_b;
pbb = (B*)paa;
pbb->fun();
return 0;
}
根据派生类和基类对象赋值关系,我们可以得到上面五种情况。
第一种:将派生类对象赋值给基类对象,调用虚函数 fun() ,实际调用的是基类的虚函数,不能体现出多态性。
第二种:将派生类对象赋值给基类指针,调用虚函数 fun() ,实际调用的是派生类虚函数,体现出多态性。
第三种:将派生类对象赋给给基类引用,调用虚函数 fun() ,实际调用的是派生类虚函数,体现出多态性。
第四种:将指向基类对象的基类指针强转成派生类指针类型,并赋值给派生类指针,调用虚函数 fun() ,实际调用的是基类虚函数,不能体现出多态性。
第五种:将指向派生类对象的基类指针强转成派生类指针类型,并赋值给派生类指针,调用虚函数 fun() ,实际调用的是派生类虚函数,体现出多态性。
程序运行结果:
虚函数重写的特例:协变
虚函数重写有一个例外:重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。
class A{};
class B : public A {};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
return new B;
}
};
虚析构函数
来看一段代码:
class A
{
public:
~A()
{
cout << "基类析构函数" << endl;
}
};
class B:public A
{
public:
~B()
{
cout << "派生类析构函数" << endl;
}
};
int main()
{
A* p = new B;
delete p;
return 0;
}
首先我们定义了一个指向基类的指针变量 p,然后用 new 创建了一个派生类的无名对象,并将无名对象的地址赋给 对象指针p,我们用 delete 撤销无名对象,释放存储空间,结果发现调用的是基类的析构函数。显然,这不是我们预期的结果,如果我们希望在撤销无名对象时先调用的是派生类的析构函数然后在调用基类的析构函数,就要我们采用动态联编的方式,即虚析构函数。
class A
{
public:
virtual ~A()
{
cout << "基类析构函数" << endl;
}
};
class B:public A
{
public:
virtual ~B()
{
cout << "派生类析构函数" << endl;
}
};
int main()
{
A* p = new B;
delete p;
return 0;
}
这个时候就是先调用派生类析构函数,再调用基类析构函数。基类的析构函数最好写成虚函数。
纯虚函数和抽象类
在虚函数后面加上 =0 说明这个函数为纯虚函数。纯虚函数没有函数体,不具备函数的功能不能被调用。它的作用是方便派生类对它重写。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,只能作为其他类的基类。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
如果抽象类的派生类中没有对纯虚函数进行重写,则该函数在派生类中仍然是纯虚函数,而这个派生类仍然是一个抽象类。
class A
{
public:
virtual void show() = 0;
};
class B :public A
{
public:
virtual void show()
{
cout << "纯虚函数重写" << endl;
}
};
C++11 中 override 和 final
C++11中提供了关键字override来强制重写虚函数。该关键字写在派生类虚函数的后面,如果没有重写虚函数或者某一处重写有错误(返回值,参数列表,函数名必须一致),编译器就会报错。
class A
{
public:
virtual void show()
{
cout << "基类虚函数" << endl;
}
};
class B :public A
{
public:
virtual void show() override
{
cout << "派生类虚函数" << endl;
}
};
可以看出来 override 和纯虚函数都是为了防止没有重写虚函数而设定的。
在继承中,将 final 写在类的最后面,表示该类不能被继承,用 final 修饰基类的虚函数,在派生类中不能被重写。
class A final
{
};
class B :public A //不能继承
{
};
class A
{
public:
virtual void show() final
{
}
};
class B :public A
{
virtual void show() //不能重写
{
}
};
虚函数表,虚表指针
虚函数表 (vtable) 和虚表指针 (vfptr) 是多态中的重要内容,有关这部分的详细讲解请查看我的另一篇博客:
https://blog.youkuaiyun.com/smx_dd/article/details/83273458