C++ Virtual详解

Virtual是C++ OO机制中很重要的一个关键字。只要是学过C++的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数(例如下面例子中的函数print),于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的指针point指向派生类Derived的对象时,对point的print函数的调用实际上是调用了Derived的print函数而不是Base的print函数。这是面向对象中的多态性的体现。(关于虚拟机制是如何实现的,参见Inside the
C++ Object Model ,Addison Wesley 1996)

[cpp] view
plain
copy

  1. class Base  
  2. {  
  3. public:Base(){}  
  4. public:  
  5.        virtual void print(){cout<<"Base";}  
  6. };  
  7.    
  8. class Derived:public Base  
  9. {  
  10. public:Derived(){}  
  11. public:  
  12.        void print(){cout<<"Derived";}  
  13. };  
  14.    
  15. int main()  
  16. {  
  17.        Base *point=new Derived();  
  18.        point->print();  
  19. }   

//---------------------------------------------------------

Output:

Derived

//---------------------------------------------------------

这也许会使人联想到函数的重载,但稍加对比就会发现两者是完全不同的:

(1)      重载的几个函数必须在同一个类中;

覆盖的函数必须在有继承关系的不同的类中

(2)      覆盖的几个函数必须函数名、参数、返回值都相同;

重载的函数必须函数名相同,参数不同。参数不同的目的就是为了在函数调用的时候编译器能够通过参数来判断程序是在调用的哪个函数。这也就很自然地解释了为什么函数不能通过返回值不同来重载,因为程序在调用函数时很有可能不关心返回值,编译器就无法从代码中看出程序在调用的是哪个函数了。

(3)      覆盖的函数前必须加关键字Virtual;

重载和Virtual没有任何瓜葛,加不加都不影响重载的运作。

 

关于C++的隐藏规则:

我曾经听说过C++的隐藏规则:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual

关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual

关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

                                               ----------引用自《高质量C++/C 编程指南》林锐  2001

这里,林锐博士好像犯了个错误。C++并没有隐藏规则,林锐博士所总结的隐藏规则是他错误地理解C++多态性所致。下面请看林锐博士给出的隐藏规则的例证:

[cpp] view
plain
copy

  1. #include <iostream.h>  
  2. class Base  
  3. {  
  4. public:  
  5. virtual void f(float x){ cout << "Base::f(float) " << x << endl; }  
  6. void g(float x){ cout << "Base::g(float) " << x << endl; }  
  7. void h(float x){ cout << "Base::h(float) " << x << endl; }  
  8. };  
  9.    
  10. class Derived : public Base  
  11. {  
  12. public:  
  13. virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }  
  14. void g(int x){ cout << "Derived::g(int) " << x << endl; }  
  15. void h(float x){ cout << "Derived::h(float) " << x << endl; }  
  16. };  
  17.    
  18. void main(void)  
  19. {  
  20. Derived d;  
  21. Base *pb = &d;  
  22. Derived *pd = &d;  
  23. // Good : behavior depends solely on type of the object  
  24. pb->f(3.14f); // Derived::f(float) 3.14  
  25. pd->f(3.14f); // Derived::f(float) 3.14  
  26. // Bad : behavior depends on type of the pointer  
  27. pb->g(3.14f); // Base::g(float) 3.14  
  28. pd->g(3.14f); // Derived::g(int) 3 (surprise!)  
  29. // Bad : behavior depends on type of the pointer  
  30. pb->h(3.14f); // Base::h(float) 3.14 (surprise!)  
  31. pd->h(3.14f); // Derived::h(float) 3.14  
  32. }   

林锐博士认为bp 和dp 指向同一地址,按理说运行结果应该是相同的,而事实上运行结果不同,所以他把原因归结为C++的隐藏规则,其实这一观点是错的。决定bp和dp调用函数运行结果的不是他们指向的地址,而是他们的指针类型。“只有在通过基类指针或引用间接指向派生类子类型时多态性才会起作用”(C++ Primer 3rdEdition)。pb是基类指针,pd是派生类指针,pd的所有函数调用都只是调用自己的函数,和多态性无关,所以pd的所有函数调用的结果都输出Derived::是完全正常的;pb的函数调用如果有virtual则根据多态性调用派生类的,如果没有virtual则是正常的静态函数调用,还是调用基类的,所以有virtual的f函数调用输出Derived::,其它两个没有virtual则还是输出Base::很正常啊,nothing
surprise!

所以并没有所谓的隐藏规则,虽然《高质量C++/C 编程指南》是本很不错的书,可大家不要迷信哦。记住“只有在通过基类指针或引用间接指向派生类子类型时多态性才会起作用”。

 

纯虚函数:

C++语言为我们提供了一种语法结构,通过它可以指明,一个虚拟函数只是提供了一个可被子类型改写的接口。但是,它本身并不能通过虚拟机制被调用。这就是纯虚拟函数(pure

virtual function)。 纯虚拟函数的声明如下所示:

[cpp] view
plain
copy

  1. class Query {  
  2. public:  
  3. // 声明纯虚拟函数  
  4. virtual ostream& print( ostream&=cout ) const = 0;  
  5. // ...  
  6. };  

这里函数声明后面紧跟赋值0。

包含(或继承)一个或多个纯虚拟函数的类被编译器识别为抽象基类。试图创建一个抽象基类的独立类对象会导致编译时刻错误。(类似地通过虚拟机制调用纯虚函数也是错误的)

[cpp] view
plain
copy

  1. // Query 声明了纯虚拟函数, 我们不能创建独立的 Query 类对象  
  2. // 正确: NameQuery 是 Query 的派生类  
  3. Query *pq = new NameQuery( "Nostromo" );  
  4. // 错误: new 表达式分配 Query 对象  
  5. Query *pq2 = new Query();  

虚析构:

如果一个类用作基类,我们通常需要virtual来修饰它的析构函数,这点很重要。如果基类的析构函数不是虚析构,当我们用delete来释放基类指针(它其实指向的是派生类的对象实例)占用的内存的时候,只有基类的析构函数被调用,而派生类的析构函数不会被调用,这就可能引起内存泄露。如果基类的析构函数是虚析构,那么在delete基类指针时,继承树上的析构函数会被自低向上依次调用,即最底层派生类的析构函数会被首先调用,然后一层一层向上直到该指针声明的类型。

虚继承:

如果只知道virtual加在函数前,那对virtual只了解了一半,virtual还有一个重要用法是virtual public,就是虚拟继承。虚拟继承在C++ Primer中有详细的描述,下面稍作修改的阐释一下:

在缺省情况下C++中的继承是“按值组合”的一种特殊情况。当我们写

class Bear : public ZooAnimal { ... };

每个Bear 类对象都含有其ZooAnimal 基类子对象的所有非静态数据成员以及在Bear中声明的非静态数据成员。类似地当派生类自己也作为一个基类对象时如:

class PolarBear : public Bear { ... };

则PolarBear 类对象含有在PolarBear 中声明的所有非静态数据成员以及其Bear 子对象的所有非静态数据成员和ZooAnimal 子对象的所有非静态数据成员。在单继承下这种由继承支持的特殊形式的按值组合提供了最有效的最紧凑的对象表示。在多继承下当一个基类在派生层次中出现多次时就会有问题最主要的实际例子是iostream 类层次结构。ostream 和istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream 和istream 派生

class iostream :public istream, public ostream { ... };

缺省情况下,每个iostream 类对象含有两个ios 子对象:在istream 子对象中的实例以及在ostream 子对象中的实例。这为什么不好?从效率上而言,iostream只需要一个实例,但我们存储了ios 子对象的两个复本,浪费了存储区。此外,在这一过程中,ios的构造函数被调用了两次(每个子对象一次)。更严重的问题是由于两个实例引起的二义性。例如,任何未限定修饰地访问ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果ostream 和istream 对其ios 子对象的初始化稍稍不同,会怎样呢?怎样通过iostream
类保证这一对ios 值的一致性?在缺省的按值组合机制下,真的没有好办法可以保证这一点。

C++语言的解决方案是,提供另一种可替代按“引用组合”的继承机制--虚拟继承(virtual inheritance)。在虚拟继承下只有一个共享的基类子对象被继承而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚拟基类。

       通过用关键字virtual 修正,一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得ZooAnimal 成为Bear 和Raccoon 的虚拟基类:

// 这里关键字 public 和 virtual的顺序不重要

class Bear : public virtual ZooAnimal { ... };

class Raccoon : virtual public ZooAnimal { ... };

虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。

时间: 2024-09-21 19:04:35

C++ Virtual详解的相关文章

C++:虚继承(virtual inheritance) 详解

在多重继承(multiple inheritance)中, 可能出现多个基类继承自同一个基类, 即"菱形继承", 会导致最顶层的基类, 被复制多次; 可以采用虚继承(virtual inheritance)的方式, 使派生类(derived class)只保留最顶层基类的一个副本. virtual, 即需要vptr(virtual table pointer), 即虚表指针, 额外占用4个字节; 代码如下: /* * test.cpp * * Created on: 2014.04.1

Tomcat与Java Web开发技术详解连载之一

web|详解 本章介绍如何在Tomcat上创建和发布Web应用.这里首先讲解Tomcat的目录结构以及Web应用的目录结构,接着介绍如何将HTML.Servlet.JSP和Tag Library部署到Web应用中,然后介绍把整个Web应用打包并发布的方法,最后介绍如何在Tomcat上配置虚拟主机. 本章侧重于讨论Web应用的结构和发布方法,所以没有对本章的Servlet和JSP的例子进行详细解释,关于Servlet和JSP的技术可以分别参考其它章节的内容. 2.1 Tomcat的目录结构 在To

shtml网页SSI使用详解

网页|详解 1.Config命令 Config命令主要用于修改SSI的默认设置.其中: Errmsg:设置默认错误信息.为了能够正常的返回用户设定的错误信息,在HTML文件中Errmsg参数必须被放置在其它SSI命令的前面,否则客户端只能显示默认的错误信息,而不是由用户设定的自定义信息. <!--#config errmsg="Error! Please email webmaster@mydomain.com -->  Timefmt:定义日期和时间的使用格式.Timefmt参数必

HTML标签详解

详解 --  HTML标签详解HTML指令详解结构<html><head><title>标题<title></head><body>..........文件内容..........</body></html>1.文件标题<title>..........</title>2.文件更新--<meta> [1]10秒后自动更新一次 <meta http-equiv="

Tomcat 配置技巧精华详解分析

技巧|精华|详解 1.配置系统管理(Admin Web Application) 大多数商业化的J2EE服务器都提供一个功能强大的管理界面,且大都采用易于理解的Web应用界面.Tomcat按照自己的方式,同样提供一个成熟的管理工具,并且丝毫不逊于那些商业化的竞争对手.Tomcat的Admin Web Application最初在4.1版本时出现,当时的功能包括管理context.data source.user和group等.当然也可以管理像初始化参数,user.group.role的多种数据库

SSI使用详解(二)

详解 下面我们将逐一进行介绍. 1.Config命令 Config命令主要用于修改SSI的默认设置.其中: Errmsg:设置默认错误信息.为了能够正常的返回用户设定的错误信息,在HTML文件中Errmsg参数必须被放置在其它SSI命令的前面,否则客户端只能显示默认的错误信息,而不是由用户设定的自定义信息. <!--#config errmsg="Error! Please email webmaster@mydomain.com --> Timefmt:定义日期和时间的使用格式.T

SSI使用详解(转一)

详解 你是否曾经或正在为如何能够在最短的时间内完成对一个包含上千个页面的网站的修改而苦恼?那么可以看一下本文的介绍,或许能够对你有所帮助. 什么是SSI? SSI是英文Server Side Includes的缩写,翻译成中文就是服务器端包含的意思.从技术角度上说,SSI就是HTML文件中,可以通过注释行调用的命令或指针.SSI 具有强大的功能,只要使用一条简单的SSI 命令就可以实现整个网站的内容更新,动态显示时间和日期,以及执行shell和CGI脚本程序等复杂的功能.SSI 可以称得上是那些

SSI使用详解(转二)

详解 2.Include命令 Include命令可以把其它文档中的文字或图片插入到当前被解析的文档中,是整个SSI的关键所在.通过Include命令只需要改动一个文件就可以瞬间更新整个站点! Include命令具有两个不同的参数,如果使用错误的参数更新站点的话,不仅达不到初衷,反而会得到一大堆的错误信息. Virtual:给出到服务器端某个文档的虚拟路径.例如: <!--#include virtual="/includes/header.html" --> 为了更加合理的

ASP包含文件方法详解

详解 ASP包含文件方法详解 SSI 指令为用户提供在 Web 服务器处理之前将一个文件的内容插入到另一个文件的方法.ASP 只使用这一机制的 #include 指令.要在 .asp 文件中插入一个文件,使用下面的语法: <!--#include virtual | file ="filename"--> virtual 和 file 关键字指示用来包含该文件的路径的类型,filename 是您想包含的文件的路径和名称. 被包含文件不要求专门的文件扩展名:但是,为被包含文件