透过WinDBG的视角看String

摘要 : 最近在博客园里面看到有人在讨论 C# String的一些特性. 大部分情况下是从CODING的角度来讨论String. 本人觉得非常好奇, 在运行时态, String是如何与这些特性联系上的. 本文将侧重在通过WinDBG来观察String在进程内的布局, 以此来解释C# String的一些特性.

 

问题

C# String有两个比较有趣的特性.

  1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
  2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。

对应着两个特性, 我产生了一些疑问.

  • String的恒定性是怎么样让string进行比较的时候出现有趣的结果的? 它的比较结果为什么会与其他引用类型的结果不一样?
  • 什么样的String会被放到拘留池中?
  • 拘留池是怎样的数据结构? 它真是个Hashtable吗?
  • 驻留在拘留池内的String会不会被GC,  它的生命周期会有多长(什么时候才会被回收)?

 

String的恒定性

先看一下下面的例子 :

 

private static void Comparation()
{string a = "Test String";string b = "Test String";string c = a;Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));Console.WriteLine("a vs c : " + object.ReferenceEquals(a, c));SimpleObject smp1 = new SimpleObject(a);SimpleObject smp2 = new SimpleObject(a);Console.WriteLine("smp1 vs smp2 : " + object.ReferenceEquals(smp1, smp2));Console.ReadLine();}class SimpleObject
{public string name = string.Empty;public SimpleObject(string name){this.name = name;}
}


 

image

从结果上看, 虽然是不同的变量 a, b, c. 由于字符串的内容是相同的, 所以比较的结果也是完全相同的. 对比SimpleObject的实例, smp1和smp2的值虽然也是相同的,但是比较的结果为false.

下面看一下运行时, 这些objects的的情况.

在运行时态, 一切皆是地址. 判断两个变量是否是相同的对象, 直观的可以从它地址是否是相同的地址来进行判断.

用dso命令打印出栈上对应的Objects. 可以看到Test String”虽然出现了3次, 但是他们都对应了一个地址0000000002473f90 . SimpleObject的对象实例出现了2次, 而且地址不一样, 分别是00000000024776700000000002477688 .

所以, 在使用String的时候, 实质上是重用了相同的String 对象. 在new一个SimpleObject的实例时候, 每一次new都会在新的地址上初始化该对象的结构. 每次都是一个新的对象.

 

 

0:000> !dso
OS Thread Id: 0x3f0c (0)
RSP/REG          Object           Name
......000000000043e730 0000000002473f90 System.String
000000000043e738 0000000002473f90 System.String
000000000043e740 0000000002473f90 System.String
000000000043e748 0000000002477670 ConsoleApplication3.SimpleObject
000000000043e750 0000000002477688 ConsoleApplication3.SimpleObject
.......0:000> !do 0000000002473f90 
Name: System.String
MethodTable: 00007ffdb0817df0
EEClass: 00007ffdb041e560
Size: 48(0x30) bytes
GC Generation: 0
(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: Test String
Fields:MT            Field           Offset                 Type VT             Attr            Value Name
00007ffdb081f060  4000096        8         System.Int32  1 instance               12 m_arrayLength
00007ffdb081f060  4000097        c         System.Int32  1 instance               11 m_stringLength
00007ffdb0819838  4000098       10          System.Char  1 instance               54 m_firstChar
00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty>> Domain:Value  0000000000581880:0000000002471308 <<
00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars>> Domain:Value  0000000000581880:0000000002471be0 <<

 

当字符串内容发生改变的时候, 任何微小的变化都会重新创建出一个新的String对象. 在我们调用这段代码的时候

    Console.WriteLine("a vs b : " + object.ReferenceEquals(a, b));

CLR runtime实际上做了两件事情. 为字符"a vs b"分配了到了一个新的地址. 将对比结果与刚才的字符拼接到了一起, 分配到了另外一个新的地址. 如果多次拼接字符串, 就会分配到更多的新地址上, 从而可能会快速的占用大量的虚拟内存. 这就是为什么微软建议在这种情况下使用StringBuilder的原因.

 

0:000> !dsoListing objects from: 0000000000435000 to 0000000000440000 from thread: 0 [3f0c]Address          Method Table    Heap Gen      Size Type
…..
0000000002473fc0 00007ffdb0817df0   0  0         44 System.String a vs b : 
0000000002474138 00007ffdb0817df0   0  0         52 System.String a vs b : True…..

 

String的驻留

CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。 我们看一下如何来理解这句话.

下面是示例代码 :

 

static void Main(string[] args)
{int i = 0;while (true){SimpleString(i++);Console.WriteLine( i + " : Run GC.Collect()");GC.Collect();Console.ReadLine();}
}private static void SimpleString(int i)
{string s = "SimpleString method ";string c = "Concat String";Console.WriteLine(s + c);Console.WriteLine(s + i.ToString());Console.ReadLine();
}

 

这是第一次的执行结果. 此时只执行到了SimpleString里面, 还没有从这个方法返回.

image

我们可以看到stack上有4个string. 分别是按照代码逻辑拼接起来的string的内容. 从这里我们就可以当我们在拼接字符串的时候, 实际上会在Heap上创建出多个String的对象, 以此来完成这个拼接动作.

0:000> !dsoListing objects from: 0000000000386000 to 0000000000390000 from thread: 0 [3f50]…..
0000000002a93f70 00007ffdb0817df0   0  0         66 System.String SimpleString method 
0000000002a93fb8 00007ffdb0817df0   0  0         52 System.String Concat String
0000000002a93ff0 00007ffdb0817df0   0  0         92 System.String SimpleString method Concat String
0000000002a97a90 00007ffdb0817df0   0  0         28 System.String 0
0000000002a97ab0 00007ffdb0817df0   0  0         68 System.String SimpleString method 0……

 

随意用其中一个来检查它的引用情况.

从!gcroot的结果看, 这个string被两个地方引用到. 一个是当前的线程. 因为正在被当前线程使用到, 所以能够看到这个非常正常.

另外一个是root在一个System.Object[]数组上. 这个数组被PINNED在了App Domain 0000000000491880 上面. 这里显示出来, String其实是驻留在一个System.Object[]上面, 而不是很多人猜测的Hashtable. 不过料想CLR 应该有一套机制可以从这个数组中快速的获取正确的String. 不过这点不在本篇的讨论范围之内.

 

0:000> !gcroot 0000000002a93f70
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 81a0
RSP:b9e9b8:Root:0000000002a93f70(System.String)
Scan Thread 2 OSTHread 7370
DOMAIN(0000000000C51880):HANDLE(Pinned):217e8:Root:0000000012a93030(System.Object[])->
0000000002a93f70(System.String)

 

我们可以检查一下这个System.Object[]里面都有什么.

从这个数组里面可以看到代码中显示声明的的字符串. 第一个元素是一个空值, 这个里面保留的是我们最常用的String.Empty的实例. 第二个元素是”Run GC.Collect()”. 这个在code的里面的main函数中. 当前还没有被执行到, 但是已经被JITed到了该数组中. 其他两个被显示定义的字符串也能够在这个数组中被找到. 另外可以确认的是, 拼接出来的字符串, 临时生成的字符串都没有在这里出现. 然而, 通过拼接出来的String并不在这个数组里面. 虽然拼接出来的String同样分配到了heap上面, 但是不会被收纳到数组中.

0:000> !dumparray -details 0000000012a93030
Name: System.Object[]
MethodTable: 00007ffdb0805be0
EEClass: 00007ffdb041eb88
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Methodtable: 00007ffdb08176e0
[0] 0000000002a91308Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 26(0x1a) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:         Fields:MT    Field   Offset                 Type VT     Attr            Value Name00007ffdb081f060  4000096        8         System.Int32  1 instance                1 m_arrayLength00007ffdb081f060  4000097        c         System.Int32  1 instance                0 m_stringLength00007ffdb0819838  4000098       10          System.Char  1 instance                0 m_firstChar00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty>> Domain:Value  0000000000c51880:0000000002a91308 <<00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars>> Domain:Value  0000000000c51880:0000000002a91be0 <<
[1] 0000000002a93f30Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 64(0x40) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:      : Run GC.Collect()    Fields:MT    Field   Offset                 Type VT     Attr            Value Name00007ffdb081f060  4000096        8         System.Int32  1 instance               20 m_arrayLength00007ffdb081f060  4000097        c         System.Int32  1 instance               19 m_stringLength00007ffdb0819838  4000098       10          System.Char  1 instance               20 m_firstChar00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty>> Domain:Value  0000000000c51880:0000000002a91308 <<00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars>> Domain:Value  0000000000c51880:0000000002a91be0 <<
[2] 0000000002a93f70Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 66(0x42) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:     SimpleString method     Fields:MT    Field   Offset                 Type VT     Attr            Value Name00007ffdb081f060  4000096        8         System.Int32  1 instance               21 m_arrayLength00007ffdb081f060  4000097        c         System.Int32  1 instance               20 m_stringLength00007ffdb0819838  4000098       10          System.Char  1 instance               53 m_firstChar00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty>> Domain:Value  0000000000c51880:0000000002a91308 <<00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars>> Domain:Value  0000000000c51880:0000000002a91be0 <<
[3] 0000000002a93fb8Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 52(0x34) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:     Concat String    Fields:MT    Field   Offset                 Type VT     Attr            Value Name00007ffdb081f060  4000096        8         System.Int32  1 instance               14 m_arrayLength00007ffdb081f060  4000097        c         System.Int32  1 instance               13 m_stringLength00007ffdb0819838  4000098       10          System.Char  1 instance               43 m_firstChar00007ffdb0817df0  4000099       20        System.String  0   shared           static Empty>> Domain:Value  0000000000c51880:0000000002a91308 <<00007ffdb08196e8  400009a       28        System.Char[]  0   shared           static WhitespaceChars>> Domain:Value  0000000000c51880:0000000002a91be0 <<

 

继续让代码执行下去, 我们需要来几次GC. 验证一下驻留的字符串是否会在不使用之后被GC掉.

GC完成之后, 按照所设想的, CallStack上面的String都已经被清除掉了.同时因为已经做过了GC动作, GC heap进过了压缩, 没有被PINNED住的对象地址会发生改变. 所以要验证驻留的String是否会被回收, 可以从驻留数组下手. 由于该数组是被PINNED住, 所以即使发生了GC的动作, 它的地址也不会发生改变. 所以可以通过相同的命令把数组里面驻留的String都列出来.

结果是与我的预期是一致的. 只有被显示定义的String保留在该数组内, 而这些String不会被回收. 通过拼接零时生产的String, 则不会加入到这个数组内, 在GC发生后, 由于没有被引用而被回收掉.

 

0:000> !dumparray -details 0000000012a93030
Name: System.Object[]
MethodTable: 00007ffdb0805be0
EEClass: 00007ffdb041eb88
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Methodtable: 00007ffdb08176e0
[0] 0000000002a91308Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 26(0x1a) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:         
...
[1] 0000000002a93f30Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 64(0x40) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:      : Run GC.Collect()    …
[2] 0000000002a93f70Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 66(0x42) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:     SimpleString method     ...
[3] 0000000002a93fb8Name: System.StringMethodTable: 00007ffdb0817df0EEClass: 00007ffdb041e560Size: 52(0x34) bytes(C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)String:     Concat String 

 

 

所以经过上面的观察, 可以得出的结论是驻留的String生命周期非常长. 那么, 在什么时候他才会被回收?

从上面gcroot的结果, 可以看到主流数组是被PINNED住. 而引用这个数组的App Domain 0000000000C51880.

用!dumpdomain -stat的命令将所有的app domain信息打印出来. 可以看到这个App Domain是我们代码运行的Domain (ConsoleApplication3.exe). 这个驻留数组是由CLR 来维护, 并且与当前的App Domain联系到一起. 所以, 理论上这些驻留数组的生命周期跟这个App Domain是一致的.

 

0:000> !dumpdomain -stat
--------------------------------------
System Domain: 00007ffdb1f16f60
LowFrequencyHeap: 00007ffdb1f16fa8
HighFrequencyHeap: 00007ffdb1f17038
StubHeap: 00007ffdb1f170c8
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 00007ffdb1f17860
LowFrequencyHeap: 00007ffdb1f178a8
HighFrequencyHeap: 00007ffdb1f17938
StubHeap: 00007ffdb1f179c8
Stage: OPEN
Name: None
Assembly: 000000000047fa60
--------------------------------------
Domain 1: 0000000000491880
LowFrequencyHeap: 00000000004918c8
HighFrequencyHeap: 0000000000491958
StubHeap: 00000000004919e8
Stage: OPEN
SecurityDescriptor: 0000000000494140
Name: ConsoleApplication3.exe
Assembly: 000000000047fa60 [C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 000000000047f820
SecurityDescriptor: 000000000047f9a0Module Name
00007ffdb03e1000 C:\windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll

 

写在最后面

  1. String的恒定性. 字符串横定性是指一个字符串一经创建,就不可改变。那么也就是说当我们改变string值的时候,便会在托管堆上重新分配一块新的内存空间,而不会影响到原有的内存地址上所存储的值。
  2. String的驻留. CLR runtime通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统(App Domain)中只有一个。
    直接在CODE里面声明的String会被CLR runtime维护在一个Object[]内.
    临时生成的string或者拼接出来的String不会维护在这个驻留数组中.
    驻留数组的生命周期跟它位于的App Domain一样长. 所以GC并不会影响驻留数组所引用的String, 它们不会被GC.

可以参考下面这个链接来对这两个特性加深理解.

http://blog.csdn.net/fengshi_sh/article/details/14837445

http://www.cnblogs.com/charles2008/archive/2009/04/12/1434115.html

http://www.cnblogs.com/instance/archive/2011/05/24/2056091.html

转载于:https://www.cnblogs.com/developersupport/p/4212102.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/375372.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ueditor富文本编辑器 修改框宽度和高度的方法

在使用ueditor的时候&#xff0c;用的textarea <textarea name"content" id"myEditor">这里写这条规则的回复内容</textarea> 给它加style"width:300" 属性的时候&#xff0c;发现不起作用。 正确的方法应该是&#xff1a; <scri…

Win32ASM学习[9]: 标志寄存器

TF(Trap Flag)——位8&#xff0c;跟踪标志。置1 则开启单步执行调试模式&#xff0c;置0 则关闭。在单步执行模式下&#xff0c;处理器在每条指令后产生一个调试异常&#xff0c;这样在每条指令执行后都可以查看执行程序的状态。如果程序用POPF、POPFD 或者ET 指令设置TF 标志…

JavaScript消息框

1.警告框 function myTest(){alert("这里的内容会弹出");} 2.确认框 其返回的值是 true 或 false 。 function myTest(){confirm("这里的内容会弹出");} 3.提示框 prompt prompt(参数1&#xff0c;参数2)&#xff1a;其参数1 是显示提示要输入的信息&…

.Net 事务

在分布式应用程序中&#xff0c;不可避免地会经常使用到事务控制。事务有一个开头和一个结尾&#xff0c;它们指定了事务的边界&#xff0c;事务在其边界之内可以跨越进程和计算机。事务边界内的所有资源都参与同一个事务。要维护事务边界内资源间的一致性&#xff0c;事务必须…

Android WifiDisplay分析一:相关Service的启动

网址&#xff1a;http://www.2cto.com/kf/201404/290996.html 最近在学习Android 4.4上面的WifiDisplay(Miracast)相关的模块&#xff0c;这里先从WifiDisplay用到的各个Service讲起&#xff0c;然后再从WifiDisplaySettings里面讲解打开wfd的流程。首先看下面的主要几个Servic…

Cortex-A15 Memory Hierarchy

ARM 平台为实现速度和成本的平衡&#xff0c;使用多个层次的内存架构。对于多核 CPU 组成的 SOC&#xff0c;每个CPU 内部都有一组高速缓存&#xff0c;包含&#xff1a;ICache、DCache 和 TLB。多个 CPU 共享一个更大的 L2 缓存。L2缓存再和 CPU 外部的DDR3 内存交互。ICache …

Android 图片的缩略图

<1>简介 之前往往是通过Bitmap、Drawable和Canvas配合完成&#xff0c;需要写一系列繁杂的逻辑去缩小原有图片&#xff0c;从而得到缩略图。 现在我给大家介绍一种比较简单的方法&#xff1a;&#xff08;网上有&#xff09; 在Android 2.2版本中&#xff0c;新增了一个T…

《JavaScript权威指南》学习笔记 第二天 下好一盘大棋

前段学习js的时候总是零零散散的&#xff0c;以至于很多东西都模棱两可。时间稍微一久&#xff0c;就容易忘记。最主要的原因是这些东西&#xff0c;原来学的时候就不是太懂&#xff0c;以至于和其他知识无法形成记忆链&#xff0c;所以孤零零的知识特别容易忘记。重温犀牛书&a…

SUID或SGID程序中能不能用system函数

system()函数的声明和说明如下&#xff1a; 注意它的描述那里&#xff0c;system()执行一个由command参数定义的命令&#xff0c;通过调用/bin/sh -c命令来实现这个功能。也就是说它的逻辑是这样的&#xff01; 进程调用system函数&#xff0c;system函数调用fork创建一个子进程…

Xamarin iOS编写第一个应用程序创建工程

Xamarin iOS编写第一个应用程序创建工程 在Xcode以及Xamarin安装好后&#xff0c;就可以在Xamarin Studio中编写程序了。本节将主要讲解在Xamarin Studio中如何进行工程的创建以及编写代码等内容XamariniOS编写第一个应用程序创建工程本文选自Xamarin iOS开发实战大学霸。 1.3.…

Birt使用总结

把report放到其他服务器要重新建立Data Source ,这是配置&#xff0c;拷贝项目时不会同时拷贝 (1)在EXTJs中利用Report实现报表的刷新 Ext.getCmp("showview").body.update("<iframe idshowviewframe src" "> </iframe>"…

【线性代数公开课MIT Linear Algebra】 第二十三课 微分方程与exp(At)

本系列笔记为方便日后自己查阅而写&#xff0c;更多的是个人见解&#xff0c;也算一种学习的复习与总结&#xff0c;望善始善终吧~ 一阶常系数微分方程 Aududt 将一阶常系数微分方程转换为线性代数问题的关键在于常系数微分方程的解一定是指数形式的。那么我们的需要求解的东西…

iOS学习之基本概念

学习iOS最重要的是态度和兴趣&#xff0c;如果你对于学习始终抱有不断的热情和端正的态度&#xff0c;那么&#xff0c;无论是什么&#xff0c;你总会成功的&#xff01; 有一句话与大家共勉&#xff1a;过程中跌倒多少次都没有关系&#xff0c;重要的是&#xff0c;跌倒后你能…

【转】gvim配置及相关插件安装

0.准备软件及插件。(a)gvim72.exe 地址ftp://ftp.vim.org/pub/vim/pc/gvim72.exe。(b)vimcdoc-1.7.0-setup.exe 地址http://prdownloads.sourceforge.net/vimcdoc/vimcdoc-1.7.0-setup.exe?download(c)ec57w32.zip 地址http://prdownloads.sourceforge.net/ctags/ec57w32.zip(…

Android 高级编程 RecyclerView 控件的使用

RecyclerView 是Android 新添加的一个用来取代ListView的控件&#xff0c;它的灵活性与可替代性比listview更好。 看一下继承关系&#xff1a; ava.lang.Object ↳android.view.View ↳android.view.ViewGroup ↳android.support.v7.widget.RecyclerViewKnown Direct …

jquery判断一个div的边界是否超出另外一个div的边界

摘要&#xff1a;本文简单介绍jquery判断一个div的边界是否超出另外一个div的边界&#xff0c;如果超出边界做出相应的处理。 1、实现效果 判断前 判断后 2、实现思路 实现类似的判断&#xff0c;主要是获取两个div在浏览器中的上下左右的四至&#xff0c;在jquery中&#xff0…

maven环境快速搭建(转)

最近&#xff0c;开发中要用到maven&#xff0c;所以对maven进行了简单的学习。因为有个maven高手在身边&#xff0c;所以&#xff0c;很快就上手了&#xff0c;我这里算是自我总结吧。关于maven是什么东东&#xff0c;请参考其它文章。 ----------------准备工作-------------…

cocos2d-x3.0 相对布局(一)

2dx相对布局和Android非常类似。假设前完成Android它应该是easy入门。Size widgetSize Director::getInstance()->getWinSize();Text* alert Text::create("Layout", "fonts/Marker Felt.ttf", 30 );alert->setColor(Color3B(159, 168, 176));aler…

夺命雷公狗---ECSHOP---08---商品页的拇改成星星

<strong>用户评价&#xff1a;</strong>{*---------商品评价星星开始----------*}<img src"./images/stars{$goods.comment_rank}.gif" alt"comment rank {$goods.comment_rank}">{*---------商品评价星星结束-------*} 这里主要是要有星…

文件指针

一.移动文件指针 SetFilePointer,hFile,lDistanceToMove,lpDistanceToMoveHigh,dwMoveMethod dwMoveMethod 指明移动的模式 FILE_BEGIN 不管文件处于什么地方,总是从文件的头部开始移动,这时的位置参数相当于指定了一个绝对位置 FILE_CURRENT 从当前的文件指针处开始移…