前面我们说了虚函数实现了运行时的多态,但是没有详细说明虚函数的具体实现机制,所以这里详细说明一下虚函数,并对多态进行一下补充
上一篇链接:C++多态详解_真的没事鸭的博客-CSDN博客
目录
1,虚函数定义时的注意事项:
1,类中的静态成员函数不可声明为虚函数
2,派生类对基类函数重新定义时,必须与基类中虚函数的原型完全一致
2,虚函数的作用:
虚函数是运行时多态,若某个基类函数声明为虚函数,则其公有派生类将定义与其基类虚函数原型相同(函数名、返回值类型、参数个数、参数类型及参数顺序均相同)的函数,这时,当使用基类指针或基类引用操作派生类对象时,系统会自动用派生类中的同名函数代替(覆盖)基类虚函数。
下面看一个例子:
#include <iostream>
using namespace std;
class A
{
public:
void display()
{
cout << "输出基类函数" << endl;
}
};
class B :public A
{
public:
void display()
{
cout << "输出派生类函数" << endl;
}
};
int main()
{
A* p = new A();
p->display();
p = new B();
p->display();
}
输出结果:
输出基类函数
输出基类函数
从这个结果可以看出来,我们定义一个基类指针,先指向了基类,然后调用了基类的display函数,之后我们又将这个基类指针指向了派生类,调用派生类的display函数, 但是我们看输出结果可以发现并没有调用派生类的display函数,并没有实现我们想要的结果。
原因:派生类继承并改写了基类同名函数display(),这种改变在静态联编(编译时)条 件下,编译器并不知道,造成了上述结果。
解决方法:若想通知编译器这种改变,则需要通过动态联编(运行时多态),实现方法就是在基类中将可能发生改变的成员函数声明为虚函数。
3,虚函数的声明形式
在成员函数原型前添加virtual关键字:
class 类名
{
virtual 函数返回值类型 函数名(参数表)
};
含义:
声明为虚函数的成员函数,可以被派生类重新定义。
注意:
1,虚函数只能是类中的成员函数,但类中的静态成员函数(归类所拥有,不归某一对象所有)不可声明为虚函数。
2,派生类对基类虚函数重新定义时,必须与基类中虚函数的原型完全一致。包括函数名、返回值类型、参数个数、参数类型及参数顺序。
3,无论派生类中同名函数前是否添加virtual,均被视为虚函数(可在其派生类中再次重新定义,即虚函数是被继承的)。
4,虚函数有两种:一般虚函数和虚析构函数
1,一般虚函数
对于普通成员函数,派生类可以重新定义从基类继承下来的虚函数。派生类对基类虚函数重新定义后,仍作为虚函数可在更下层派生类中被重新定义。
通常,在派生类中重新定义虚函数时,可以不写virtual,但最好保留,以增强程序的可 读性。
我们对上面的代码进行一下修改
#include <iostream>
using namespace std;
class A
{
public:
int a = 10;
public:
virtual void display()
{
cout << "输出基类函数" << endl;
}
};
class B :public A
{
public:
void display()
{
cout << "输出派生类函数" << endl;
a += 10;
}
};
int main()
{
A* p = new A();
p->display();
p = new B();
p->display();
}
在基类的display函数前面加上virtual关键字,将成员函数声明为虚函数,这样就可以通过p指针调用派生类的函数了
结论:有了虚函数后,通过基类指针或基类引用调用派生类对象的虚函数时,会实际调用指针或引用指向的派生类对象中那个重定义版本,即操作派生类的虚函数。
虚函数通过动态联编实现了运行时多态
什么是联编
联编是指将源代码中的函数调用解释为执行特定的函数代码块
在c语言中的函数没有重载一说,所以一个函数就对应一个函数代码块,但是在c++中允许函数重载,所以比c语言复杂,必须根据函数名和形参列表来对应一个函数代码块。
那什么是静态联编和动态联编呢?
静态联编:在编译过程进行联编
动态联编:编译器生成能够在程序运行时选择正确的虚方法的代码
在编译过程中进行联编就是静态联编,但是虚函数的存在使静态联编变得困难,因为子类对继承父类的函数进行了重写,当我们用一个父类指针指向一个子类对象时,编译器阶段可以知道父类指针的类型,然后调用父类指针类型的函数。但是我们想要调用的是子类继承父类后重写的函数。
所以便有了动态联编,对于虚函数,编译器可以通过动态联编确定对应的函数代码块, 即在运行时根据对象类型,针对不同的对象调用对应的函数代码块。
总结:
可以简单的理解为,静态联编在编译时根据对象的类型,调用对应的函数代码块,动态联编在运行时指针不同的对象类型调用对应的函数代码块
2,虚析构函数
在C++中不能声明虚构造函数,因为构造函数执行时,对象还没有构建好,不可能按虚函数方式进行调用,但可以声明虚析构函数。
使用虚析构函数的目的
虚析构函数是为了解决基类指针指向派生类对象,并用基类指针销毁派生类对象的应用产生的。(注意:动态内存的分配和回收必须使用指针变量来存放空间地址)
通常,使用基类指针指向一个new生成的派生类对象,通过delete销毁基类指针指向的派生类对象时,有以下两种情况:
如果基类析构函数不是虚析构函数,则只会调用基类的析构函数,派生类的析构函数不被调用,此时派生类中的申请资源不被回收。
如果基类析构函数为虚析构函数,则释放基类指针指向的对象时会调用基类及派生类析构函数,派生类对象中的所有资源被回收。
虚析构函数的声明形式:
virtual ~类名(;)
下面看一个例子
#include <iostream>
using namespace std;
class Father
{
public:
~Father()
{
cout << "调用父类析构函数" << endl;
}
};
class Son :public Father
{
public:
~Son()
{
cout << "调用基类虚构函数" << endl;
}
};
int main()
{
Father* f=new Son();
delete f;
}
输出结果
调用父类析构函数
从结果可以看出来,我们将父类指针指向派生类对象,销毁指针时只会调用父类的析构函数,派生类的申请资源没有被回收
我们使用虚析构函数修改一下上面的代码
#include <iostream>
using namespace std;
class Father
{
public:
virtual ~Father()
{
cout << "调用父类析构函数" << endl;
}
};
class Son :public Father
{
public:
~Son()
{
cout << "调用基类虚构函数" << endl;
}
};
int main()
{
Father* f=new Son();
delete f;
}
输出结果
调用基类虚构函数
调用父类析构函数
通过结果可以发现,使用虚析构函数之后,销毁基类指针会调用基类和派生类的析构函数,派生类的资源也会被回收,资源全部被释放
结论:
虚析构函数可以完成基类及派生类对象资源的释放,因此在继承结构中,通常将基类析构函数声明为虚函数。
5,虚函数的实现机制
虚函数通过动态联编实现了运行时多态,编译器在执行过程中遇到virtual关键字时, 会为这些包含虚函数的类建立一张虚函数表vtable。在虚函数表中,编译器将按照虚函 数的声明顺序依次保存虚函数地址,同时在每个带有虚函数的类中放置一个vptr指针, 用来指向虚函数表,通常在定义类对象时,为vptr分配空间,该指针被置于对象的起始位置,继而通过对象的构造函数将vptr初始化为本类的虚函数表地址。
下面看三个实例
1,派生类不重写基类虚函数
class Father
{
int father;
virtual void A();
virtual void B();
};
class Son :public Father
{
int son;
virtual void C();
};
解析一下这两个类的内存布局
派生类不重写基类虚函数的话,基类和派生类的虚函数表如上图所示,可见在派生类的虚函数表中,派生类的虚函数是在基类虚函数的后面
2,派生类重写基类函数
class Father
{
int father;
virtual void A();
virtual void B();
};
class Son :public Father
{
int son;
Virtual void B();
virtual void C();
};
派生类重写基类的虚函数的话,基类和派生类的虚函数表如图所示。经过重写后,基类的虚函数表很好构造,派生类的虚函数表构造相对复杂,构造方式是先拷贝基类的虚函数表,替换已重写的虚函数指针,追加派生类自己的虚函数指针。
3,多重继承
class Father
{
int f;
virtual void A();
virtual void B();
};
class Mother
{
Int m;
virtual void C();
virtual void D();
};
class Son :public Father,public Mother
{
int s;
virtual void B();
virtual void C();
virtual void E();
};
多重继承的虚函数表和前面类似,不过多了一个虚函数表,多了一次拷贝和替换的过程
虚函数调用过程
我们以上面的派生类重写基类函数为例
class Father
{
int father;
virtual void A();
virtual void B();
};
class Son :public Father
{
int son;
Virtual void B();
virtual void C();
};
void test(Father* p)
{
p->B();
}
对应的虚函数表为
这里编译器只知道p是Father类型的指针,并不知道它具体的指向,它可能指向Father 也可能指向Son
需要注意的是,虚函数指针中的vptr部分是虚函数表中偏移值(单位是字节)加1,也 就是说如果我们确定了p指向的类型,再确定被调函数在虚函数表中的偏移值,在运行时就可以调用对应的函数了
不管是在Father的虚函数表,还是Son的虚函数表中,B函数在各自虚函数表的偏移位置是相等的,都在第二个位置
比如Father::B是一个虚函数指针,它的vptr部分是9,它在Father虚函数表的偏移值为8(8+1=9)
当程序执行到p->B()时,就可以根据指向的对象,判断具体调用的函数
如果p指向Father的对象,可以获取到Father对象的vptr,加上偏移值8((char*)vptr+8) 可以找到Father::B
如果p指向Son对象,可以获取到Son对象的vptr,加上偏移值8((char*)vptr+8)可以找 到Son::B
如果p指向其他类型对象,同理
6,纯虚函数
在定义一个表示抽象概念的基类时,有时无法或者不需要给出某些成员函数的具体实现, 函数的实现在派生类中完成,基类中这样的函数声明为纯虚函数。
例如:动物都有叫声,但不同的动物叫声不相同,因此,不需要在动物类中实现描述叫声的函数,而只需保留一个接口,具体的实现在各派生类中完成。
纯虚函数的用法
纯虚函数不存在函数体,只有函数声明,用来在基类中为派生类保留一个函数接口,方 便派生类根据需要对它实现,实现多态。因此纯虚函数通常在基类中声明,派生类根据 自身需要提供函数体,实现纯虚函数。
纯虚函数的声明形式
virtual 函数返回值类型 函数名(参数表) = 0;
注意:
虽然纯虚函数与虚函数都用virtual关键字声明,但纯虚函数只有函数名,没有函数体, 不是完整的函数,不具备函数功能,无法被调用,也无法为其分配内存空间。
声明后的“=0”并不表示函数返回值为0,只是以这样的形式说明该函数为纯虚函数。
若在一个类中声明了纯虚函数,但是在其派生类中没有实现该函数,则该函数在派生类中仍为纯虚函数(不可被调用)。即:基类中的纯虚函数(没有函数体)必须在派生类中有实现部分,否则不可调用。而基类中的虚函数可以为空函数,只是在派生类中可以重定义而已。
下面看一个案例
#include <iostream>
using namespace std;
class Anima
{
public:
virtual void Speak() = 0;
};
class Dog :public Anima
{
public:
void Speak()
{
cout << "Dog叫" << endl;
}
};
class Cat :public Anima
{
public:
void Speak()
{
cout << "Cat叫" << endl;
}
};
class Chicken :public Anima
{
public:
void Speak()
{
cout << "Chicken叫" << endl;
}
};
int main()
{
Anima* p;
p = new Cat();
p->Speak();
p = new Dog();
p->Speak();
p = new Chicken();
p->Speak();
}
输出结果:
Cat叫
Dog叫
Chicken叫
7,抽象类
包含纯虚函数的类称为抽象类。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。上面例子中基类Anima中声明了纯虚函数Speak(),则Anima类是抽象类。
抽象类的用途
建立公共接口,在各派生类中完成各自的实现,更好地发挥多态的特性。 抽象类声明了公共接口,而接口的完整实现(即纯虚函数的函数体)要由派生类自己定 义。
抽象类的定义形式
class 类名
{
public:
virtual 函数返回值类型 函数名(参数表) = 0;
其他函数声明
};
使用纯虚函数和抽象类的注意事项
抽象类只是定义操作接口,不是一个类的完整实现,只能做基类来派生新类,不能声明抽象类对象,但可以声明抽象类指针或引用,通过指针或引用操作派生类对象。
抽象类中可以有多个纯虚函数,在派生类中应该实现这些纯虚函数,使得派生类不再是抽象类。派生类中若没有实现所有纯虚函数,则未重新定义的函数仍为纯虚函数,派生类也是抽象类。
总结:
基类指针指向派生类对象,如果基类声明的不是虚函数就调用基类的,如果基类中是虚函数并且在派生类中实现,就调用派生类的函数。
基类指针指向派生类对象,但只能访问派生类从基类继承的那些成员函数,若想访问只在派生类才有的成员函数,则只能通过在基类中声明为纯虚函数或虚函数来实现。
抽象类的应用
实际开发应用时:
(1)可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁 派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。 虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。
(2)抽象基类除了约束派生类的功能,还可以实现多态。这是C++提供纯虚函数的主 要目的。
注意
(1)一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外, 还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
(2)只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明 为纯虚函数。
8,内部类
C++允许在类内部定义类,这样的类称为内部类,这个内部类所在的类称为外部类。内 部类可以作为外部类的基础,外部类在内部类基础上扩充新的功能并且不会相互影响。
定义形式
class 外部类名
{
外部类成员;
访问限定符
class 内部类名
{
内部类成员;
};
};
内部类了解即可,这里不做过多说明
章末总结
最需要掌握还是虚函数和纯虚函数,尤其是虚函数的实现机制一定要搞懂,明白是怎么实现的。最后,如有错漏之处,敬请指正!