虚函数 虚函数表

 

C# 是面向对象的程序设计语言,每一个函数都属于一个类。

Static:当一个方法被声明为Static时,这个方法是一个静态方法,编译器会在编译时保留这个方法的实现。也就是说,这个方法属于类,但是不属于任何成员,不管这个类的实例是否存在,它们都会存在。就像入口函数Static void Main,因为它是静态函数,所以可以直接被调用。

Virtua:当一个方法被声明为Virtual时,它是一个虚拟方法,直到你使用ClassName variable = new ClassName();声明一个类的实例之前,它都不存在于真实的内存空间中。这个关键字在类的继承中非常常用,用来提供类方法的多态性支持。

overrride:表示重写 这个类是继承于Shape类
public override double Area 这个属性再shape中肯定存在 但是这里我们不想用shape中的 所以要重写
virtual,abstract是告诉其它想继承于他的类 你可以重写我的这个方法或属性,否则不允许。

一个生动的例子 :老爸表示基类(被继承的类) 儿子表示子类(继承的类)

老爸用virtual告诉儿子:"孩子,你要继承我的事业,在这块上面可以自己继续发展你自己的"

儿子用override告诉全世界:"这个我可不是直接拿我爸的,他只是指个路给我,是我自己奋斗出来的"

abstract:抽象方法声明使用,是必须被派生类覆写的方法,抽象类就是用来被继承的;可以看成是没有实现体的虚方法;如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法;抽象类不能有实体的。
详解----------------------------------------------

interface用来声明接口

1.只提供一些方法规约,不提供方法主体. 如:

public interface IPerson

{

    void getName();//不包含方法主体

}

2.方法不能用public abstract等修饰,无字段变量,无构造函数。

3.方法可包含参数。 如

public interface IPerson

{

    void getAge(string s);

}

一个例子(例1):

public interface IPerson

{

   IPerson();              //错误

   string name;            //错误

   public void getIDcard();//错误

   void getName();         //right

   void getAge(string s); //right

}

实现interface的类

1.与继承类的格式一致,如 public class Chinese:IPerson{}

2.必须实现 interface 中的各个方法

   例2,继承例1

public class Chinese:IPerson

{

   public Chinese(){}                  //添加构造

   public void getName(){}          //实现getName()

   public void getAge(string s){} //实现getAge()

}

abstract声明抽象类、抽象方法

1.抽象方法所在类必须为抽象类

2.抽象类不能直接实例化,必须由其派生类实现。

3.抽象方法不包含方法主体,必须由派生类以override方式实现此方法,这点跟interface中的方法类似

public abstract class Book

{

public Book()

{  

}

public abstract void getPrice();      //抽象方法,不含主体

public virtual void getName()   //虚方法,可覆盖

{

      Console.WriteLine("this is a test:virtual getName()");

}

public virtual void getContent()   //虚方法,可覆盖

{

      Console.WriteLine("this is a test:virtual getContent()");

}

public void getDate()                           //一般方法,若在派生类中重写,须使用new关键字

{

      Console.WriteLine("this is a test: void getDate()");

   }

}

public class JavaBook:Book

{

      public override void getPrice()   //实现抽象方法,必须实现

      {

           Console.WriteLine("this is a test:JavaBook override abstract getPrice()");

      }

      public override void getName()   //覆盖原方法,不是必须的

      {

           Console.WriteLine("this is a test:JavaBook override virtual getName()");

      }

}

测试如下:

public class test

{

   public test()

   {

    JavaBook jbook=new JavaBook();

         jbook.getPrice();      //将调用JavaBook中getPrice()

         jbook.getName();       //将调用JavaBook中getName()

         jbook.getContent();    //将调用Book中getContent()

         jbook.getDate();       //将调用Book中getDate()

    }

   public static void Main()

   {

       test t=new test();

   }

}

virtual标记方法为虚方法

1.可在派生类中以override覆盖此方法

2.不覆盖也可由对象调用

3.无此标记的方法(也无其他标记),重写时需用new隐藏原方法

abstract 与virtual : 方法重写时都使用 override 关键字


接口定义以大写字母I开头。方法只定义其名称,在C#中,方法默认是公有方法;用public修饰方法是不允许的,否则会出现编译错误;接口可以从别的接口继承,如果是继承多个接口,则父接口列表用逗号间隔。
接口可以通过类来实现,当类的基列表同时包含基类和接口时,列表中首先出现的是基类;类必须要实现其抽象方法;
接口使用:见代码(转)

interface使用
interface使用(实例一)

using System;
namespace Dage.Interface
{
//打印机接口
public interface IPrint
{
string returnPrintName();
}
}
//--------------------------------------------
using System;
using Dage.Interface;
namespace Dage.Print
{
//HP牌打印机类
public class HP: IPrint
{
public string returnPrintName()
{
return "这是HP牌打印机";
}
}
}
//--------------------------------------------
using System;
namespace Dage.Print
{
//Eps牌打印机类
public class Eps: IPrint
{
public string returnPrintName()
{
return "这是Eps牌打印机";
}
}
}
//--------------------------------------------
using System;
using Dage.Interface;
namespace Dage
{
//打印类
public class Printer
{
public Printer()
{}
public string PrintName(IPrint iPrint)
{
return iPrint.returnPrintName();
}
}
}
//--------------------------------------------
--WinFrom中调用代码:
private void button1_Click(object sender, System.EventArgs e)
{
Printer p= new Printer();
switch (this.comboBox1.Text)
{
case "HP":
MessageBox.Show(p.PrintName(new HP()));
break;
case "Eps":
MessageBox.Show(p.PrintName(new Eps()));
break;
default:
MessageBox.Show("没有发现这个品牌!");
break;
}
}

 


虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
什么是虚函数?
在一个类里 一个类成员函数前加一个virtual关键字 ,并在派生类中重新定义的成员函数,可实现成员函数的动态重载.就是 一个虚函数,
但是虚函数也有 两种
1 普通虚函数
class Test
{
public:
virtual add(int,int); //普通的虚函数
}
2 纯虚函数
class Test
{
public:
virtual add(int,int)=0 //纯虚函数
}
普通的虚函数 :  函数体里面有函数实现部分,但到派生类里可以重载也可以不重载
纯虚函数: 函数体里面没有函数实现部分,必须在派生类里重载,所以直接等于0,函有纯虚函数的类,我们叫做抽像类,抽象类不可以实例化对象,如果实例化编绎器会报错的
虚函数的意义?
可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,   基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数 , 而不是基类中定义的成员函数(只要派生类改写了该成员函数)。  
若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都  
会调用基类中定义的那个函数。
什么是虚函数表?
C++  了解的人都应该知道虚函数( Virtual Function )是通过一张虚函数表( Virtual Table )来实现的。简称为 V-Table 。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。 C++ 的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能 —— 如果有多层继承或是多重继承的情况下)。   这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。   没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base {
public :
      virtual   void  f() { cout <<  "Base::f" << endl; }
      virtual   void  g() { cout <<  "Base::g" << endl; }
      virtual   void  h() { cout <<  "Base::h" << endl; }
};
按照上面的说法,我们可以通过 Base 的实例来得到虚函数表。   下面是实际例程:
       typedef   void (*Fun)( void );
       Base b;
       Fun pFun = NULL;
       cout <<  " 虚函数表地址: "  << ( int *)(&b) << endl;
       cout <<  " 虚函数表  —  第一个函数地址: "  << ( int *)*( int *)(&b) << endl;
        // Invoke the first virtual function   
       pFun = (Fun)*(( int *)*( int *)(&b));
       pFun();
实际运行经果如下: (WindowsXP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址: 0012FED4
虚函数表  —  第一个函数地址: 0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把 &b 转成 int * ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是 Base::f() ,这在上面的程序中得到了验证(把 int*  强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用 Base::g() Base::h() ,其代码如下:
     (Fun)*(( int *)*( int *)(&b)+0);  // Base::f()
       (Fun)*(( int *)*( int *)(&b)+1);  // Base::g()
       (Fun)*(( int *)*( int *)(&b)+2);  // Base::h()
这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:
                              
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“ /0 ”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在 WinXP+VS2003 下,这个值是 NULL 。而在 Ubuntu 7.10 +Linux 2.6.22 + GCC 4.1.3 下,这个值是如果 1 ,表示还有下一个虚函数表,如果值是 0 ,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例: Derive d;  的虚函数表如下:
我们可以看到下面几点:
1 虚函数按照其声明顺序放于表中。
2 父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数: f() 。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1 覆盖的 f() 函数被放到了虚表中原来父类虚函数的位置。
2 没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
       Base *b =  new  Derive();
       b->f();
b 所指的内存中的虚函数表的 f() 的位置已经被 Derive::f() 函数地址所取代,于是在实际调用发生时,是 Derive::f() 被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1   每个父类都有自己的虚表。
2   子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的 f() 函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的 f() 的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的 f() 了。如:
       Derive d;
       Base1 *b1 = &d;
       Base2 *b2 = &d;
       Base3 *b3 = &d;
       b1->f();  //Derive::f()
       b2->f();  //Derive::f()
       b3->f();  //Derive::f()
       b1->g();  //Base1::g()
       b2->g();  //Base2::g()
       b3->g();  //Base3::g()
安全性
每次写 C++ 的文章,总免不了要批判一下 C++ 。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到 Base1 的虚表中有 Derive 的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
       Base1 *b1 =  new  Derive();
       b1->f1();  // 编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反 C++ 语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是 private 或是 protected 的,但这些非 public 的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些 non-public 的虚函数,这是很容易做到的。
如:
class  Base {
private :
      virtual   void  f() { cout <<  "Base::f"  << endl; }
};
class  Derive :  public  Base{
};
typedef   void (*Fun)( void );
void  main() {
Derive d;
Fun pFun = (Fun)*(( int *)*( int *)(&d)+ 0 );
pFun();
}
为什么派生类的析构函数是虚函数 ?
Base *pb;
inherit c;//inherit是继承 Base的
pb=&c;
delete   pb;时需要调用对象的析构函数,如果基类析构不是virtual型,会根据pb的定义类型调用相应类的析构函数,即调用即类析构,但如果你在派生类析构里有内存释放操作,那就会发生内存泄漏。假如基类析构是virtual型,会根据pb所指对象的类型调用相应类的析构函数,即派生类析构,派生类析构再根据析构函数调用的层次原则调用即类析构。这样就保证不会有问题。


        
  1. <span style="color:#ff6666;">EG:
    class A{
    public:
          virtual ~A(){}
    };
    
    class B:public A{
    public:
         ~B(){}
    };
    
    A *p = new B();
    ......
    delete p;(这时就会先调用B的析构函数,然后再调用A的析构函数)</span>


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值