虚函数
百度百科对于虚函数的解释:
定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数 [1]语法:virtual 函数返回类型 函数名(参数表) { 函数体 }用途 :实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数
函数声明和定义和普通的类成员函数一样,只是在返回值之前加入了关键字“virtual”声明为虚函数。而虚函数是实现多态的重要手段,意思是只有对虚函数的调用才能动态决定调用哪一个函数,这是相对于普通成员函数而言的,普通的成员函数在编译阶段就能确定调用哪一个函数。举个栗子:
#include <stdio.h>
class A {
public:
void fn() { printf("fn in A\n"); }
virtual void v_fn() { printf("virtual fn in A\n"); }
};
class B : public A {
public:
void fn() { printf("fn in B\n"); }
virtual void v_fn() { printf("virtual fn in B\n"); }
};
int main() {
A *a = new B();
a->fn();
a->v_fn();
return 0;
}
基类A有两个成员函数fn和v_fn,派生类B继承自基类A,同样实现了两个函数,然后在main函数中用A的指针指向B的实例(向上转型,也是实现多态的必要手段),然后分别调用fn和v_fn函数。结果是“fn in A"和"virtual fn in B"。这是因为fn是普通成员函数,它是通过类A的指针调用的,所以在编译的时候就确定了调用A的fn函数。而v_fn是虚函数,编译时不能确定,而是在运行时再通过一些机制来调用指针所指向的实例(B的实例)中的v_fn函数。假如派生类B中没有实现(完全一样,不是重载)v_fn这个函数,那么依然会调用基类类A中的v_fn;如果它实现了,就可以说派生类B覆盖了基类A中的v_fn这个虚函数。这就是虚函数的表现和使用,只有通过虚函数,才能实现面向对象语言中的多态性。
a指针虽然是A*类型的,但是它却调用了B中的v_fn,因为不管是A类,还是A的基类,都会有一个变量vftable,它指向的虚函数表中保存了正确的v_fn入口。所以a->v_fn()实际做的工作就是从a指向的实例中取出vftable的值,然后找到虚函数表,再从表中去的v_fn的入口,进行调用。不管a是指向A的实例,还是指向B的实例,a->fn()所做的步骤都是上面说的一样,只是A的实例和B的实例有着不同的虚函数表,虚函数表里也保存着可能不同的虚函数入口,所以最终将进入不同的函数调用中。通过表来达到不用判断类型,亦可实现多态的作用。还有一点指的提醒的是,因为虚函数表是一个常量表,在编译时,编译器会自动生成,并且不会改变,所以如果有多个B类的实例,每个实例中都会有一个vftable指针,但是它们指向的是同一个虚函数表。
上面一段中说到了,A和B的实例有着不同的虚函数表,但是虚函数表中只是可能保存着不同的v_fn,那是因为C++允许派生类不覆盖基类中的虚函数,意思就是假如派生类B中没有实现v_fn这个函数(不是重载),那么B的实例的虚函数表会保存着基类A中v_fn的入口地址。也就是说B类不实现v_fn函数,但是它同样提供了这个接口,实际上是调用基类A中的v_fn。假如某个类只是一个抽象类,抽象出一些列接口,但是又不能实现这些接口,而要有派生类来实现,那么就可以把这些接口声明为纯虚函数,包含有纯虚函数的类称为抽象类。纯虚函数是一类特殊的虚函数,它的声明方式如下:
class A {
public:
virtual 返回值 函数名(参数表)= 0;
};
在虚函数声明方式后加一个“=0”,并且不提供实现。抽象类不允许实例化(这样做编译器会报错,因为有成员函数没有实现,编译器不知道怎么调用)。纯虚函数的实现机制和虚函数类似,只是要求派生类类必须自己实现一个(也可以不实现,但是派生类也会是个抽象类,不能实例化)。
1、以下代码的输出结果是什么?
#include<iostream>
using namespace std;
class A
{
protected:
int m_data;
public:
A(int data = 0)
{
m_data = data;
}
int GetData()
{
return doGetData();
}
virtual int doGetData()
{
return m_data;
}
};
class B : public A
{
protected:
int m_data;
public:
B(int data = 1)
{
m_data = data;
}
int doGetData()
{
return m_data;
}
};
class C : public B
{
protected:
int m_data;
public:
C(int data = 2)
{
m_data = data;
}
};
int main ()
{
C c(10);
cout << c.GetData() <<endl;
cout << c.A::GetData() <<endl;
cout << c.B::GetData() <<endl;
cout << c.C::GetData() <<endl;
cout << c.doGetData() <<endl;
cout << c.A::doGetData() <<endl;
cout << c.B::doGetData() <<endl;
cout << c.C::doGetData() <<endl;
return 0;
}
解析: 构造函数从最初始的基类开始构造的,各个类的同名变量没有形成覆盖,都是单独的变量.理解这两个重要的C++特性后解决这个问题就比较轻松了,下面我们看看:
cout << c.GetData() <<endl;
本来是要调用C类的GetData(),C中未定义,故调用B中的,但是B中也未定义,故调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。
cout << c.A::GetData() <<endl;
因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。
cout << c.B::GetData() <<endl;
肯定是B类的返回值 1 了。
cout << c.C::GetData() <<endl;
跟cout << c.GetData() <<endl;语句是一样的。
cout << c.doGetData() <<endl;
B类的返回值 1 了。
cout << c.A::doGetData() <<endl;
因为直接调用了A的doGetData() ,所以输出0。
cout << c.B::doGetData() <<endl;
cout << c.C::doGetData() <<endl;
这两个都是调用了B的doGetData(),所以输出 1。
这里要注意存在一个就近调用,如果父类存在相关接口则优先调用父类接口,如果父类也不存在相关接口则调用祖父辈接口。
答案:
1 1 1 1 1 0 1 1
2、以下代码输出结果是什么?
#include<iostream>
using namespace std;
class A
{
public:
void virtual f()
{
cout<<"A"<<endl;
}
};
class B : public A
{
public:
void virtual f()
{
cout<<"B"<<endl;
}
};
int main ()
{
A* pa=new A();
pa->f();
B* pb=(B*)pa;
pb->f();
delete pa,pb;
pa=new B();
pa->f();
pb=(B*)pa;
pb->f();
return 0;
}
解析: 这是一个虚函数覆盖虚函数的问题。A类里的f()函数是一个虚函数,虚函数是被子类同名函数所覆盖的。而B类里的f()函数也是一个虚函数,它覆盖A类f()函数的同时,也会被它的子类覆盖。但是在 B* pb=(B*)pa;里面,该语句的意思是转化pa为B类型并新建一个指针pb,将pa复制到pb。但是这里有一点请注意,就是pa的指针始终没有发生变化,所以pb也指向pa的f()函数。这里并不存在覆盖的问题。
delete pa,pb;删除了pa和pb所指向的地址,但是pa、pb指针并没有删除,也就是我们通常说的悬浮指针,现在重新给pa指向新地址,所指向的位置是B类的,而之前pa指针类型是A类的,所以就产生了一个覆盖。pa->f();的值是B。
pb=(B*)pa;转化pa为B类指针给pb赋值,但pa所指向的f()函数是B类的f() 函数,所以pb所指向的f()函数是B类的f()函数。pb->f();的值是B。
答案:
A A B B
3、派生类的3种继承方式?
答案:
(1)公有继承方式:
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见,基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的。
(2)私有继承方式:
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所以成员都是不可见的。
所以说,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
(3)保护继承方式:
这种继承方式与私有继承方式的情况相同,两者的区别仅在于对派生类的成员而言,基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问;基类的私有成员不可见,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所以成员都是不可见的。
所以说,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
4、下面程序运行结果是什么?
#include<iostream>
using namespace std;
class A
{
char k[3];
public:
virtual void aa(){};
};
class B : public virtual A
{
char j[3];
public:
virtual void bb(){};
};
class C : public virtual B
{
char i[3];
public:
virtual void cc(){};
};
int main ()
{
cout << "sizeof(A):" << sizeof(A) << endl;
cout << "sizeof(B):" << sizeof(B) << endl;
cout << "sizeof(C):" << sizeof(C) << endl;
return 0;
}
解析: (1)对于A类,由于有一个虚函数,那么必须有一个对应的虚函数表来记录对应的函数入口地址。每个地址需标有一个虚指针,指针的大小为4。类中还有一个char k[3],每一个char值所占空间是1,所以char k[3]所占大小是3。做一个数据对齐后变为4。所以,sizeof(A)的结果就是char k[3]所占大小4和虚指针所占大小4之和等于8。
(2)对于B类,由于B继承了A,同时还拥有自己的虚函数,那么B中首先拥有一个vfptr_B,指向自己的虚函数表。还有char j[3],大小为4,可虚继承该如何实现呢?首先要通过加入一个虚类指针(记vbptr_B_A)来指向其父类,然后还要包含父类的所有内容,所以sizeof(B)的大小是:A类所占大小8,char j[3]所占大小4,vfptr_B所占大小4,vbptr_B_A所占大小4,它们之和等于20。
(3)对于C类和B类差不多,结果是32。
答案:
sizeof(A):8
sizeof(B):20
sizeof(C):32
5、什么是虚继承?它与一般的继承有什么不同?它有什么用?写出一段虚继承的C++代码。
答案:
虚拟继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,请看下图:
在图 1中,类D接触自类B和类C,而类B和类C都继承自类A,因此出现了图 2所示的情况。
在图 2中,类D中会出现两次A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A成了虚拟基类。最后形成了图 3。
代码如下:
class
A;
class
B :
public
virtual
A;
class
C :
public
virtual
A;
class
D :
public
B,
public
C;