C++:虚函数的详解

5.4.2 虚函数详解

1.虚函数的定义
虚函数就是在基类中被关键字virtual说明,并在派生类重新定义的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual。定义虚函数的格式如下:

virtual 函数类型 函数名(形参表)
{
     函数体
}
在基类中的某个成员函数声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

//例 5.21 虚函数的使用

#include<iostream>
using namespace std;
class B0{
 public:
   virtual void print(char *p)      //定义基类B0中的虚函数
   {
     cout<<p<<"print()"<<endl;
   }
};
class B1:public B0{
  public:

    virtual void print(char *p)     //定义基类B0的公有派生类B1中的虚函数
    {
     cout<<p<<"print()"<<endl;
    }
};
class B2:public B1{
  public:
    virtual void print(char *p)     //定义基类B1的公有派生类B2中的虚函数
    {
     cout<<p<<"print()"<<endl;
    }
};
int main()
{
 B0 ob0,*op;                  //定义基类对象ob0和对象指针op
 op=&ob0;op->print("B0::");   //调用基类的B0的print()
 B1 ob1;                      //定义派生类B1的对象ob1
 op=&ob1;op->print("B1::");   //调用派生类B1的print()
 B2 ob2;                      //定义派生类B2的对象ob2
 op=&ob2;op->print("B2::");   //调用派生类B2的print()
 return 0;
}
 /*
   在程序中,语句op->print();
   出现了3次,由于op指向的对象不同,每次出现都执行了相应对象的虚函数print

   程序运行结果:
     B0::print()
     B1::print()
     B2::print()

说明:
(1)若在基类中,只是声明虚函数原型(需要加上virtual),而在类外定义虚函数时,则不必再加上virtual。

(2)在派生类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

(3)C++规定,当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。因此,在派生类中重新定义该虚函数时,关键字virtual可写可不写。但是为了程序更加清晰,最好在每一层派生类中定义函数时都加上关键字virtual。

(4)如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。
一个虚函数无论被公有继承多少次。它仍然保持其虚函数的特性。
例如:
class B0{
      ...
      public:
              virtual void show(); //在基类中定义show为虚函数
};
class B1:public B0{
      ...
};
若在公有派生类B1中没有重新定义虚函数show,则函数在派生类中被继承,仍然是虚函数。
(5)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数的调用要靠特定的对象来决定该激活哪个函数。

(6)虽然使用对象名和点运算符的方式也可以调用虚函数,但是这种调用是在编译时进行的,是静态联编,它没有利用虚函数的特性。只有通过指针访问虚函数时才能获得运行时的多态性。

2. 虚析构函数

在C++中,不能声明虚构造函数,但是可以声明虚析构函数。
//例5.23 虚析构函数的引例

#include<iostream>
using namespace std;
class B{
  public:
    ~B()
    {
     cout<<"调用基类B的析构函数\n";
    }
};
class D:public B{
  public:
    ~D()
    {
     cout<<"调用派生类D的析构函数\n";
    }
};
int main()
{
 D obj;
 return 0;
}
*/
/*
运行结果是:
调用派生类D的析构函数
调用基类B的析构函数

显然本程序的运行结果是符和预想的。但是,如果在主函数中用new运算符建立一个无名对象
和定义了一个基类的对象指针,并将无名的对象的地址赋给这个对象指针。当用delete运算符
撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。
例如下面的例子:
*/

//例5.24 虚析构函数的引例2

#include<iostream>
using namespace std;
class B{
  public:
    ~B()
    {
     cout<<"调用基类B的析构函数\n";
    }
};
class D:public B{
  public:
    ~D()
    {
     cout<<"调用派生类D的析构函数\n";
    }
};
int main()
{
 B *p;          //定义指向基类B的指针变量p
 p = new D;     //用new运算符为派生类的无名对象动态的分配了一个存储空间,并将地址赋给对象指针p
 delete p;      //用delete撤销无名对象时,释放动态存储空间
 return 0;
} /*
程序运行结果:
调用基类B的析构函数

程序结果表示,本程序只执行了基类B的析构函数,而没有执行派生类D的析构函数。
原因是:当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态联编方式,
只调用了基类B的析构函数。

那么如何在撤销指针p所指的派生类的无名对象,既调用基类B的析构函数,也调用派生类D的
析构函数呢?

方法是:可以将基类的析构函数声明为虚析构函数,采用了多态性的动态联编方式。

虚析构函数没有类型,也没有参数,和普通虚函数相比,虚析构函数比较简单。
其声明格式: virtual ~类名()
*/

//例5.25 虚析构函数的使用

#include<iostream>
using namespace std;
class B{
  public:
    virtual ~B()
    {
     cout<<"调用基类B的析构函数\n";
    }
};
class D:public B{
  public:
    ~D()
    {
     cout<<"调用派生类D的析构函数\n";
    }
};
int main()
{
 B *p;          //定义指向基类B的指针变量p
 p = new D;     //用new运算符为派生类的无名对象动态的分配了一个存储空间,并将地址赋给对象指针p
 delete p;      //用delete撤销无名对象时,释放动态存储空间
 return 0;
}
/*
程序运行结果是:
调用派生类D的析构函数
调用基类B的析构函数

   说明:虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数
         定义为虚函数,则由该基类所派生的所有派生类的析构函数也都自动成为虚函数。
*/ 

3.虚函数与重载函数的关系
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般函数重载。
当普通的函数重载时,其函数的参数或参数类型有所不同,函数的返回类型也可以不同。但是当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类的虚函数原型完全相同。如果仅仅返回类型不同,其余均相同,系统会给出错误信息;若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。

//例5.26 虚函数与重载函数的关系

#include<iostream>
using namespace std;
class Base{
 public:
   virtual void func1();
   virtual void func2();
   virtual void func3();
   void func4();
};
class Derived:public Base{
 public:
   virtual void func1();     //func1是虚函数,这里可以不写virtual
   void func2(int i);        //与基类中的func2作为普通函数的重载,虚特性消失
   //char func3();           //错误,因为与基类的func3返回类型不同,应删去
   void func4();             //与基类中的func4是普通函数的重载,不是虚函数
};
void Base::func1()
{
 cout<<"--Base func1--\n";
}
void Base::func2()
{
 cout<<"--Base func2--\n";
}
void Base::func3()
{
 cout<<"--Base func3--\n";
}
void Base::func4()
{
 cout<<"--Base func4--\n";
}
void Derived::func1()
{
 cout<<"--Derived func1--\n";
}
void Derived::func2(int i)
{
 cout<<"--Derived func2--\n";
}
void Derived::func4()
{
 cout<<"--Derived func4--\n";
}
int main()
{
 Base b,*pt;    //定义基类的对象b和对象指针pt
 Derived d;     //定义派生类的对象d
 pt = &d;       //基类的指针pt象派生类的对象d
 pt->func1();   //调用的是派生类的fun1,结果是--Derived func1-- (虚函数的特性)
 pt->func2();   //调用的是基类的fun2,结果是--Base func2--(参数表中多了一个参数,变成普通重载函数,丢失虚函数的特性)
 pt->func4();   //调用的是基类的fun4,结果是--Base func4--(基类和派生类中均没有virtual关键字,普通成员函数的重载)
 return 0;
}
/*
程序运行结果是:
--Derived func1--
--Base func2--
--Base func4--
*/

4. 多重继承与虚函数

//例5.27 多重继承与虚函数的例子

#include<iostream>
using namespace std;
class Base1{
  public:
   virtual void fun()           //定义fun是虚函数
   {
    cout<<"--Base1--\n";
   }
};
class Base2{
  public:
   void fun()                  //定义fun是普通的成员函数
   {
    cout<<"--Base2--\n";
   }
};
class Derived:public Base1,public Base2{
  public:
   void fun()
   {
    cout<<"--Derived--\n";
   }
};
int main()
{
 Base1 *ptr1;
 Base2 *ptr2;
 Derived d;
 ptr1 = &d;  //基类的指针指向派生类的对象
 ptr1->fun();/*(*ptr1).fun();*/  //调用派生类Derived的fun方法,因为它是由Base1派生来的,为虚函数,有虚特性
 ptr2->fun();/*(*ptr2).fun();*/  //调用基类Base2的fun方法,派生类的fun方法是由Base2派生类来的,为普通成员重载函数 

 Derived obj;//基类的引用指向派生类的对象
 Base1 &p1 = obj;
 Base2 &p2 = obj;
 p1.fun();//调用派生类Derived的fun方法,因为它是由Base1派生来的,为虚函数,有虚特性
 p2.fun();//调用基类Base2的fun方法,派生类的fun方法是由Base2派生类来的,为普通成员重载函数,无虚特性 

 return 0;
}

5.虚函数的综合应用

//例5.28 应用C++的多态性,计算三角形、矩形和圆的面积。

#include<iostream>
#define PI 3.1416
using namespace std;
class Shape{                    //定义一个公共的基类
   public:
   // Shape(){}
    Shape(double a=0.0,double b=0.0)  //带默认的构造函数
    {
     x = a;
     y = b;
    }
    virtual void area()
    {
     cout<<"在基类中定义的虚基类";
     cout<<"为派生类提供一个公共的接口,";
     cout<<"以便派生类根据需要重新定义虚函数"<<endl;
    }
   protected:
    double x;
    double y;
};
class Triangle:public Shape{     //定义一个三角形的派生类
  public:
     Triangle(double a,double b):Shape(a,b){}
     void area()
     {
       cout<<"三角形的高是:"<<x<<","<<"底是:"<<y<<endl;
       cout<<"三角形面积:"<<0.5*x*y<<endl;
     }
};
class Square:public Shape{     //定义一个矩形的派生类
  public:
     Square(double a,double b):Shape(a,b){}
     void area()
     {
       cout<<"矩形的长是:"<<x<<","<<"宽是:"<<y<<endl;
       cout<<"矩形面积:"<<x*y<<endl;
     }
};
class Circle:public Shape{     //定义一个圆的派生类
  public:
     Circle(double a):Shape(a,a){}
     void area()
     {
       cout<<"圆的半径是:"<<x/2<<endl;
       cout<<"圆面积:"<<PI*x*x<<endl;
     }
};
int main()
{
 Shape *p,obj;                     //定义基类的对象指针p和对象obj
 p=&obj;                           //基类的对象指针p指向基类的对象obj
 p->area();                        //调用基类的area方法
 Triangle t(10.0,6.0);             //定义三角形的对象t
 Square s(10.0,6.0);               //定义矩形的对象s
 Circle c(10.0);                   //定义圆形的对象t
 p=&t;
 p->area();                        //计算三角形的面积
 p=&s;
 p->area();                        //计算矩形的面积
 p=&c;
 p->area();                        //计算圆形的面积
 return 0;
}
/*
 程序运行结果:
 在基类中定义的虚基类为派生类提供一个公共的接口,以便派生类根据需要重新定义虚函数
三角形的高是:10,底是:6
三角形面积:30
矩形的长是:10,宽是:6
矩形面积:60
圆的半径是:
圆面积:314.16
*/
时间: 2024-12-24 22:53:58

C++:虚函数的详解的相关文章

虚方法virtual详解

虚方法virtual详解 从C#的程序编译的角度来看,它和其它一般的函数有什么区别呢?一般函数在编译时就静态地编译到了执行文件中,其相对地址在程序运行期间是不发生变化的,也就是写死了的!而虚函数在编译期间是不被静态编译的,它的相对地址是不确定的,它会根据运行时期对象实例来动态判断要调用的函数,其中那个申明时定义的类叫申明类,那个执行时实例化的类叫实例类. 如:飞禽 bird = new 麻雀();那么飞禽就是申明类,麻雀是实例类. 具体的检查的流程如下 1.当调用一个对象的函数时,系统会直接去检

C++多态篇3——虚函数表详解之多继承、虚函数表的打印

在上上一篇C++多态篇1一静态联编,动态联编.虚函数与虚函数表vtable中,我最后简单了剖析了一下虚函数表以及vptr. 而在上一篇文章C++多态篇2--虚函数表详解之从内存布局看函数重载,函数覆盖,函数隐藏中我详细介绍了虚函数的函数重载,函数覆盖以及函数隐藏的问题,其实在那一篇文章中,对单继承的虚函数已经做了十分详细的解答了,如果对前面有兴趣的人可以先看一下那篇文章. 在这一篇中,我会具体的分析一下在不同继承中(单继承,多继承)关于虚函数表在内存中的布局以及如何打印虚函数表.但是有关在虚继承

PE文件结构与函数导出表——详解与实例

PE文件结构与函数导出表--详解与实例 随着windows系统从Xp升级到Win7.Win8, 从32位升级到64位,PE文件结构在整体未变的情况下发生了一些小的变动,一方面是推荐的程序装载地址未采用,另一方面,导出函数序号不再是简单的升序,而是一定程度上的进行了乱序.本文首先对PE文件结构进行了详尽的解说,接着介绍了如何得出函数导出表,整个过程采用SysWoW64目录下的wininet.dll实例进行说明.在介绍过程中,明确指出了Win7.Win8等新系统相对Xp带来的区别. 文章链接:htt

Python学习入门教程,字符串函数扩充详解

因有用户反映,在基础文章对字符串函数的讲解太过少,故写一篇文章详细讲解一下常用字符串函数.本文章是对:程序员带你十天快速入门Python,玩转电脑软件开发(三)中字符串函数的详解与扩充. 如果您想学习并参与本教程的完善与写作.请在下方讨论区,回复相关问题.一起完善本文章教程的书写. Python字符串常用函数. 声明字符串变量: str = ‘关注做全栈攻城狮,写代码也要读书,爱全栈,更爱生活.’ 下面所有字符串函数函数,是对变量str进行操作: 求字符串长度: 函数使用: 运行结果: 值得注意

自写函数VB6 STUFF函数 和 VB.net 2010 STUFF函数 详解

'*************************************************************************'**模 块 名:自写函数VB6 STUFF函数 和 VB.net 2010 STUFF函数 详解'**说    明:蓝凤凰设计商城 浴火凤凰-郭卫 | 蓝凤凰-魔灵 | 郭卫-icecept'**创 建 人:浴火凤凰-郭卫'**日    期:2015年10月10日  23:13:55'**修 改 人:浴火凤凰-郭卫'**日    期:'**描   

C++虚基类详解

1.虚基类的作用从上面的介绍可知:如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员.在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其惟一地标识一个成员,如    c1.A::display( ).在一个类中保留间接共同基类的多份同名成员,这种现象是人们不希望出现的.C++提供虚基类(virtual base class )的方法,使得在继承间接共同基类时只保留一份成员.现在,将类A声明为

ThinkPHP源码阅读2-----C函数配置文件详解

ThinkPHP的配置非常灵活,可自定义加载.大概看了一下,一共有这几个地方会加载配置文件,方便以后的读取 /** * 获取和设置配置参数 支持批量定义 * * @param string|array $name * 配置变量 * @param mixed $value * 配置值 * @return mixed */ function C($name = null, $value = null) { static $_config = array (); // 无参数时获取所有 if (emp

wp_list_categories函数用法详解

本以为写完新手教程之后,可以不写新手应用方面的文章了的,可今天又有朋友在群里问如何显示每个分类下文章数量这个基础性问题,看来Wordpress中文化还有很长的一段路要走,我们任重而道远啊!好,解决你的问题先:正如标题所说,Wordpress是用wp_list_categories这个函数来显示分类的,其用法是:< ?php wp_list_categories('arguments'); ?>arguments即参数,默认参数设置为: $defaults = array('show_optio

setInterval()函数用法详解

setInterval()函数用法详解:此函数用途相当广泛,在滚动代码或者焦点图片等等效果中都有应用,下面就通过实例简单介绍一下此函数的用法.setInterval()函数可以规定在按照指定的周期来执行一段函数,也就是说每隔一定事件就开始执行一次指定的函数.语法如下: setInterval(code,interval) 此函数具有两个参数,第一个参数规定要执行的函数,第二个参数规定函数两次执行之间的间隔,单位是毫秒(1秒=1000毫秒).代码实例如下: <!DOCTYPE HTML> <