上节我们说过C#所开发的程序源代码并不是编译成能够直接在操作系统上执行的二进制代码。与Java类似,它被编译成为中间代码,然后通过.NET Framework的虚拟机——被称之为通用语言运行时(CLR)执行。所有的.Net编程语言都被编译成这种被称为MSIL(Microsoft Intermediate Language )的中间代码。因此虽然最终的程序在表面上仍然与传统意义上的可执行文件都具有“.exe”的后缀名。但是实际上,如果计算机上没有安装.Net Framework,那么这些程序将不能够被执行。在程序执行时,.Net Framework将中间代码翻译成为二进制机器码,从而使它得到正确的运行。最终的二进制代码被存储在一个缓冲区中。所以一旦程序使用了相同的代码,那么将会调用缓冲区中的版本。这样如果一个.Net程序第二次被运行,那么这种翻译不需要进行第二次,速度明显加快。
c#是组件编程,是90年代面向对象编程的深度发展。组件编程已经成为当今世界软件业面向下一代程序开发的一致选择。下面我将给大家介绍c#无处不在的面向组件编程。
首先让我们先暂时告别微软给我们提供的VisualStudio,用windows自带的记事本和微软提供给我们开发者的Visual Studio 2012(2010,2008) 命令提示程序手动运行一个HelloWorld程序。(p.s.如果想用命令提示符[cmd]的话需要配置系统环境变量,就像搭建java环境时做的那样)
(一)新建一个记事本,输入如下代码,另存为HelloWorld.cs,其中".cs"是C#源代码文件的扩展名(注:笔者亲测,源代码用任意的后缀,都可以编译出.exe文件)
using System; namespace HelloWorld { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.ReadKey(); } } }
(二)打开Visual Studio 2012(2010,2008) 命令提示程序,如(图一) 注:笔者是win8和vs2012
(图一)
(三)切换到HelloWorld.cs的目录(注:笔者放在D盘),并运行命令:csc /out:Hello.exe HelloWorld.cs 结果如(图二)
(图二)
(四)此时运行Hello.exe将会打印出Hello World.
下面我们来仔细分析上面的代码和整个程序的编译输出及执行过程。
首先第一行的"using System;"语句是C#语言的using命名空间指示符,这里的"System"是微软提供的基础类库。C#语言没有自己的语言类库,它直接获取.NET的系统类库。using语句使得我们可以用简短的别名"Console"来代替类型"System.Console",就像在c++里面的using namespace ***一样,所以同样可以得知using指示符并不是必须的,我们可以用类型的全局名字来获取类型。
接着我们声明并实现了一个含有静态Main()函数的Program类。C#所有的声明和实现都要放在同一个文件里,不像C++那样可以将两者分离。Main()函数在C#里非常特殊,它是编译器规定的所有可执行程序的入口点。由于其特殊性,对Main()函数我们有以下几条准则:
Main()函数必须封装在类或结构里来提供可执行程序的入口点。C#采用了完全的面向对象的编程方式,C#中不可以有像C++那样的全局函数。
Main()函数必须为静态函数(static)。这允许C#不必创建实例对象即可运行程序。
Main()函数保护级别没有特殊要求, public,protected,private等都可,但一般我们都指定其为public。
Main()函数名的第一个字母要大写,否则将不具有入口点的语义。C#是大小写敏感的语言。
Main()函数的参数只有两种参数形式:无参数和string 数组表示的命令行参数,即static void Main()或static void Main(string[]args) ,后者接受命令行参数。一个C#程序中只能有一个Main()函数入口点。其他形式的参数不具有入口点语义,C#不推荐通过其他参数形式重载Main()函数,这会引起编译警告。
Main()函数返回值只能为void(无类型)或int(整数类型)。其他形式的返回值不具有入口点语义。
我们再来看"HelloWorld.cs"程序中Main()函数的内部实现。前面提过,Console是在命名空间System下的一个类,它表示我们通常打交道的控制台。而我们这里是调用其静态方法WriteLine()。如同C++一样,静态方法允许我们直接作用于类而非实例对象。WriteLine()函数接受字符串类型的参数"Hello World !",并把它送入控制台显示。如前所述,C#没有自己的语言类库,它直接获取Microsoft.NET系统类库。我们这里正是通过获取Microsoft.NET系统类库中的System.Console.WriteLine()来完成我们想要的控制台输出操作。这样我们便完成了"Hello World!"程序。
我们先来看我们对"HelloWorld.cs"文件用csc.exe命令编译后发生了什么。首先我们得到了Hello.exe文件。但那仅仅是事情的表象,正如文章开始所说,Hello.exe根本不是一个可执行文件!它是编译输出的HelloWorld.exe是一个由中间语言(IL),元数据(Metadata)和一个额外的被编译器添加的目标平台的标准可执行文件头(比如Win32平台就是加了一个标准Win32可执行文件头)组成的PE(portable executable,可移植执行体)文件,而不是传统的二进制可执行文件--虽然他们有着相同的扩展名。
上面说的过程大致如下图:
中间语言是一组独立于CPU的指令集,它可以被即时编译器翻译成目标平台的本地代码。中间语言代码使得所有Microsoft.NET平台的高级语言C#,VB.NET,VC.NET等得以平台独立,以及语言之间实现互操作。元数据是一个内嵌于PE文件的表的集合。元数据描述了代码中的数据类型等一些通用语言运行时(Common Language Runtime)需要在代码执行时知道的信息。元数据使得.NET应用程序代码具备自描述特性,提供了类型安全保障,这在以前需要额外的类型库或接口定义语言(Interface Definition Language,简称IDL)。
知道了这么多,现在让我们回过头看看刚才我们写的HelloWorld是怎么运行的,首先呗编译器编译生成了程序集,程序集内的是IL代码,根据上面的解释可知它还不是可运行的代码,IL是与CPU无关的机器语言.直到程序集被调用,才会由JIT(Just-in-Time,实时)编译器编译为本机代码(CPU指令).重新强调一下在运行时,CLR执行如下步骤:
static void Main(string[] args) { Console.WriteLine("Hello"); Console.WriteLine("World!"); Console.ReadKey(); }
这样的解释可能还是有点让人困惑,那么我们来实际的解剖一下这个PE文件。我们采用的工具是.NET SDK中自带的ildasm.exe,它可以帮助我们提取PE文件中的有关数据。我们键入命令"ildasm /output:HelloWorld.il HelloWorld.exe",一般可以得到两个输出文件:helloworld.il和helloworld.res。其中后者是提取的资源文件,我们暂且不管,我们来看helloworld.il文件。我们用"记事本"程序打开可以看到元数据和中间语言(IL)代码,由于篇幅关系,我们只将其中的中间语言代码提取出来列于下面,有关元数据的表项我们暂且不谈:
class private auto ansi beforefieldinit Program extends [mscorlib]System.Object { .method public hidebysig static void Main() cil managed { .entrypoint // Code size 11 (0xb) .maxstack 8 IL_0000: ldstr "Hello World !" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } // end of method Program ::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method Program::.ctor
} // end of class Program
我们粗略的感受是它很类似于早先的汇编语言,但它具有了对象定义和操作的功能。我们可以看到它定义并实现了一个继承自System.Object 的Program类及两个函数:Main()和.ctor()。其中.ctor()是Program类的构造函数,可在"HelloWorld.cs"源代码中我们并没有定义构造函数呀--是的,我们没有定义构造函数,但C#的编译器为我们添加了它。你还可以看到C#编译器也强制Program类继承System.Object类,虽然这个我们也没有指定。
下面我们总结一下PE文件时怎样运行的
用户执行编译器输出的应用程序(PE文件),操作系统载入PE文件,以及其他的DLL(.NET动态连接库)。 操作系统装载器根据前面PE文件中的可执行文件头跳转到程序的入口点。显然,操作系统并不能执行中间语言,该入口点也被设计为跳转到mscoree.dll(.NET平台的核心支持DLL)的_ CorExeMain()函数入口。 CorExeMain()函数开始执行PE文件中的中间语言代码。这里的执行的意思是通用语言运行时按照调用的对象方法为单位,用即时编译器将中间语言编译成本地机二进制代码,执行并根据需要存于机器缓存。程序的执行过程中,垃圾收集器负责内存的分配,释放等管理功能。 程序执行完毕,操作系统卸载应用程序。清楚的知晓编译输出的PE文件的执行过程是深度掌握C#语言编程的关键,这种过程的本身就诠释着C#语言的高级内核机制以及其背后Microsoft.NET平台种种诡秘的性质。一个"Hello World !"程序的概括力已经足够,在我们对C#语言有了一个很好的起点之后,下面的专题会和大家一起学习些C#基础特点,看看Microsoft.NET平台构造。