Delphi 关于内存和指针操作,数据类型转换的本质的理解。
很多朋友问的问题感觉都是没有理解内存和指针与数据类型之间的关系。想解释一下。
   很少写东西,觉得有些东西不好表达,就想到那说到那了,希望能提供一些帮助。
指针的使用,和使用指针直接读取数据是软件开发中经常使用到的技术,也是软件开发所需要掌握的基础,理解并能灵活的使用指针来操作内存,读写数据是软件开发必须要熟练掌握的基本。
内存可以看成是下图一个个的带颜色的小格子,每个小格子是一个字节的长度,<图一>共显示了8个小格式了,每个格子是一个字节,每个字节是由8bit 组成。<图一>
因为我们一般操作的最小单元就是一个字节,所以展示内存布局的时候就用<图二>了。
 
<图二> 图上方的 1 2 ,3 …8 代表每个格子的地址。就象是房号一样。
  内存的使用的是小单位是一个字节,Byte 类型和Char类型都是一个字节的长度。SizeOf(Byte)返回值是1,对计算机来说并不知道数据类型是 Byte还是Char ,它只是知道这个8个bit 的数据。
Var
  B:Byte; 
  C:Char;
Begin
  C:=’A’; 
  B:=65;
End;
这两上类型在内存中的保存都是二进制:01000001 ,当然因为我们能操作的最小是一个字节,即8个二进制长。01000001用十六进制表示为: $41。@B 表示得到数据B所在的内存位置,即数据所保存的房间号。如<图二>,@B将所返回 1 。如果告诉你说这个字节保存的是 $41 ,你可能将它看成是 Byte 类型,也可以看成是 Char类型,这些数据类型只是人们自己为了操作方便而起的名字罢了。
Size(Word) 返回的长度是2 ,即两个字节。Var W:Word;  定义一个Word类型的变量,@W将返回的是第一个房间号,因为系统知道Word 是占用两个字节,所以在操作时候会将两个房间的数据来操作。如 @W返回的是 3 ,如<图三>
 
<图三>
则W的值是 17475 十六进制 $4443 。
 Arr1:Array[0..1] of Byte; 一个占两个字节的数组,它的房号是 @Arr1 或者 @Arr1[0]。
 如果 @Arr1 的值也是 3 则 Arr1[0]为$43 Arr1[1]为$44。
 Arr2:Array[0..1] of  Char ; 也是一个占两个字节的数组,如果 @Arr2 的值也是 3 则 Arr2[0]为C Arr2[1]为D。因为AScall码表中用 $43 即 67表示大字字母 C。用 $44 即 68表示大字字母 D。
以上说明,内存中保存的只是一些数字,至于这个数字是代表什么意思,是我们自己决定的,我们定义了很多的数据类型,如 Byte;Char;Word;Integer;Array [0..1] of Byte;Array [0..1] of  Char;还有更多我们自己定义的结构。如
TMyInfo= packet record
  ID:Byte;
  年龄:Byte;
  进球数:Word
End;Var MyInfo: TMyInfo;  @ MyInfo返回的地址是3 ,那ID=67,年龄=68岁,进球数=69个。
我们也可定义成如下这样的结构。说明每个 TMyInfo2结构占用SizeOf(TMyInfo2)=5 字节
TMyInfo2=packet record
  进球数:Word
  ID: Word;
  年龄:Byte;
End;
Var MyInfo2: TMyInfo2;  @ MyInfo2返回的地址是3 ,则内存中的数据表示的意思就成了,进球数=17475($4443) ,ID= 17989个($4645),年龄=71($47)岁
以上两个结构的内存布局是一样的,大小也是一样的,只是我们对它的解释不同,两个结构体就象是两个模具,对内存数据的意义根据模具的描述来确定。
上面两个结构也可以和下面的这个结构相同
TMyInfo2=Pack record
  ID:Integer;
  年龄:Byte;
End;
就上面的内存布局 ID =1178944579($46454443) 年龄=71($47)岁
也可以和下面的内存布局一样
   Var Arr3:Array [0..5] Byte; Var Arr4:Array [0..5] Char;
Arr3 [0]=67;Arr3 [1]=68;Arr3 [2]=69;Arr3]=70;Arr3 [4]=71;
  Arr4[0]=C;Arr4[1]=D;Arr4[2]=E;Arr4[3]=F;Arr4[4]=G;
  
  我们时刻要明白我们定义的数据在内存中的真实布局情况是什么样子,我们对各种数据类型的转换,指针的移动就更明确了,我们可以将一个内存块的数据看成一个数组,也可以看成一个结构体,也可以看成是一个个的数字,在这些数据类型之间我们可以互相转换。也可以将一个结构体复制到一个数组中。ComyMemory 进行数据的复制是不管内存中的数据是什么业务意义的,只是将内存块进行复制罢了。  对内存操作,指针操作不理解的朋友都是没有明白这些操作,没有理解内存和数据类型的关系。理解了这些以后就可以更好的理解软件开发的过程,更好的解释很多的错误原因了。

解决方案 »

  1.   

    呵呵,推荐了。最近在写一个网络通信程序,以前也写过,但都没有好好复用起来。最近比较有时间,觉得应该将很多东西
    都归纳总结一下,实现成一些通用的类。
    ----------------------------------------------
    Delphi实现的环形缓冲区
    http://topic.csdn.net/u/20110904/11/9eb0ae68-b0d2-41a9-8b0b-e474ef392095.html
    源码和Demo下载
    http://download.csdn.net/source/3574470 
    ----------------------------------------------
        一个先进先出的的环形数组缓冲区在网络通信程序中应该是必须使用的。每个对于服务器来说,每个客户端就是一个连接,就会有数据进来,因为网络传输数据时,并不是发送了100字节,接收端的事件中收到的就是100字节,只能先收到70,然后又收到30。如果另一端在不停的发送,共发送了200,则可能先收到70,又收到20,最后又收到处110。所以在发送的时候,每个数据包必须应该有头,长度这两个描述,最好在数据的末尾加上CRC校验
      PMyPack=^TMyPack;
      TMyPack=packed Record
        Head:Word; //数据头 Word 类型 两字节 。我一般用 $AAFF 来标识
        PackLen:Integer; //数据包的长度Integer类型 四字节
        Data:Byte;//用来标误数据的头位置,方便使用。1节点
      end;比较我要发送 Str:=ABCDEFGH这向个字母,则应该申请 Sizeof(TMyPack)+Length(Str)=7+8=15字节。
    则要申请15字节的内存。前面6个字节用来放数据头,数据包长度,然后是8个节点的数据,最后是一个字
    节的CRC校验。var
      Pack:PMyPack;
      ArrLen:Integer;
      Arr:Array of Byte; 
      CRC:Byte;
      Str:String;
    begin
      Str:='ABCDEFGH';
      ArrLen:=Sizeof(TMyPack)+Length(Str);
      SetLength(Arr,ArrLen);
      Pack:=@Arr[0];
      Pack.Head:=$AAFF; //数据头
      Pack.PackLen:=ArrLen;数据长度
      CopyMemory(@Pack.Data,@Str[1],8); 用户数据复制到数组中,Pack.Data的做用方便获取用户数据的起始位置
      Arr[ArrLen-1]:=$52;  //计算校验
    //这些操作完成后数据在内存中的布局如下。  Socket.SendBuffer(Arr,ArrLen); //Socket 只能是发送指定的一整块数据。 Arr 就是分配的一整块。
      SetLength(Arr,0);
    end;   当然,你也可以不用将 Arr定义成数组,可以定义成指针,用GetMemory 来分配一块内存。定义成数组的好处是你可以在调试的时候很容易知道 数组 中的内容。现在假设发送端执行了两次 Socket.SendBuffer(Arr,ArrLen);
    数据的接收------------------------------------------------------
      在数据的接收端,要先分配好一大块内存做为缓冲区,比如先分配好 Arr:Array [0..1023] of Byte ;1024长度的数组.
       Socket 收到数据后,可能是收到了全部的数据,也可能是收到了一部分。不过对于接收来说不用管这些问题,
    接收只负责将这次收到的数据放在缓冲区中。发送端发了两个数据包,可能是多次接收,如下图所示.   需要实现一个数据读取的方法,检查缓冲区中是否将包收完整了,如果收完整了,就将数据转换成业务对象,次业务对象交给对应的业务处理对象,并且将缓冲区中对应的数据删除。
       检查包是否收完整了,通过数据头,数据的长度,和校验来进行,如果你的程序刚打开,可能先收的是一个不完整的数据,则通过数据头来确定一个包的开始位置。  数据的接收和保存可以在一个方法中完成,最好不要在接收到数据保存到缓冲区后就立刻检查,并且进行业务处理,
    这应该在三个线程中完成。
    1.一般主线程主要负责数据的接收和保存,并给数据处理线程发消息,有数据到了。
    2.数据处理线程只负责检查缓冲区中数据,读取创建业务对象,删除缓冲区。然后将业务对象也放到一个业务列表中。
      同时通知业务处理线程有业务数据到了。
    3.业务处理线程从业务列表中读取业务对象进行对应的操作,然后将这个业务对象从业务列表中删除。
      

  2.   

    上面的
    只能先收到70,然后又收到30。
    写错了,应该是  可能[color=#0000FF]先收到70,然后又收到30。[/color]
      

  3.   

    最近在看Delphi源码,里面有很多指针写的代码
      

  4.   

    Delphi...
    好久没有用了。。忘差不多了
      

  5.   

    http://baike.baidu.com/view/159417.htmhttp://www.ylmf.net/zhuanti/zt02/2010/1108/8783.html
      

  6.   

    问下LZ,如下部分:--------
    因为系统知道Word 是占用两个字节,所以在操作时候会将两个房间的数据来操作。如 @W返回的是 3 ,如<图三>
     
    <图三>
    则W的值是 17475 十六进制 $4443 。
    -----------------------
    W的值为什么不是十六进制 $4344呢?怎么知道是从高位开读还是从地位开始读的呢?
      

  7.   

    Word类型保存是高位在前,低位在后,只要理解成计算机当初设计就设计成这个样子就行了。其实当初设计成低位在后 高位在前 也可以,但是现在就是这个样子了。我们知道就OK了。至于当初设计的真正原因,那可能得翻翻计算机设计原理一类的底层的书了。
      

  8.   

      对内存,指针,线程这些的操作应用最典型集中常用的例子就是网络通信了。Socket收到的是数据流,至于数据的内容是什么根据双方的约定,比如都是以数据包的形式来保存一个类型或对象,数据包开头以定义的AAFF开始,然后是一个Integer来保存整个数据包的长度,然后是数据的内容,最后一位保存这个数据包的校验。解析的时候从收到的数据开始,先找 AAFF ,找后在在看数据的长度是多少,然后根据长度找到校验,这基本就可以认为是一个数据包了,然后在计算一下收到的数据的校验码看和数据包中保存的校验是否相同,如果相同就将这个数据包的交给对应的业务处理过程,否则就认为这个数据包是错误的,丢弃。
      一般大家都用 TServerSocket 来做为服务器,TClientSocket 来做为客户端。对于TServerSocket来说,可以接收很多个客户端连接,每个客户端都会收发数据。对于每个连接上来的客户端来说都是独立的,要为每个客户端来创建一个接收缓冲区,因为有可能第一次只收到数据包的一部分,而且第二又收到了剩余的部分和每二个数据包的一部分。所以要通过缓冲区来保存。
      缓冲区的大小可以先预设一个值,然后根据接收数据的大小和处理数据的速度来调整,如接收到100个字节,而这时缓冲区只有30个字节的位置了,那就将缓冲区扩大,我反正每次都将缓冲区扩大为原来长度的一倍在加上当前要接收的数据的长度。更安全点的话防止你的缓冲区无限的扩大,可以设置一个上限值,当达到这个上限时将缓冲区中前面旧的数据删除掉,一般来说如果缓冲区总是在扩大,说明你的数据处理太慢了。   如有100个客户端来连接了,那就是有100个缓冲区,为每个缓冲区设置一个临界区,每个缓冲区都是先进先出的环形缓冲区有Push ,Pop 两个方法,用临界区进行同步.
      然后在创建一个线程,就是循环这0到99个缓冲区,检查缓冲区中是否有完整的数据包,如果有就处理,没有就检查下一个。如果你有1000个连接,一个业务处理线程可能有些少,那可以设置一个值,比如说每个线程就只处理300个连接,那第一个线程就只循环0..299,第二个只循环300到599,第三个只循环其它的。
      通迅部分的效率优先级是最高的,不要阻塞,有些写法都是在 OnClientRead中去检查是否有完整的数据包,如果有话就直接调用了业务处理程序,并在界面上显示,这造成了通迅接收部分的执行时间太长。很容易出错和系统不稳定。就算是用了缓冲区,然后由另一个线程进行读缓冲区,也不要在线程中去处理复杂业务和直接去操作界面控件显示。因为你在缓冲区数据读取线程中执行太多的工作的话,如你在处理1号客户的数据,长时间的处理,这时在来了数据就不能及时读取,会造成缓冲区不停的扩大。
      如果有完整的数据包,这个数据包的内容要显示在界面上,先最好将这个数据包转化成具体的业务对象,然后将业务对象保存一个列表中,然后给窗体发一个Postmessage消息,窗体收到消息后在从列表中读这个业务对象。或在窗体上放一个TTimer,定时的去列表中检查,看是否有业务对象,如果有的话就处理。
      如果收到的数据包是要保存到数据库,那就提交给数据库管理对象。
      如果收到的数据包是要保存成文件,那就先创建一个临时文件,将数据直接写入,然后通过消息的方式让界面显示。
      总的来说:就是利用Postmessage,或TTimer在加上数据列表,将通信部分和业务部分完全的分离开。
      
      通信部分的代码最起码不要写在 TForm窗体中,最好写和 TDataModule中。界面只是用来展示和接收输入的,一定要将它各业务处理,底层通信这些分离开,不能耦合。