C#的内存管理知识 - dz_huanbao的专栏 - CSDN博客

来源:百度文库 编辑:神马文学网 时间:2024/04/28 15:01:34
 C#的内存管理知识 收藏
本章介绍内存管理和内存访问的各个方面。尽管运行库负责为程序员处理大部分内存管理工作,但程序员仍必须理解内存管理的工作原理,了解如何处理未托管的资源。如果很好地理解了内存管理和C#提供的指针功能,也就能很好地集成C#代码和原来的代码,并能在非常注重性能的系统中高效地处理内存。本章的主要内容如下:●       运行库如何在堆栈和堆上分配空间●       垃圾收集的工作原理●       如何使用析构函数和System.IDisposable接口来确保正确释放未托管的资源●       C#中使用指针的语法●       如何使用指针实现基于堆栈的高性能数组11.1  后台内存管理
C#编程的一个优点是程序员不需要担心具体的内存管理,尤其是垃圾收集器会处理所有的内存清理工作。用户可以得到像C++语言那样的效率,而不需要考虑像在C++中那样内存管理工作的复杂性。虽然不必手工管理内存,但如果要编写高效的代码,就仍需理解后台发生的事情。本节要介绍给变量分配内存时计算机内存中发生的情况。 注意:本节的许多内容是没有经过事实证明的。您应把这一节看作是一般规则的简化向导,而不是实现的确切说明。11.1.1  值数据类型
Windows使用一个系统:虚拟寻址系统,该系统把程序可用的内存地址映射到硬件内存中的实际地址上,这些任务完全由Windows在后台管理,其实际结果是32位处理器上的每个进程都可以使用4GB的内存—— 无论计算机上有多少硬盘空间。(在64位处理器上,这个数字会更大)。这个4GB内存实际上包含了程序的所有部分,包括可执行代码、代码加载的所有DLL,以及程序运行时使用的所有变量的内容。这个4GB内存称为虚拟地址空间,或虚拟内存,为了方便起见,本章将它简称为内存。4GB中的每个存储单元都是从0开始往上排序的。要访问存储在内存的某个空间中的一个值,就需要提供表示该存储单元的数字。在任何复杂的高级语言中,例如C#、VB、C++和Java,编译器负责把人们可以理解的变量名称转换为处理器可以理解的内存地址。在进程的虚拟内存中,有一个区域称为堆栈。堆栈存储不是对象成员的值数据类型。另外,在调用一个方法时,也使用堆栈存储传递给方法的所有参数的复本。为了理解堆栈的工作原理,需要注意在C#中变量的作用域。如果变量a在变量b之前进入作用域,b就会先出作用域。下面的代码:   {      int a;      // do something      {         int b;         // do something else      }   }首先声明a。在内部的代码块中声明了b。然后内部的代码块终止,b就出作用域,最后a出作用域。所以b的生存期会完全包含在a的生存期中。在释放变量时,其顺序总是与给它们分配内存的顺序相反,这就是堆栈的工作方式。我们不知道堆栈在地址空间的什么地方,这些信息在进行C#开发是不需要知道的。堆栈指针(操作系统维护的一个变量) 表示堆栈中下一个自由空间的地址。程序第一次运行时,堆栈指针指向为堆栈保留的内存块末尾。堆栈实际上是向下填充的,即从高内存地址向低内存地址填充。当数据入栈后,堆栈指针就会随之调整,以始终指向下一个自由空间。这种情况如图11-1所示。在该图中,显示了堆栈指针800000(十六进制的0xC3500),下一个自由空间是地址799999。
图  11-1下面的代码会告诉编译器,需要一些存储单元以存储一个整数和一个双精度浮点数,这些存储单元会分别分配给nRacingCars和engineSize,声明每个变量的代码表示开始请求访问这个变量,闭合花括号表示这两个变量出作用域的地方。   {      int nRacingCars = 10;      double engineSize = 3000.0;      // do calculations;   }假定使用如图11-1所示的堆栈。变量nRacingCars进入作用域,赋值为10,这个值放在存储单元799996~799999上,这4个字节就在堆栈指针所指空间的下面。有4个字节是因为存储int要使用4个字节。为了容纳该int,应从堆栈指针中减去4,所以它现在指向位置799996,即下一个自由空间 (799995)。下一行代码声明变量engineSize(这是一个double),把它初始化为3000.0。double要占用8个字节,所以值3000.0占据栈上的存储单元799988~799995上,堆栈指针减去8,再次指向堆栈上的下一个自由空间。当engineSize出作用域时,计算机就知道不再需要这个变量了。因为变量的生存期总是嵌套的,当engineSize在作用域中时,无论发生什么情况,都可以保证堆栈指针总是会指向存储engineSize的空间。为了从内存中删除这个变量,应给堆栈指针递增8,现在指向engineSize使用过的空间。此处就是放置闭合花括号的地方。当nRacingCars也出作用域时,堆栈指针就再次递增4,此时如果内存中又放入另一个变量,从799999开始的存储单元就会被覆盖,这些空间以前是存储nRacingCars的。如果编译器遇到像int i、j这样的代码,则这两个变量进入作用域的顺序就是不确定的:两个变量是同时声明的,也是同时出作用域的。此时,变量以什么顺序从内存中删除就不重要了。编译器在内部会确保先放在内存中的那个变量后删除,这样就能保证该规则不会与变量的生存期冲突。11.1.2  引用数据类型
堆栈有非常高的性能,但对于所有的变量来说还是不太灵活。变量的生存期必须嵌套,在许多情况下,这种要求都过于苛刻。通常我们希望使用一个方法分配内存,来存储一些数据,并在方法退出后的很长一段时间内数据仍是可以使用的。只要是用new运算符来请求存储空间,就存在这种可能性——例如所有的引用类型。此时就要使用托管堆。如果以前编写过需要管理低级内存的C++代码,就会很熟悉堆(heap)。托管堆和C++使用的堆不同,它在垃圾收集器的控制下工作,与传统的堆相比有很显著的性能优势。托管堆(简称为堆)是进程的可用4GB中的另一个内存区域。要了解堆的工作原理和如何为引用数据类型分配内存,看看下面的代码:   void DoWork()   {      Customer arabel;      arabel = new Customer();      Customer otherCustomer2 = new EnhancedCustomer();   }在这段代码中,假定存在两个类Customer 和 EnhancedCustomer。EnhancedCustomer类扩展了Customer类。首先,声明一个Customer引用arabel,在堆栈上给这个引用分配存储空间,但这仅是一个引用,而不是实际的Customer对象。arabel引用占用4个字节的空间,包含了存储Customer对象的地址(需要4个字节把内存地址表示为0到4GB之间的一个整数值)。然后看下一行代码:      arabel = new Customer();这行代码完成了以下操作:首先,分配堆上的内存,以存储Customer实例(一个真正的实例,不只是一个地址)。然后把变量arabel的值设置为分配给新Customer对象的内存地址(它还调用合适的Customer()构造函数初始化类实例中的字段,但我们不必担心这部分)。
Customer实例没有放在堆栈中,而是放在内存的堆中。在这个例子中,现在还不知道一个Customer对象占用多少字节,但为了讨论方便,假定是32字节。这32字节包含了Customer实例字段,和.NET用于识别和管理其类实例的一些信息。为了在堆上找到一个存储新Customer对象的存储位置,.NET运行库在堆中搜索,选取第一个未使用的、32字节的连续块。为了讨论方便,假定其地址是200000,arabel引用占用堆栈中的799996~799999位置。这表示在实例化arabel对象前,内存的内容应如图11-2所示。
图  11-2给Customer对象分配空间后,内存内容应如图11-3所示。注意,与堆栈不同,堆上的内存是向上分配的,所以自由空间在已用空间的上面。
图  11-3下一行代码声明了一个Customer引用,并实例化一个Customer对象。在这个例子中,需要在堆栈上为mrJones引用分配空间,同时,也需要在堆上为它分配空间:      Customer otherCustomer2 = new EnhancedCustomer();该行把堆栈上的4字节分配给otherCustomer2引用,它存储在799992~799995位置上,而otherCustomer2对象在堆上从200032开始向上分配空间。从这个例子可以看出,建立引用变量的过程要比建立值变量的过程更复杂,且不能避免性能的降低。实际上,我们对这个过程进行了过分的简化,因为.NET运行库需要保存堆的状态信息,在堆中添加新数据时,这些信息也需要更新。尽管有这些性能损失,但仍有一种机制,在给变量分配内存时,不会受到堆栈的限制。把一个引用变量的值赋予另一个相同类型的变量,就有两个引用内存中同一对象的变量了。当一个引用变量出作用域时,它会从堆栈中删除,如上一节所述,但引用对象的数据仍保留在堆中,一直到程序停止,或垃圾收集器删除它为止,而只有在该数据不再被任何变量引用时,才会被删除。这就是引用数据类型的强大之处,在C#代码中广泛使用了这个特性。这说明,我们可以对数据的生存期进行非常强大的控制,因为只要有对数据的引用,该数据就肯定存在于堆上。11.1.3  垃圾收集
由上面的讨论和图可以看出,托管堆的工作方式非常类似于堆栈,在某种程度上,对象会在内存中一个挨一个地放置,这样就很容易使用指向下一个空闲存储单元的堆指针,来确定下一个对象的位置。在堆上添加更多的对象时,也容易调整。但这比较复杂,因为基于堆的对象的生存期与引用它们的基于堆栈的变量的作用域不匹配。在垃圾收集器运行时,会在堆中删除不再引用的所有对象。在完成删除动作后,堆会立即把对象分散开来,与已经释放的内存混合在一起,如图11-4所示。
图  11-4如果托管的堆也是这样,在其上给新对象分配内存就成为一个很难处理的过程,运行库必须搜索整个堆,才能找到足够大的内存块来存储每个新对象。但是,垃圾收集器不会让堆处于这种状态。只要它释放了能释放的所有对象,就会压缩其他对象,把它们都移动回堆的端部,再次形成一个连续的块。因此,堆可以继续像堆栈那样确定在什么地方存储新对象。当然,在移动对象时,这些对象的所有引用都需要用正确的新地址来更新,但垃圾收集器也会处理更新问题。垃圾收集器的这个压缩操作是托管的堆与旧未托管的堆的区别所在。使用托管的堆,就只需要读取堆指针的值即可,而不是搜索链接地址列表,来查找一个地方来放置新数据。因此,在.NET下实例化对象要快得多。有趣的是,访问它们也比较快,因为对象会压缩到堆上相同的内存区域,这样需要交换的页面较少。Microsoft相信,尽管垃圾收集器需要做一些工作,压缩堆,修改它移动的所有对象引用,致使性能降低,但这些性能会得到弥补。注意:一般情况下,垃圾收集器在.NET运行库认为需要时运行。可以通过调用System. GC.Collect(),强迫垃圾收集器在代码的某个地方运行,System.GC是一个表示垃圾收集器的.NET基类, Collect()方法则调用垃圾收集器。但是,这种方式适用的场合很少,例如,代码中有大量的对象刚刚停止引用,就适合调用垃圾收集器。但是,垃圾收集器的逻辑不能保证在一次垃圾收集过程中,所有未引用的对象都从堆中删除。11.2  释放未托管的资源垃圾收集器的出现意味着,通常不需要担心不再需要的对象,只要让这些对象的所有引用都超出作用域,并允许垃圾收集器在需要时释放资源即可。但是,垃圾收集器不知道如何释放未托管的资源(例如文件句柄、网络连接和数据库连接)。托管类在封装对未托管资源的直接或间接引用时,需要制定专门的规则,确保未托管的资源在回收类的一个实例时释放。
在定义一个类时,可以使用两种机制来自动释放未托管的资源。这些机制常常放在一起实现,因为每个机制都为问题提供了略为不同的解决方法。这两个机制是:
●       声明一个析构函数(或终结器),作为类的一个成员
●       在类中执行System.IDisposable接口
下面依次讨论这两个机制,然后介绍如何同时实现它们,以获得最佳的效果。
11.2.1  析构函数
前面介绍了构造函数可以指定必须在创建类的实例时进行的某些操作,在垃圾收集器删除对象之前,也可以调用析构函数。由于执行这个操作,所以析构函数初看起来似乎是放置释放未托管资源、执行一般清理操作的代码的最佳地方。但是,事情并不是如此简单。注意:
在讨论C#中的析构函数时,在底层的.NET结构中,这些函数称为终结器(finalizer)。在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法。这不会影响源代码,但如果需要查看程序集的内容,就应知道这个事实。C++开发人员应很熟悉析构函数的语法,它看起来类似于一个方法,与包含类同名,但前面加上了一个发音符号(~)。它没有返回类型,不带参数,没有访问修饰符。下面是一个例子:class MyClass {   ~MyClass()     {      // destructor implementation   }}C#编译器在编译析构函数时,会隐式地把析构函数的代码编译为Finalize()方法的对应代码,确保执行父类的Finalize()方法。下面列出了编译器为~MyClass()析构函数生成的IL的对应C#代码:protected override void Finalize(){   try   {      // destructor implementation   }   finally   {      base. Finalize();   }}如上所示,在~MyClass()析构函数中执行的代码封装在Finalize()方法的一个try块中。对父类Finalize()方法的调用放在finally块中,确保该调用的执行。第13章会讨论try块和finally块。有经验的C++开发人员大量使用了析构函数,有时不仅用于清理资源,还提供调试信息或执行其他任务。C#析构函数的使用要比在C++中少得多,与C++析构函数相比,C#析构函数的问题是它们的不确定性。在删除C++对象时,其析构函数会立即运行。但由于垃圾收集器的工作方式,无法确定C#对象的析构函数何时执行。所以,不能在析构函数中放置需要在某一时刻运行的代码,也不应使用能以任意顺序对不同类实例调用的析构函数。如果对象占用了宝贵而重要的资源,应尽快释放这些资源,此时就不能等待垃圾收集器来释放了。另一个问题是C#析构函数的执行会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾收集器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能删除:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。另外,运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数,而且使用它们执行长时间的清理任务,对性能的影响就会非常显著。11.2.2  IDisposable接口在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一个模式(具有语言级的支持),为释放未托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾函数器相关的问题。IDisposable接口声明了一个方法Dispose(),它不带参数,返回void,Myclass的方法Dispose()的执行代码如下:class Myclass : IDisposable{   public void Dispose()    {      // implementation   }}Dispose()的执行代码显式释放由对象直接使用的所有未托管资源,并在所有实现IDisposable接口的封装对象上调用Dispose()。这样,Dispose()方法在释放未托管资源的时间方面提供了精确的控制。假定有一个类ResourceGobbler,它使用某些外部资源,且执行IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码: ResourceGobbler theInstance = new ResourceGobbler();    // do your processing   theInstance.Dispose();如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所以应使用try块(详见第13章),编写下面的代码:ResourceGobbler theInstance = null;try{   theInstance = new ResourceGobbler();// do your processing }finally  {   if (theInstance != null)  {theInstance.Dispose(); }}即使在处理过程中出现了异常,这个版本也可以确保总是在theInstance上调用Dispose(),总是释放由theInstance使用的资源。但是,如果总是要重复这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在执行IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()。该语法使用了using关键字来完成这一工作—— 但目前,在完全不同的环境下,它与命名空间没有关系。下面的代码生成与try块相对应的IL代码:using (ResourceGobbler theInstance = new ResourceGobbler()){   // do your processing }using语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量放在随附的语句块中。另外,在变量超出作用域时,即使出现异常,也会自动调用其Dispose()方法。如果已经使用try块来捕获其他异常,就会比较清晰,如果避免使用using语句,仅在已有的try块的finally子句中调用Dispose(),还可以避免进行额外的缩进。注意:对于某些类来说,使用Close()方法要比Dispose()更富有逻辑性,例如,在处理文件或数据库连接时就是这样。在这些情况下,常常实现IDisposable接口,再执行一个独立的Close()方法,来调用Dispose()。这种方法在类的使用上比较清晰,还支持C#提供的using语句。11.2.3  实现IDisposable接口和析构函数
前面的章节讨论了类所使用的释放未托管资源的两种方式:●       利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾收集器的工作方式,它会给运行库增加不可接受的系统开销。●       IDisposable接口提供了一种机制,允许类的用户控制释放资源的时间,但需要确保执行Dispose()。一般情况下,最好的方法是执行这两种机制,获得这两种机制的优点,克服其缺点。假定大多数程序员都能正确调用Dispose(),同时把执行析构函数作为一种安全的机制,以防没有调用Dispose()。下面是一个双重实现的例子:public class ResourceHolder : IDisposable{private bool isDispose = false;public void Dispose()     {        Dispose(true);        GC.SuppressFinalize(this);     }    protected virtual void Dispose(bool disposing)    {      if (!isDisposed)      {          if (disposing) {               // Cleanup managed objects by calling their// Dispose() methods.          }          // Cleanup unmanaged objects       }       isDisposed=true;   }    ~ResourceHolder()   {      Dispose (false);   }    public void SomeMethod()    {      // Ensure object not already disposed before execution of any method      if(isDisposed)       {         throw new ObjectDisposedException("ResourceHolder");      }       // method implementation…   }}可以看出,Dispose()有第二个protected重载方法,它带一个bool参数,这是真正完成清理工作的方法。Dispose(bool)由析构函数和IDisposable.Dispose()调用。这个方式的重点是确保所有的清理代码都放在一个地方。传递给Dispose(bool)的参数表示Dispose(bool)是由析构函数调用,还是由IDisposable. Dispose()调用—— Dispose(bool)不应从代码的其他地方调用,其原因是:●       如果客户调用IDisposable.Dispose(),该客户就指定应清理所有与该对象相关的资源,包括托管和非托管的资源。●       如果调用了析构函数,原则上所有的资源仍需要清理。但是在这种情况下,析构函数必须由垃圾收集器调用,而且不应访问其他托管的对象,因为我们不再能确定它们的状态了。在这种情况下,最好清理已知的未托管资源,希望引用的托管对象还有析构函数,执行自己的清理过程。isDisposed成员变量表示对象是否已被删除,并允许确保不多次删除成员变量。它还允许在执行实例方法之前测试对象是否已释放,如SomeMethod()所示。这个简单的方法不是线程安全的,需要调用者确保在同一时刻只有一个线程调用方法。要求客户进行同步是一个合理的假定,在整个.NET类库中反复使用了这个假定(例如在集合类中)。第18章将讨论线程和同步。最后,IDisposable.Dispose()包含一个对System.GC.SuppressFinalize()方法的调用。GC 表示垃圾收集器,SuppressFinalize()方法则告诉垃圾收集器有一个类不再需要调用其析构函数了。因为Dispose()已经完成了所有需要的清理工作,所以析构函数不需要做任何工作。调用SuppressFinalize()就意味着垃圾收集器认为这个对象根本没有析构函数。11.3  不安全的代码
如前面的章节所述,C#非常擅长于隐藏基本内存管理,因为它使用了垃圾收集器和引用。但是,有时需要直接访问内存,例如由于性能问题,要在外部(非.NET环境)的DLL中访问一个函数,该函数需要把一个指针当作参数来传递(许多Windows API函数就是这样)。本节将论述C#直接访问内存内容的功能。11.3.1  指针
下面把指针当作一个新论题来介绍,而实际上,指针并不是新东西,因为在代码中可以自由使用引用,而引用就是一个类型安全的指针。前面已经介绍了表示对象和数组的变量实际上包含存储相应数据(引用)的内存地址。指针只是一个以与引用相同的方式存储地址的变量。其区别是C#不允许直接访问引用变量包含的地址。有了引用后,从语法上看,变量就可以存储引用的实际内容。C#引用主要用于使C#语言易于使用,防止用户无意中执行某些破坏内存中内容的操作,另一方面,使用指针,就可以访问实际内存地址,执行新类型的操作。例如,给地址加上4字节,就可以查看甚至修改存储在新地址中的数据。
下面是使用指针的两个主要原因:●       向后兼容性。尽管.NET运行库提供了许多工具,但仍可以调用内部的Windows API 函数。 对于某些操作来说,这可能是完成任务的唯一方式。这些API函数都是用C语言编写的,通常要求把指针作为其参数。但在许多情况下,还可以使用DllImport声明,以避免使用指针,例如使用System.IntPtr类。●       性能。在一些情况下,速度是最重要的,而指针可以提供最优性能。假定用户知道自己在做什么,就可以确保以最高效的方式访问或处理数据。但是,注意在代码的其他区域中,不使用指针,也可以对性能做必要的改进。请使用代码配置文件,查找代码中的瓶颈,代码配置文件随VS2005一起安装。但是,这种低级内存访问也是有代价的。使用指针的语法比引用类型更复杂。而且,指针使用起来比较困难,需要非常高的编程技巧和很强的能力,仔细考虑代码所完成的逻辑操作,才能成功地使用指针。如果不仔细,使用指针很容易在程序中引入微妙的难以查找的错误。例如很容易重写其他变量,导致堆栈溢出,访问某些没有存储变量的内存区域,甚至重写.NET运行库所需要的代码信息,因而使程序崩溃。另外,如果使用指针,就必须为代码获取代码访问安全机制的高级别信任,否则就不能执行。在默认的代码访问安全策略中,只有代码运行在本地机器上,这才是可能的。如果代码必须运行在远程地点,例如Internet,用户就必须给代码授予额外的许可,代码才能工作。除非用户信任您和代码,否则他们不会授予这些许可,第19章将讨论代码访问安全性。
尽管有这些问题,但指针在编写高效的代码时是一种非常强大和灵活的工具,这里就介绍指针的使用。注意:这里强烈建议不要使用指针,因为如果使用指针,代码不仅难以编写和调试,而且无法通过CLR的内存类型安全检查(详见第1章)。1. 编写不安全的代码
因为使用指针会带来相关的风险,所以C#只允许在特别标记的代码块中使用指针。标记代码所用的关键字是unsafe。下面的代码把一个方法标记为unsafe:unsafe int GetSomeNumber(){   // code that can use pointers}任何方法都可以标记为unsafe—— 无论该方法是否应用了其他修饰符(例如,静态方法、虚拟方法等)。在这种方法中,unsafe修饰符还会应用到方法的参数上,允许把指针用作参数。还可以把整个类或结构标记为unsafe,表示所有的成员都是不安全的:unsafe class MyClass{   // any method in this class can now use pointers}同样,可以把成员标记为unsafe:class MyClass{   unsafe int *pX;   // declaration of a pointer field in a class}也可以把方法中的一个代码块标记为unsafe:void MyMethod(){   // code that doesn't use pointers   unsafe   {      // unsafe code that uses pointers here   }   // more 'safe' code that doesn't use pointers}但要注意,不能把局部变量本身标记为unsafe: int MyMethod(){    unsafe int *pX;   // WRONG}如果要使用不安全的局部变量,就需要在不安全的方法或语句块中声明和使用它。在使用指针前还有一步要完成。C#编译器会拒绝不安全的代码,除非告诉编译器代码包含不安全的代码块。标记所用的关键字是unsafe。因此,要编译包含不安全代码块的文件MySource.cs(假定没有其他编译器选项),就要使用下述命令:csc /unsafe MySource.cs
或者csc –unsafe MySource.cs
注意:如果使用Visual Studio 2005,就可以在项目属性窗口中找到编译不安全代码的选项。2. 指针的语法
把代码块标记为unsafe后,就可以使用下面的语法声明指针:int* pWidth, pHeight;double* pResult;byte*[] pFlags;这段代码声明了4个变量,pWidth和pHeight是整数指针,pResult是double型指针,pFlags是byte型的指针数组。我们常常在指针变量名的前面使用前缀p来表示这些变量是指针。在变量声明中,符号*表示声明一个指针,换言之,就是存储特定类型的变量的地址。提示:C++开发人员应注意,这个语法与C#中的语法是不同的。C#语句中的int* pX, pY; 对应于C++ 语句中的 int *pX, *pY;在C#中,*符号与类型相关,而不是与变量名相关。声明了指针类型的变量后,就可以用与一般变量的方式使用它们,但首先需要学习另外两个运算符:●       & 表示“取地址”,并把一个值数据类型转换为指针,例如int转换为*int。这个运算符称为寻址运算符。●       * 表示“获取地址的内容”,把一个指针转换为值数据类型(例如,*float转换为float)。这个运算符称为“间接寻址运算符”(有时称为“取消引用运算符”)。从这些定义中可以看出,&和*的作用是相反的。注意:符号&和*也表示按位AND(&)和乘法(*)运算符,那么如何以这种方式使用它们?答案是在实际使用时它们是不会混淆的:用户和编译器总是知道在什么情况下这两个符号有什么含义,因为按照新指针的定义,这些符号总是以一元运算符的形式出现—— 它们只作用于一个变量,并出现在代码中变量的前面。另一方面,按位AND和乘法运算符是二元运算符,它们需要两个操作数。下面的代码说明了如何使用这些运算符:int x = 10;int* pX, pY;pX = &x;pY = pX;*pY = 20;首先声明一个整数x,其值是10。接着声明两个整数指针pX和pY。然后把pX设置为指向x(换言之,把pX的内容设置为x的地址)。把pX的值赋予pY,所以pY也指向x。最后,在语句*pY = 20中,把值20赋予pY指向的地址。实际上是把x的内容改为20,因为pY指向x。注意在这里,变量pY和x之间没有任何关系。只是此时pY碰巧指向存储x的存储单元而已。要进一步理解这个过程,假定x存储在堆栈的存储单元0x12F8C4到0x12F8C7中(十进制就是1243332到1243335,即有4个存储单元,因为int占用4字节)。因为堆栈向下分配内存,所以变量pX存储在0x12F8C0到 0x12F8C3的位置上,pY存储在0x12F8BC 到 0x12F8BF的位置上。注意,pX和pY也分别占用4字节。这不是因为int占用4字节,而是因为在32位处理器上,需要用4字节存储一个地址。利用这些地址,在执行完上述代码后,堆栈应如图11-5所示。
图  11-5注意:这个示例使用int来说明该过程,其中int存储在32位处理器中堆栈的连续空间上,但并不是所有的数据类型都会存储在连续的空间中。原因是32位处理器最擅长于在4字节的内存块中获取数据。这种机器上的内存会分解为4字节的块,在Windows上,每个块都时常称为DWORD,因为这是32位无符号int在.NET出现之前的名字。这是从内存中获取DWORD的最高效的方式—— 跨越DWORD边界存储数据通常会降低硬件的性能。因此,.NET运行库通常会给某些数据类型加上一些空间,使它们占用的内存是4的倍数。例如,short数据占用2字节,但如果把一个short放在堆栈中,堆栈指针仍会减少4,而不是2,这样,下一个存储在堆栈中的变量就仍从DWORD的边界开始存储。可以把指针声明为任意一种数据类型—— 即任何预定义的数据类型uint、int和byte等,也可以声明为一个结构。但是不能把指针声明为一个类或数组,因为这么做会使垃圾收集器出现问题。为了正常工作,垃圾收集器需要知道在堆上创建了什么类实例,它们在什么地方。但如果代码使用指针处理类,将很容易破坏堆中.NET运行库为垃圾收集器维护的与类相关的信息。在这里,垃圾收集器可以访问的数据类型称为托管类型,而指针只能声明为非托管类型,因为垃圾收集器不能处理它们。3. 将指针转换为整数类型
由于指针实际上存储了一个表示地址的整数,所以任何指针中的地址都可以转换为任何整数类型。指针到整数类型的转换必须是显式指定的,隐式的转换是不允许的。例如,编写下面的代码是合法的:int x = 10;
int* pX, pY;
pX = &x;
pY = pX;
*pY = 20;
uint y = (uint)pX;
int* pD = (int*)y;
把指针pX中包含的地址转换为一个uint,存储在变量y中。接着把y转换回int*,存储在新变量pD中。因此pD也指向x的值。把指针的值转换为整数类型的主要目的是显示它。Console.Write()和Console. WriteLine()方法没有带指针的重载方法,所以必须把指针转换为整数类型,这两个方法才能接受和显示它们:Console.WriteLine("Address is" + pX);   // wrong – will give a                                  // compilation errorConsole.WriteLine("Address is" + (uint) pX);   // OK可以把一个指针转换为任何整数类型,但是,因为在32位系统上,地址占用4字节,把指针转换为不是uint、long 或 ulong的数据类型,肯定会导致溢出错误(int也可能导致这个问题,因为它的取值范围是–20亿~20亿,而地址的取值范围是0~40亿
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dz_huanbao/archive/2008/11/17/3313449.aspx