上帝是一个程序员,创造了动物(基类),给予了动物吃饭,睡觉,叫唤等通用功能。(封装)
只指定了平均睡觉八小时(虚函数),其中没有指定具体的吃饭,叫唤的行为。(纯虚函数)然后细分一下,动物有猫狗羊和人。(继承)
人类明确它们物种的时候(明确类型的派生类指针)
猫吃鱼 狗吃肉 羊吃草
猫喵喵 狗汪汪 羊咩咩
(多态 同名覆盖)一切都如此顺理成章。
突然人发现一只动物!
这只是什么呢?诶?这货不知道是啥!只能用"动物"来称呼他!(基类指针指向子类对象)当没有虚函数的时候,人类发现这只动物不会叫也不会吃!因为他根本没有这样的实现!(注意:真正编程上如果派生类不对纯虚函数进行实现将无法通过编译)
有了虚函数,让那只动物"吃",发现他吃草!于是捅一下这只动物,发现它会 哞哞叫!于是得知这是一头牛!
可以吃!于是人类就把它吃掉了。
(1)虚函数
虚函数是指在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};含有虚函数的类是虚类。实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数。简单来说虚函数就是实现基类指针调不同子类的同一方法时有不同的行为。
实际上每个含有虚函数的类都有一张虚函数表(vtbl),表中每一项是一个虚函数的地址 。如果继承的父类中有虚函数,那么子类中也会有虚函数表。我们这里只讨论虚函数,推荐一个不错的博客C++虚函数表深入探索(详细全面)。
那么虚函数到底有啥用呢?
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类指针指向其子类的实例,然后通过父类的指针调用子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;
形成多态必须具备三个条件:
- 必须存在继承关系;
- 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
- 存在基类类型的指针或者引用,通过该指针或引用调用虚函数;
C++支持两种多态性:
- 编译时多态性:通过重载函数实现
- 运行时多态性:通过虚函数实现。
#include<iostream>
using namespace std;
// 基类
class A
{
public:
void eat()
{
printf("1\n");
}
virtual void sleep()
{
printf("2\n");
}
};
// 派生类
class B : public A
{
public:
void eat()
{
printf("3\n");
}
void sleep()
{
printf("4\n");
}
};
int main()
{
A a;
B b;
A* p = &a; // 基类指针指向基类对象
p->eat(); // 1
p->sleep();// 2
p = &b; // 基类指针指向子类对象
p->eat();// 1
p->sleep();// 4
return 0;
}
第一个p->eat()和p->sleep()很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
第二个输出结果就是1、4。p->eat()和p->sleep()则是基类指针指向子类对象,正式体现多态的用法,p->eat()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的eat()函数的代码了,因此输出的结果还是1。
而p->sleep()是基类指针,指向的sleep()是一个虚函数,由于每个虚函数都有一个虚函数表,此时p调用sleep()并不是直接调用函数,而是通过虚函数表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的sleep()函数的地址,因此输出的结果也会是子类的结果4。
通过上面的例子做个总结就是:指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
什么函数不能声明为虚函数?
- 非类的成员函数(比如友元函数、全局函数)不能定义为虚函数,非类成员函数只能被重载(overload),不能被继承(override),而虚函数主要的作用是在继承中实现动态多态,非成员函数早在编译期间就已经绑定函数了,无法实现动态多态,那声明成虚函数还有什么意义呢?
- 类的成员函数中静态成员函数不能定义为虚函数,因为静态成员函数全局通用,不受限于某个具体对象。
- 构造函数也不能定义为虚函数, (1)构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定 (2) 要想调用虚函数必须要通过“虚函数表”来进行的,但虚函数表是要在对象实例化之后才能够进行调用。而在构造函数运行期间,还没有为虚函数表分配空间,自然就没法调用虚函数。
但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
(2)纯虚函数
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能够实例化,但可以声明指向实现该抽象类的具体类的指针或引用。
为啥引入纯虚函数呢?
- 为了方便使用多态特性,我们常常需要在基类中定义虚函数。
- 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,"你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它"。
说了这么多,来个实例看看吧:
#include <iostream>
using namespace std;
// 抽象类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total Rectangle area: 35
Total Triangle area: 17
从上面的实例中,我们可以看到一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。