前文回顾:
《用CIL写程序:你好,沃尔德》
《用CIL写程序:写个函数做加法》
今天是乙未羊年的第一天,小匹夫先在这里给各位看官拜个年了。不知道各位看官是否和匹夫一样,摸键盘的手都已经有点生疏了呢?所以,为了不忘却程序猿的使命,不冷落程序猿最好的伙伴--键盘。匹夫决定来写《用CIL写程序》的最新一篇文章。可是写什么主题呢?之前匹夫也介绍过CIL其实也是面向对象的,所以寻思着大过年的,不如就写一个类,一个用来抽象化小匹夫的类吧,既可以介绍下小匹夫,小匹夫也可以借这个类给各位拜年。那么顺序由上到下,无外乎如何声明一个类,类成员如何定义,以至于到后来如何实例化一个类,并且调用实例的各个方法,当然本文的完整CIL代码各位可以在附录部分看到。
OK,那么声明一个类的第一步是什么呢?定义一个类的名字?写构造函数?NO,NO。
定义一个类的第一步,自然是要告诉别人你定义的是一个类!不是方法,不是变量,而是一个类。那么我们仿照前两篇文章中介绍的声明方法的方式:.method,来声明我们的类。那么各位是不是已经想到了呢?对,我们直接使用“.class”指令来声明一个类。
那么第二步呢?对,既然知道它是一个类了,那么我们显然还可以指定关键字比如abstract、sealed、public、private或者interface等等,而在CIL中很多都是可以省略的,所以这里我们也就省略了。之后我们还需要给它起个名字来标识它,既然题目已经叫做《这个叫慕容小匹夫的类》了,那么我们的类的名字就叫做MurongXiaoPiFu。
之后第三部呢?对啊,貌似应该有个构造函数啊。但有时候我们写C#代码的时候,并不需要自己手动写构造函数呀。可为什么到了CIL里就需要自己手动写呢?因为C#的编译器会自动为我们生成构造函数,而CIL的编译器则不会。所以构造函数这个环节我们一定不要忘记。
所以类的声明部分我们就写出来了:
//MurongXiaoPiFu类的声明 .class MurongXiaoPiFu { .method public void .ctor() { //TODO } }
OK,需要的三个步骤走完了。这里总结一下:
好了,要写构造函数这一点我们已经明确了。我们声明完.ctor()之后,该如何实现它呢?那么我们首先考虑一下一个没有参数的构造函数都可能要做一些什么。
匹夫在上一篇文章中详细的介绍过CIL是如何使用堆栈的,包括值类型是如何使用堆栈以及引用类型是如何使用堆栈。此处的MurongXiaoPiFu类其实就是一个引用类型,所以呢?不错,它需要把自己的引用压栈。
那么还有呢?所有的引用类型都派生自System.Object,所以生成一个新的类实例其实是通过System.Object的构造函数来实现的。那么基于以上2点,我们的构造函数的实现也就十分清楚了:
//构造函数.ctor的实现 .method public void .ctor() { .maxstack 1 ldarg.0 //1.将实例的引用压栈 call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数 ret }
但是小匹夫觉得写到这里还有点不完整,还缺点啥呢?对啊,面向对象的三大法宝:封装、继承、多态嘛。其中继承和多态都和所谓的继承有很大的关系。所以说到类,不说继承似乎有点不专业。为了不给各位留下匹夫不专业的印象,继承还是不得不说。但是看匹夫你定义的这个类MurongXiaoPiFu貌似没有用上继承啊?
此言差矣。殊不知,MurongXiaoPiFu这个类默认是继承自System.Object这个基类的。只不过当我们省略掉继承的语句时,CIL的编译器会自动将我们的类识别为从System.Object派生而来的,换言之,会被当做是引用类型。那么如果我们不省略继承语句,我们的类该如何显式地实现继承呢?没错,用extend关键字。那么加上继承自System.Object的语句(虽然这是多余的,但是为了演示继承的用法这里还是很low的这样写了)之后,我们完整的声明代码如下:
//MurongXiaoPiFu类的声明 .class MurongXiaoPiFu extends [mscorlib]System.Object //同样System.Object来自mscorlib程序集 { //构造函数.ctor的实现 .method public void .ctor() { .maxstack 1 ldarg.0 //1.将实例的引用压栈 call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数 ret } }
好了,行文至此,各位对CIL中如何声明一个类应该就有了一个直观的印象了吧。不过这还不够啊,没有类中的各个成员,这个类也就没有什么实际的用途呀。所以接下来匹夫继续完善这个叫慕容小匹夫的类,来介绍一下小匹夫自己,同时也为各位拜个年。
说到类的成员无非就是类的成员变量和类的成员函数。
那么具体到我们的类,它的成员变量无非就是介绍一个人所需要的信息。无非就是姓名(name),年龄(age),性别(sex),职业(job)这些咯。匹夫相信在C#中,各位都知道如何声明一个成员变量。但是在CIL的世界中呢?应该如何声明一个变量呢?
和声明方法的.method指令类似,这里匹夫再引入一个指令.field用来声明变量。同样.field之后也可以加一些修饰符。
但是写代码对这些变量赋值之前,匹夫还是要强调一下CIL的执行都是依托于堆栈的,也就是要把堆栈记心中。这里我们需要用到stfld指令,这个指令是什么意思呢?简短截说就是把栈中的数据弹出,并赋值给这个类实例中对应的字段。那么之前,显然我们要将那个值先入栈,这样才能出栈赋值给实例中的字段嘛。
所以咯,匹夫的基本信息就是这样的了:
//声明各个变量
.field public string name
.field public int32 age
.field public string sex
.field public string job
//为各个成员变量赋值 ldstr "陈嘉栋" stfld string MurongXiaoPiFu::name ldc.i4.s 0x19 //25岁 stfld int32 MurongXiaoPiFu::age ldstr "男人" stfld string MurongXiaoPiFu::sex ldstr "程序猿" stfld string MurongXiaoPiFu::job
匹夫的基本资料有了,但是匹夫还想向大伙拜年和介绍自己呢啊。所以光有成员变量还是不够的,我们还需要几个成员函数。说到一个类的函数,首先要关注点什么呢?对啊,一个类中的函数究竟是静态函数呢还是实例函数呢?所以接下来我们要去实现的函数既要包括静态函数,还要有实例函数。当然,如果要全面一些,我们还要考虑进去虚函数。比如这三个函数:
同时,这两个实例函数还会用到刚才定义的几个成员变量,所以这里我们会引入另一个新的指令ldfld。这个指令是啥意思呢?别忘了堆栈额~所有被操作的数据都需要通过堆栈,所以各位明白了吧?这个指令就是将字段中值压入堆栈中供之后使用的。
首先,我们使用static关键字实现静态函数SayHi:
//问好,静态函数 .method static void SayHi() {
.maxstack 1 ldstr "你好!" call void [mscorlib]System.Console::WriteLine(string)
ret }
然后,我们实现那两个实例函数,与静态函数使用static相反,实例函数使用instance关键字。不过在CIL中类的成员函数默认是实例函数,因此我们可以省略instance关键字。同时,由于是实例函数,因此在获取实例的各个变量之前肯定要先知道该实例的引用。所以每一步取值,我们首先要”ldarg.0“将实例的引用压栈,之后再使用”ldfld“去取值压栈。
//实例函数介绍自己 .method void Introduce() { .maxstack 7 ldstr "我叫{0},今年{1}岁,是一个{2}" ldarg.0 ldfld string MurongXiaoPiFu::name ldarg.0 ldfld int32 MurongXiaoPiFu::age box int32 //对int型的年龄有一个装箱 ldarg.0 ldfld string MurongXiaoPiFu::job call void [mscorlib]System.Console::WriteLine(string, object, object, object) ret } //实例函数,虚函数,过年好 .method public virtual void HappyNewYear() { .maxstack 1 ldstr "过年好" call void [mscorlib]System.Console::WriteLine(string) ret }
OK,这样我们的成员函数就定义完毕了。
好啦,既然我们的慕容小匹夫的类已经定义好了。那么就让我们来实例化一个实例出来给各位拜个年吧。
首先,我们还是使用第一篇文章中写的第一个函数Fanyou来作为我们程序的入口,控制整个流程。那么我们需要在Fanyou函数中声明一个局部变量来存放MurongXiaoPiFu这个类的实例引用。这个变量我们就叫做Murong吧。
.locals init ( class MurongXiaoPiFu Murong)
然后,我们需要实例化一个MurongXiaoPiFu类。所以此处匹夫要引入newobj指令了,newobj指令通过调用类的构造函数来创造一个新的实例。创造好实例之后,这个实例的引用还躺在栈中,所以为了给刚刚才声明的变量Murong赋值,我们需要将栈中的实例引用弹出赋值给Murong,因此需要用到stloc。
newobj instance void class MurongXiaoPiFu::'.ctor'() stloc Murong
好了,到此我们已经将慕容小匹夫这个类实例化了。那么接下来呢?不错,该调用相应的方法给各位拜年啦!
首先我们要调用我们定义的静态函数SayHi。和上面定义成员函数不同而且也很有趣的一点,就是我们使用call指令默认调用的是静态函数。所以我们调用SayHi就变得十分简单了。
//调用静态函数 call void MurongXiaoPiFu::SayHi()
而如果要调用实例函数,则要先将实例的引用压栈,而且在call的后面还需要加上instance。所以我们调用Introduce和HappyNewYear需要分别指定它们的实例,也就是将实例的引用使用ldloc压栈,之后call的时候还需要指明调用的是实例方法。(当然,如果各位有过使用工具将程序集反编译成CIL代码的经历的话。在调用某个实例的方法的部分,各位十有八九看到的可能是callvirt而非call,这个话题本来小匹夫这篇文章也想讨论来着,不过实在有点太晚了。所以留个坑,日后再专门讨论)
//调用实例方法 ldloc Murong call instance void class MurongXiaoPiFu::Introduce() ldloc Murong callvirt instance void class MurongXiaoPiFu::HappyNewYear()
那么编译,再执行的结果如图。
好,其实写到此处也算是一个good ending了。年也给大家拜了,CIL也和大家聊了。 那么~~~
如果各位看官觉得文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~
.assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. } .assembly 'HelloWorld' { } .method static void Fanyou() { .entrypoint .maxstack 4 .locals init ( class MurongXiaoPiFu Murong) newobj instance void class MurongXiaoPiFu::'.ctor'() stloc Murong call void MurongXiaoPiFu::SayHi() ldloc Murong call instance void class MurongXiaoPiFu::Introduce() ldloc Murong callvirt instance void class MurongXiaoPiFu::HappyNewYear() // ldstr "Hello World!" // call void [mscorlib]System.Console::WriteLine(string) ret } .method static void AddLife() { .maxstack 4 //局部变量 .locals init (int32 num1, int32 num2, int32 result) //第一个功能:显示提示输入加数,并获取输入的值 //在屏幕上显示“请输入第一个加数” ldstr "请输入第一个加数" call void [mscorlib]System.Console::WriteLine(string) //获取用户的输入值 call string [mscorlib]System.Console::ReadLine() //将输入的字符串转化成int call int32 [mscorlib]System.Int32::Parse(string) //值出栈,赋给局部变量num1 stloc num1 //num2 ldstr "请输入第二个加数" call void [mscorlib]System.Console::WriteLine(string) call string [mscorlib]System.Console::ReadLine() call int32 [mscorlib]System.Int32::Parse(string) stloc num2 //第二个功能:相爱相杀,不对,应该是相爱相加... //将值从变量中压入堆栈 ldloc num1 ldloc num2 //求和 add //将结果赋值给result stloc result //最后一个功能,关键的其实是装箱 //显示的格式 ldstr "{0} + {1} = {2}" //将num1,num2,result装箱,供之后的writeLine使用。 ldloc num1 box int32 ldloc num2 box int32 ldloc result box int32 //将算式显示出来 call void [mscorlib]System.Console::WriteLine(string, object, object, object) ret } //MurongXiaoPiFu类的声明 .class MurongXiaoPiFu extends [mscorlib]System.Object //同样System.Object来自mscorlib程序集 { .field public string name .field public int32 age .field public string sex .field public string job //构造函数.ctor的实现 .method public void .ctor() { .maxstack 10 //定义各个成员变量 ldarg.0 ldstr "陈嘉栋" stfld string MurongXiaoPiFu::name ldarg.0 ldc.i4.s 0x19 //25岁 stfld int32 MurongXiaoPiFu::age ldarg.0 ldstr "男" stfld string MurongXiaoPiFu::sex ldarg.0 ldstr "程序猿" stfld string MurongXiaoPiFu::job ldarg.0 //1.将实例的引用压栈 call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数 ret } //问好,静态函数 .method static void SayHi() { .maxstack 1 ldstr "你好!" call void [mscorlib]System.Console::WriteLine(string) ret } //实例函数介绍自己 .method void Introduce() { .maxstack 7 ldstr "我叫{0},今年{1}岁,是一个{2}" ldarg.0 ldfld string MurongXiaoPiFu::name ldarg.0 ldfld int32 MurongXiaoPiFu::age box int32 //对int型的年龄有一个装箱 ldarg.0 ldfld string MurongXiaoPiFu::job call void [mscorlib]System.Console::WriteLine(string, object, object, object) ret } //实例函数,虚函数,过年好 .method public virtual void HappyNewYear() { .maxstack 1 ldstr "过年好" call void [mscorlib]System.Console::WriteLine(string) ret } }