最近看书的时候看到了虚方法调用这一块,所以温习一下这块的知识,和大家分享一下。
调用虚方法时,具体调用的哪个方法不是在编译时定的,而是在运行时根据对象的真实类型而定的,因此,CLR对于虚方法调用采用了动态分派的方法
举两个例子,定义两个继承关系的类Parent和Child
1 class Parent
2 {
3 public virtual void virtualMtd()
4 {
5 Console.WriteLine("Parent");
6 }
7 }
8 class Child : Parent
9 {
10 public override void virtualMtd()
11 {
12 Console.WriteLine("Child");
13 }
14 }
Parent 类中定义了一个虚方法virtualMtd(),Child类中重写了此方法。
Main函数的代码如下
1 static void Main(string[] args)
2 {
3
4 Parent p = new Parent();
5 p.virtualMtd();
6 Child c = new Child();
7 c.virtualMtd();
8 p = c;
9 p.virtualMtd();
10 }
在main方法中一共有3处调用了virtualMtd()。编译一下。
在使用ildasm查看程序入口Mian方法的IL代码如下:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 37 (0x25)
.maxstack 1
.locals init ([0] class ConsoleApplication2.Parent p,
[1] class ConsoleApplication2.Child c)
IL_0000: nop
IL_0001: newobj instance void ConsoleApplication2.Parent::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void ConsoleApplication2.Parent::virtualMtd()
IL_000d: nop
IL_000e: newobj instance void ConsoleApplication2.Child::.ctor()
IL_0013: stloc.1
IL_0014: ldloc.1
IL_0015: callvirt instance void ConsoleApplication2.Parent::virtualMtd()
IL_001a: nop
IL_001b: ldloc.1
IL_001c: stloc.0
IL_001d: ldloc.0
IL_001e: callvirt instance void ConsoleApplication2.Parent::virtualMtd()
IL_0023: nop
IL_0024: ret
} // end of method Program::Main
请注意加颜色的三行分别对应下边的2,4,6
1 Parent p = new Parent();
2 p.virtualMtd();
3 Child c = new Child();
4 c.virtualMtd();
5 p = c;
6 p.virtualMtd();
可以看到在C#的源代码中的三句话不一样,但生成的IL语句都是一样的。
通过向堆栈中压入不同对象引用,3条一样callvirt指令将调用所引用对象的同名方法,这正是虚方法的调用实质。
其实在Parent类类型方法表里新添加一个行:虚方法virtualMtd()在子类类型方法表中也有一个virtualMtd(),并且他们在各自的类型方法表里的保存的位置也是一样的。
IL_0015: callvirt instance void ConsoleApplication2.Parent::virtualMtd()
CLR在执行IL指令时,会根据Parent类型表中virtualMtd方法的位置得到一个索引号,然后,根据执行此方法的对象的真实类型查对应的类型表(当前对象为Child类型,则查Child类型表),按前面得到的索引到方法表中找到应该调用的方法。
总结一句话:按同样的索引查不同的表。