字符串在内存中的的几种风格
字符串作为参数和返回值
参考
字符串在内存中的几种风格
所谓的风格,也就是字符串在内存中的存在形式。如何存放的,占据内存的大小,还有存放顺序等。在不同的编程语言和不同的平台上字符串风格一般不相同。
1、.net中字符串的风格
.net中的托管代码:
1 string strin = "in string"; 2 Console.WriteLine(strin);//断点下在这里 3 Console.Read();
调试时查找字符串strin的地址发现内存中的情况:
1 0x01363360 b8 97 0f 79 0a 00 00 00 09 00 00 00 69 00 6e 00 ...y........i.n. 2 0x01363370 20 00 73 00 74 00 72 00 69 00 6e 00 67 00 00 00 .s.t.r.i.n.g...
我的一个疑问,这是.net中的字符串存放风格吗?
2、C风格
托管代码:
1 [DllImport(@"C:\Documents and Settings\Administrator\桌面\pInvoke\CPPDLL\Debug\CPPDLL.dll")] 2 private static extern void Putstring(string s); 3 static void Main(string[] args) 4 { 5 string strin = "in string"; 6 Console.WriteLine(strin); 7 Putstring(strin);//调用非托管函数 8 Console.Read(); 9 }
非托管函数:
1 extern "C" __declspec(dllexport) void Putstring(char *str) 2 { 3 printf("%s\n",str);//断点 4 }
断点外,在内存中可以看到str的情况:
1 0x0012F2EC 69 6e 20 73 74 72 69 6e 67 00 00 00 01 00 00 00 in string.......
这就是C风格在内存中保存字符串,字符串以00结束,字符串的长度比字符个数多1,也就是后面的'\0'.C风格字符串的表示是以0结束的ASCII或Unicode字符数组。
3、Visual Basic 和 Java风格
1 0x0012F2EC 12 00 00 00 69 00 6e 00 20 00 73 00 74 00 72 00 ....i.n. .s.t.r. 2 0x0012F2FC 69 00 6e 00 67 00 00 00 22 00 00 00 00 00 00 00 i.n.g...".......
封送字符串
在封送字符串时,需要考虑字符的宽度、风格、是传入还是传出以及内存释放的问题。
字符串作为传入参数的情况:
非托管函数:
1 //字符串作为传入参数的情况 2 extern "C" __declspec(dllexport) void Putstring(char* str) 3 { 4 printf("%s\n",str); 5 }
一个简单功能:把字符串在控制台打印出来。
托管代码:
1 [DllImport(@"C:\Documents and Settings\Administrator\桌面\pInvoke\CPPDLL\Debug\CPPDLL.dll")] 2 private static extern void Putstring(string s); 3 static void Main(string[] args) 4 { 5 string strin = "in string"; 6 Putstring(strin); 7 Console.Read(); 8 }
由于CLR会自动采用默认的方式来进行封送,相当于下面的显式操作:
1 private static extern void Putstring([In] [MarshalAs(UnmanagedType.AnsiBstr)]string s); 2 //默认是输入并以(AnsiBstr)平台相关的字符串指针进行封送
疑问1:不知道为什么不是和MSDN上指定的默认处理一样(LPTStr)
字符串被作为对平台调用进行的调用的方法参数封送时的封送处理选项如下表:
执行过程如下:第一步,string strin = "in string";在托管内存分配内存,保存字符串"in string",在内存中的位置和分布如下:
0x01DEBF60 48 0d 87 61 0a 00 00 00 09 00 00 00 69 00 6e 00 H.?a........i.n.
0x01DEBF70 20 00 73 00 74 00 72 00 69 00 6e 00 67 00 00 00 .s.t.r.i.n.g...
可以看到在内存中的风格和上面说的是一个模型的。48 0d 87 61 不清楚这是什么意思,其他字符串的开始也是这个数据,0a 00 00 00 在其他字符串中也是如此,第三个DWORD 09 00 00 00表示字符的个数。这哪是一个字符串呀,这分明是一个对象在内存中的形式吧!
第二步:Putstring(strin);从这里进入非托管函数中。执行到函数中printf("%s\n",str);,这时可以看到str表示一个内存指针,指向的内存0x01debf6c保存了传递的字符串,这个过程会在非托管内存分配一个地址,并在这个地址写入处理后的字符串,这个地方的处理是按照非托管函数参数类型进行的,转变成ANSI 的C风格样式。如下:
69 6e 20 73 74 72 69 6e 67 00 00 00 00 00 00 00 in string.......
第三步:Console.Read();从托管代码到非托管代码。这里会有一个关键的动作,就是非托管地址0x01debf6c处的内存会被释放掉,原来保存字符的地方会变的面目全非。
在.net中,字符都是占据两个字节,也就是宽的,在这里非托管函数中使用是的窄字符,ANSI,封送处理器会进行默认的转变。如果非托管函数使用参数是wchar_t *类型,会出现什么情况呢?
非托管函数签名变化如下:
1 void Putstring(wchar_t * str)
在即时窗口中看到的情况:
str
0x002ff04c "湩猠牴湩g"
其原因是当窄字符传递的,复制到非托管内存中每个字符是紧密排列的,但是函数wprintf当宽的处理"in"的ASC码当一个汉字来处理了,,结果什么也没有输出到控制台。这次的输出只是一个特例,有一些情况是输出认不出的字符.如果把函数在托管代码中描述修改为如下,就能正常输出字符串了。并且内存也释放。
1 private static extern void Putstring([MarshalAs(UnmanagedType.BStr)] string s);//UnmanagedType.BStr
封送处理器对输入型的字符串的默认封送处理是UnmanagedType.AnsiBStr,在执行过程中会在非托管内存先分配一块内存,把字符串复制到这个内存中,进行适当的处理,最后从非托管内存返回后会自动释放掉这个内存区域,并且没有把字符串从非托管内存复制回托管内存,这就是以值的方式传递引用。其实,这个内存是使用CoTaskMemAlloc函数分配的,封送处理器会在最后返回进调用CoTaskMemFree函数来释放掉占用的内存,不然的话封送处理器没有能力来释放这块内存。
为了证明这种猜想,把非托管函数修改如下:
其他分配内存的方法有malloc,new等方式。但是封送处理器没有能力自动释放使用这些方式在非托管内存中的分配的内存。
//字符串作为传入参数的情况
extern "C" __declspec(dllexport) void Putstring(char * str)
{
char *pNew = (char *)malloc(10);//修改
printf(str);
}
最终会发现,pNew指向的内存不会被释放掉,并且在托管代码中没有能力释放,唯一的方法就是再调用一个非托管方法专门释放这个内存。如果是使用CoTaskMemAlloc来申请内存的话,可以在托管代码中使用Marshal.FreeCoTaskMem方法释放此内存。
字符串作为传出参数:
为了达到一个目标,把字符串传递到非托管函数,非托管函数对字符串处理后能够把结果反映到托管代码中并且不以返回值的方式完成这个目标,这就需要把字符串作为传出参数。在默认情况下,对字符串的操作是作为传入[In]处理的,并且是通过复制字符串到一个非托管内存中,非托管函数对字符串的操作实际是对非托管内存中的那个字符串进行的。那怎么来完成修改,使对字符串的修改能反映到托管代码中呢?
1、使用stringbuilder。有许多地方说是向非托管函数传递的是stringbuilder的缓冲区,我也就理解为是指向字符串的指针,认为非托管代码和托管代码操作的是同一块内存区域,所以最终非托管代码操作的结果也反映到了托管代码中。但是在内存中的情况并不是我想的那样。
在使用stringbuilder作为参数时,默认使用的方向属性是[In,Out],并且有一些严格的要求才会做到“非托管函数的修改能反映到托管代码中”。
第一个要求就是显式地确定封送类型为CharSet=CharSet.Unicode。否则的话,不会完整的复制。
第二个要求就是非托管函数参数是Unicode。
整个内存操作的过程还是和上面说的一样,来回的复制字符串到内存。不论过程如何,结果还是可以达到目标的。
2、使用IntPtr。
1 private static extern void Putstring(IntPtr ps); 2 static void Main(string[] args) 3 { 4 IntPtr ipstr = Marshal.StringToHGlobalAnsi("123456"); 5 Putstring(ipstr); 6 Console.Read(); 7 }
非托管函数:
1 //字符串作为传入参数的情况 2 extern "C" __declspec(dllexport) void Putstring(char * str) 3 { 4 printf(str); 5 StrCpy(str,"abc"); 6 }
通过跟踪可以看到,传递的的确是一个地址,是一个非托管地址,无论在托管代码还是非托管函数中,这个地址始终是0x00657ee0。所做的修改也都在这里。唯一感觉不太好的是这块内存CLR不能主动释放掉。
3、显式以Unicode方式封送
在托管代码中声明如下:
1 [DllImport(@"C:\Users\Administrator\Desktop\pInvoke\CPPDLL\Debug\CPPDLL.dll")] 2 //[return: MarshalAs(UnmanagedType.LPWStr)] 3 private static extern void Instring([MarshalAs(UnmanagedType.LPWStr)] string refstr); 4 static void Main(string[] args) 5 { 6 string refstr = "321"; 7 Instring( refstr); 8 }
非托管函数:
1 extern "C" __declspec(dllexport) void Instring(wchar_t *pStr) 2 { 3 wcscpy(pStr,L"abc"); 4 }
主要的问题在于使用IntPtr的方法,是一个值得考虑的问题
这时封送处理器会传递到非托管函数refstr="321"的地址,也就是pStr指向"321",这里对pStr任何修改都能反映到refstr中。这样真是
完美实现了作为传出参数的传递。这就是传说中的固定,以前做的很多情况都是复制数据。但这是有要求的:
第一,托管代码必须调用本机代码,而不是本机代码调用托管代码。第二,该类型必须可直接复制或者必须可以在某些情况下变得可直接复制。第三,您不是通过引用传递(使用 out 或 ref)。第四,调用方和被调用方位于同一线程上下文或单元中。
可直接复制类型是指在托管和非托管内存中具有共同表示方法的类型。CLR中字符串是Unicode的,这时也是以[MarshalAsAttribute(UnmanagedType.LPWSTR)]指定显式封送,就变成从托管代码到非托管代码可复制了。
返回值是字符串:
非托管函数:
1 extern "C" __declspec(dllexport) char* Outstring() 2 { 3 char *pStr = (char*)malloc(8); 4 StrCpy(pStr,"abc"); 5 return pStr; 6 }
托管代码:
1 private static extern int Outstring(); 2 static void Main(string[] args) 3 { 4 int i = Outstring(); 5 Console.Read(); 6 }
最终i接收的是一个DWORD值,是一个地址,指向非托管内存中的字符串"abc"。如果使用IntPtr接收,最后再Marshal.PtrToStringAnsi转换能得到正常结果。需要注意的是如果使用string类型进行接收的话,会出现异常提示访问不可访问的内存。
参考
http://www.codeproject.com/Articles/66243/Marshaling-with-Csharp-Chapter-3-Marshaling-Compou.aspx
复制和锁定内存管理、方向属性等
《平台互操作》