C++函数之类的虚函数和纯虚函数区别与详解

C++面向对象语言的一大特性就是抽象,在程序设计上的体现就是鼓励面向接口编程,而不要面向具体实现编程。这里所说的抽象和接口与C++的多态性密切相关。C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。虚函数是C++语言一个非常重要的特性,不同编译器对此特性的实现机制也略有差别,虚函数在某些情况下对程序的占用内存大小和执行效率有比较明显的影响,这时候知道虚函数背后的实现原理,知其然、知其所以然是很有必要的

首先:强调一个概念
定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

一、 什么是虚函数

简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性 (Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略,虚函数是C++ 的多态性的主要体现,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。

来看一个虚函数的例子:

class A
{
public:
    virtual void foo()
    {
       cout<<"A::foo() is called"<<endl;
    }
};
class B:public A
{
public:
    void foo()
    {
       cout<<"B::foo() is called"<<endl;
    }
};
int main(void)
{
    A *a = new B();
    a->foo();  // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

从上面的例子可以看出一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

 

二、 什么是纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
 virtual void funtion1()=0

引入原因
  1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

 

有纯虚函数的类是不可能生成类对象的,如果没有纯虚函数则可以。比如: 

class CA 
{ 
public: 
    virtual void fun() = 0;  // 说明fun函数为纯虚函数 
    virtual void fun1(); 
}; 

class CB 
{ 
public: 
   virtual void fun(); 
   virtual void fun1(); 
}; 

// CA,CB类的实现 
... 

void main() 
{ 
    CA a;   // 不允许,因为类CA中有纯虚函数 
    CB b;   // 可以,因为类CB中没有纯虚函数 
    ... 
} 

虚函数在多态中间的使用:  多态一般就是通过指向基类的指针来实现的。 

用父类的指针在运行时刻来调用子类时: 
例如,有个函数是这样的: 
void animal::fun1(animal *maybedog_maybehorse) 

     maybedog_maybehorse->born();


参数maybedog_maybehorse在编译时刻并不知道传进来的是dog类还是horse类,所以就把它设定为animal类,具体到运行时决定了才决定用那个函数。也就是说用父类指针通过虚函数来决定运行时刻到底是谁而指向谁的函数。

不用虚函数:

#include<iostream>
usingnamespace std;
classanimal
{
public:
    animal();
    ~animal();
    void fun1(animal *maybedog_maybehorse);
    void born();
};
 
voidanimal::fun1(animal *maybedog_maybehorse)
{
    maybedog_maybehorse->born();
}
 
animal::animal() { }
animal::~animal() { }
voidanimal::born()
{
    cout << "animal";
}
horse
classhorse :publicanimal
{
public:
    horse();
    ~horse();
    void born();
};
 
horse::horse() { }
horse::~horse() { }
voidhorse::born()
{
    cout << "horse";
}
main
void main()
{
    animal a;
    horse b;
    a.fun1(&b);
}
output:animal

用虚函数:

#include<iostream>
usingnamespace std;
classanimal
{
public:
    animal();
    ~animal();
    void fun1(animal *maybedog_maybehorse);
    virtualvoid born();
};
 
voidanimal::fun1(animal *maybedog_maybehorse)
{
    maybedog_maybehorse->born();
}
 
animal::animal() { }
animal::~animal() { }
voidanimal::born()
{
    cout << "animal";
}
horse
classhorse :publicanimal
{
public:
    horse();
    ~horse();
    virtualvoid born();
};
 
horse::horse() { }
horse::~horse() { }
voidhorse::born()
{
    cout << "horse";
}
main
void main()
{
    animal a;
    horse b;
    a.fun1(&b);
}

output:horse


抽象类的介绍
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有纯虚函数的类为抽象类。
(2)抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意:
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
• 抽象类是不能定义对象的。



看一道面试题:

#include <iostream>
#include <cstdio>
 
using namespace std;
 
class A
{
public:
   void foo()
    {
       printf("1\n");
    }
   virtual void fuu()
    {
       printf("2\n");    
    }
};
 
class B:public A
{
public :
   void foo()
    {
       printf("3\n");
    }
   void fuu()
    {
       printf("4\n");
    }
};
 
int main()
{
    A a;
    B b;
   
    A*p = &a;
   cout<< "p->foo()---" ; p->foo() ;
   cout<<"p->fuu()---";p->fuu();   
 
   cout <<"-------向上转型-----------"<<endl;
   p=&b;
   cout<<"p->foo()---";p->foo();   
   cout<<"p->fuu()---";p->fuu();   
   
   cout <<"--------向下转型----------"<<endl;
   
    B*ptr =(B *)&a;
   cout<<"ptr->foo()----";ptr->foo();
   cout<<"ptr->fuu()-----";ptr->fuu();
   return 0;
}


 

运行结果:

下面进行详细分析一下为什么结果是这样的??你全做对了没??

        第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
  第二个输出结果就是1、4。p->foo()和p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p->fuu()指针是基类指针,指向的fuu是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fuu()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fuu()函数的地址,因此输出的结果也会是子类的结果4.

 

  第三个并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。

  第四个:而ptr->fuu()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中foo()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。

 

小结:1.有virtual才可能发生多态现象

     2.不发生多态(无virtual)调用就按原类型调用

 

最后再来看一个例子,领略虚函数的功能:

#include<iostream>
#include<conio.h>
using namespace std;
 
class Parent
{
public:
    char data[20];
    void Function1();
    virtual void Function2();
}parent;
 
void Parent::Function1()
{
    printf("This is parent,function1\n");
}
 
void Parent::Function2()
{
    printf("This is parent,function2\n");
}
 
class Child:publicParent
{
    void Function1();
    void Function2();
}child;
 
void Child::Function1()
{
    printf("This is child,function1\n");
}
 
void Child::Function2()
{
    printf("This is child,function2\n");   
}
 
 
int main()
{
    Parent *p;
    if(_getch()=='c')
        p =&child;
    else
        p=&parent;
 
    p->Function1();
   
    p->Function2();
 
    return 0 ;
 
   
}



输入非c字符:运行结果:


 

输入c字符:运行结果:




参考资料:http://blog.csdn.net/yusiguyuan/article/details/12676177

http://www.cppblog.com/ElliottZC/archive/2007/07/20/28417.html

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值