C++面试之虚函数

自己参加过几次面试,总是一问三不知,感觉都会用,可是深层次的原理不清楚,

这对工作了几年的程序员来说,简直是全盘的否定,当然这是自己的问题。

定下决心要从基本开始,全面巩固,起码对得起自己的码农生涯。


就从虚函数开始吧。


一、什么是虚函数?

C++有三大特性,封装,继承和多态。虚函数就是多态的具体表现,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

面试的时候说对了意思大体就可以了。

顺便解释一下,什么是多态?

这篇文章写的很清楚:点击打开链接

相同对象接收到不同消息或者不同对象接收到相同消息是产生的动作就是多态,实际应用中更偏向不同对象接收到相同消息

相同对象接收到不同消息对应于静态多态(早绑定)

例如:

class Rect       //矩形类
{
public:
    int calcArea(int width);
    int calcArea(int width,int height);
};
//他们函数名相同,参数个数不同,是互为重载的两个函数
int main()
{
    Rect.rect;
    rect.calcArea(10);
    rect.calcArea(10,20);
    return 0;
}
//程序在编译阶段根据参数个数确定调用哪个函数。这种情况叫做静态多态(早绑定)

不同对象接收到相同消息对应于动态多态(晚绑定)

例如:

比如计算面积 当给圆形计算面积时使用圆形面积的计算公式,给矩形计算面积时使用矩形面积的计算公式。也就是说有一个计算面积的形状基类,圆形和矩形类派生自形状类,圆形与矩形的类各有自己的计算面积的方法。可见动态多态是以封装和继承为基础的。


class Shape//形状类
{
public:
    double calcArea()
    {
        cout<<"calcArea"<<endl;
        return 0;
    }
};
class Circle:public Shape      //公有继承自形状类的圆形类
{
public:
    Circle(double r);
    double calcArea();
private:
    double m_dR;
};
double Circle::calcArea()
{
    return 3.14*m_dR*m_dR;
}
class Rect:public Shape       //公有继承自形状类的矩形类
{
public:
    Rect(double width,double height);
    double calArea();
private:
    double m_dWidth;
    double m_dHeight;
};
double Rect::calcArea()
{
    return m_dWidth*m_dHeight;
}
int main()
{
    Shape *shape1=new Circle(4.0);
    Shape *shape2=new Rect(3.0,5.0);
    shape1->calcArea();
    shape2->calcArea();
    .......
    return 0;
}
实际输出的结果却是 打印两行"calcArea",也就是执行了父类的函数,想要达到需要的效果,需要用到虚函数,也就是需要Virtual关键字,也就是实现动态多态则必须使用虚函数。


二、虚函数的原理:虚函数表

需要了解的是:

(1) 32位系统 指针长度为4字节, 64位系统 指针长度为8字节
(2) new一个对象时, 只为类中成员变量分配空间, 对象之间共享成员函数。

(3) 空类只占一个字节

如下:

class Base {
public:
    virtual void f() {cout<<"base::f"<<endl;}
    virtual void g() {cout<<"base::g"<<endl;}
    virtual void h() {cout<<"base::h"<<endl;}
};
64位系统下打印sizeof(Base)会输出8,说明编译器在类中自动添加了一个8字节的成员变量, 这个变量就是_vptr也就是指向虚函数表的指针。

面试问题一般会有虚函数指针占多少字节?这当然与虚函数没关系,就是一个指针的大小。

_vptr是虚函数表指针,指向虚函数表的首地址,虚函数表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。


当实例化一个该类的子类对象的时候,如果该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。


 如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。


面试的问题一般有虚函数表存在基类还是派生类?如上所说的如果基类有虚函数的话,那么基类和派生类的对象都会有虚函数表的指针,指针不一样,但是指针指向的虚函数表是一样的。


形象一点就是这样:

class Base {
public:
    virtual void f() {cout<<"base::f"<<endl;}
    virtual void g() {cout<<"base::g"<<endl;}
    virtual void h() {cout<<"base::h"<<endl;}
};

class Derive : public Base{
public:
    void g() {cout<<"derive::g"<<endl;}
};

Base中的虚函数表是这样的:


Derive中的虚函数表是这样的:




多重继承情况下应该是这样的:


三、纯虚函数

纯虚函数没有函数体,同时在定义的时候函数名后面要加“=0”。


纯虚函数的实现原理:
在虚函数原理的基础上,虚函数表中,虚函数的地址是一个有意义的值,如果是纯虚函数就实实在在的写一个0。
含有纯虚函数的类被称为抽象类
含有纯虚函数的类被称为抽象类,比如上面代码中的类就是一个抽象类,包含一个计算周长的纯虚函数。哪怕只有一个纯虚函数,那么这个类也是一个抽象类,纯虚函数没有函数体,所以抽象类不允许实例化对象,抽象类的子类也可以是一个抽象类。抽象类子类只有把抽象类当中的所有的纯虚函数都做了实现才可以实例化对象。
对于抽象的类来说,我们往往不希望它能实例化,因为实例化之后也没什么用,而对于一些具体的类来说,我们要求必须实现那些要求(纯虚函数),使之成为有具体动作的类。
近含有纯虚函数的类称为接口类
如果在抽象类当中仅含有纯虚函数而不含其他任何东西,我们称之为接口类。

四、参考文献

http://www.cnblogs.com/jin521/p/5602190.html

http://www.cnblogs.com/hushpa/p/5707475.html

http://blog.csdn.net/li_ning_/article/details/51893748

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值