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 ;
}
/*
在程序中,语句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 ;
}
*/
/*
运行结果是:
调用派生类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 ;
}
/*
程序运行结果:
调用基类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 ;
}
/*
程序运行结果是:
调用派生类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 ;
}
/*
程序运行结果是:
--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 ;
}

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/<<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 ;
}
/*
程序运行结果:
在基类中定义的虚基类为派生类提供一个公共的接口,以便派生类根据需要重新定义虚函数
三角形的高是:10,底是:6
三角形面积:30
矩形的长是:10,宽是:6
矩形面积:60
圆的半径是:
圆面积:314.16
*/

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

  1. C#虚函数virtual详解

    在面向对象编程中,有两种截然不同的继承方式:实现继承和接口继承.在实现继承时候,在Java中,所有函数默认都是virtual的,而在C#中所有函数并不默认为virtual的,但可以在基类中通过声明关键 ...

  2. C++ 派生类函数重载与虚函数继承详解

    目录 一.作用域与名字查找 1.作用域的嵌套 2.在编译时进行名字查找 3.名字冲突与继承 4.通过作用域运算符来使用隐藏的成员 二.同名函数隐藏与虚函数覆盖 1.几种必须区分的情况 2.一个更复杂的 ...

  3. C#虚方法virtual详解

    转: http://www.cnblogs.com/jason_yjau/archive/2009/08/25/1553949.html C#虚方法virtual详解 在C++.Java等众多OOP语 ...

  4. 4.C#虚方法virtual详解

    C#虚方法virtual详解 在C++.Java等众多OOP语言里都可以看到virtual的身影,而C#作为一个完全面向对象的语言当然也不例外. 虚拟函数从C#的程序编译的角度来看,它和其它一般的函数 ...

  5. 虚方法virtual详解

    虚方法virtual详解   从C#的程序编译的角度来看,它和其它一般的函数有什么区别呢?一般函数在编译时就静态地编译到了执行文件中,其相对地址在程序运行期间是不发生变化的,也就是写死了的!而虚函数在 ...

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

    '*************************************************************************'**模 块 名:自写函数VB6 STUFF函数 和 ...

  7. SQL Server数据库ROW_NUMBER()函数使用详解

    SQL Server数据库ROW_NUMBER()函数使用详解 摘自:http://database.51cto.com/art/201108/283399.htm SQL Server数据库ROW_ ...

  8. PHP函数篇详解十进制、二进制、八进制和十六进制转换函数说明

    PHP函数篇详解十进制.二进制.八进制和十六进制转换函数说明 作者: 字体:[增加 减小] 类型:转载   中文字符编码研究系列第一期,PHP函数篇详解十进制.二进制.八进制和十六进制互相转换函数说明 ...

  9. PHP date函数参数详解

    PHP date函数参数详解 作者: 字体:[增加 减小] 类型:转载       time()在PHP中是得到一个数字,这个数字表示从1970-01-01到现在共走了多少秒,很奇怪吧 不过这样方便计 ...

随机推荐

  1. python 字典内置方法get应用

    python字典内置方法get应用,如果我们需要获取字典值的话,我们有两种方法,一个是通过dict['key'],另外一个就是dict.get()方法. 今天给大家分享的就是字典的get()方法. 这 ...

  2. 自改xss小平台上线

    原先一直用xss.hk结果不知怎么被关的,正好手上有代码于是自己搭了一个,网上的类似的xss平台大多一样,原先的xss的chrome插件,不适合 "manifest_version" ...

  3. Ubuntu下编程环境GNU安装

    ubuntu下C编程   环境搭建 其实,linux下写C也是很容易的.IDE的话用 eclipse 集成 CDT 模块就行了.当然这属于重量级的了,就如同VC++之于windows一样.那有没有像T ...

  4. Python 学习教程

    <Core Python Programming>勘误参考表 http://starship.python.net/crew/wesc/cpp/errata2.htm 笨办法学 Pytho ...

  5. Mysql 实例分析连接

    表A记录如下: aID        aNum 1           a20050111 2           a20050112 3           a20050113 4          ...

  6. 通过注册表检测UAC是否处于关闭状态(不弹窗)

    注册表路径: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System UAC各级别对应的注册表值: 从 ...

  7. c++线程传参问题

    std::thread可以和任何可调用类型一起工作,可调用对象和函数带有参数时,可以简单地将参数传递给std::thread的构造函数 例如: #include<iostream> #in ...

  8. iTween基础之Punch(摇晃)

    一.基础介绍:二.基础属性 原文地址 : http://blog.csdn.net/dingkun520wy/article/details/50828042 一.基础介绍 PunchPosition ...

  9. DB天气app冲刺二阶段第十天

    昨天困到不行了 所以就写了那么几句..所以今天好好写写了要.. 今天的收获了一个很重要的问题 就还是api接口的事情,以前的那个接口虽然能用但是总是不稳定,今天由决定百度的一下然后就发现了一个很好用的 ...

  10. 关于json的知识整理

    一.什么是json JSON:JavaScript 对象表示法(JavaScript Object Notation). JSON 是存储和交换文本信息的语法.类似 XML,但JSON 比 XML 更 ...