非安全代码编程全解析
1. 指针作为参数和返回值
指针是一种特殊的变量,可在多数情况下当作普通变量使用,尤其能作为参数或返回类型。但要注意,返回指针时,其生命周期需至少和目标对象一样长,否则会出现指针无效的情况。比如,不能返回函数内局部变量的指针,因为局部变量的作用域仅限于该函数,函数结束后,指针就会失效。
1.1 指针操作符
| 符号 | 描述 |
|---|---|
| 指针加法 (+) | 将指针类型的大小加到内存地址上 |
| 指针减法 (-) | 从指针中减去指针类型的大小 |
| 指针递增 (++) | 使指针按指针类型的大小递增 |
| 指针递减 (–) | 使指针按指针类型的大小递减 |
| 关系符号 (< > >= <= != ==) | 用于比较指针,比较的是内存地址,而非指针类型 |
1.2 代码示例
using System;
namespace Donis.CSharpBook{
public class Starter{
public unsafe static void Main() {
int val=5;
int *pA=&val;
int *pB;
pB=MethodA(pA);
Console.WriteLine("*pA = {0} | *pB = {0}",
*pA, *pB);
}
public unsafe static int *MethodA(int *pArg) {
*pArg+=15;
return pArg;
}
}
}
在上述代码中,
MethodA
接收一个指针作为参数,对该指针指向的值进行修改后返回该指针。调用
MethodA
后,
pA
和
pB
指向同一内存地址,所以输出的
*pA
和
*pB
值相同。
1.3 ref 和 out 关键字对指针参数的影响
在没有
ref
或
out
关键字时,内存地址通过指针传递,而指针本身通过值传递到栈上。在函数中,可对指针进行间接引用并修改内存地址上的值,这些修改在函数结束后仍然保留。但对指针本身的修改在函数执行结束后会被撤销。使用
ref
或
out
关键字时,指针参数通过引用传递,在函数中可直接修改指针,这些修改在函数执行结束后仍然有效。
using System;
namespace Donis.CSharpBook{
public class Starter{
public unsafe static void Main() {
int val=5;
int *pA=&val;
Console.WriteLine("Original: {0}", (int) pA);
MethodA(pA);
Console.WriteLine("MethodA: {0}", (int) pA);
MethodB(ref pA);
Console.WriteLine("MethodB: {0}", (int) pA);
}
public unsafe static void MethodA(int *pArg) {
++pArg;
}
public unsafe static void MethodB(ref int *pArg) {
++pArg;
}
}
}
在这个代码示例中,
MethodA
通过值传递指针,对指针的修改在函数结束后被撤销;而
MethodB
通过引用传递指针,对指针的修改在函数结束后仍然保留。
2. fixed 关键字
在 C# 中,引用类型是可移动的,不能直接转换为指针。例如,下面的代码是错误的:
int [] numbers={1,2,3,4,5,6};
int *pI=numbers;
这是因为
numbers
是数组,属于引用类型,无法直接转换为指针。而结构体是值类型,存储在栈上,不受垃圾回收器控制,其值有固定地址,可轻松转换为指针。
为了解决引用类型转换指针的问题,可使用
fixed
关键字,它能至少暂时固定可移动类型的内存位置。不过,长时间固定内存会降低垃圾回收的效率。
2.1 使用 fixed 关键字修正代码
int [] numbers={1,2,3,4,5,6};
fixed(int *pI=numbers) {
// 在这里执行操作
}
fixed
关键字在代码块内固定内存,在该代码块中,内存不会被移动,也不会被垃圾回收器操作。可通过
fixed
语句中声明或初始化的指针访问该内存,此指针为只读。代码块结束后,内存不再固定。
2.2 多个指针声明示例
int [] n1={1,2,3,4};
int [] n2={5,6,7,8};
int [] n3={9,10,11,12};
fixed(int *p1=n1, p2=n2, p3=n3) {
}
在这个示例中,可在
fixed
语句中声明多个指针,用分号分隔,仅第一个指针需加星号。
2.3 完整使用示例
using System;
namespace Donis.CSharpBook{
public class Starter{
private static int [] numbers={5,10,15,20,25,30};
public unsafe static void Main(){
int count=0;
Console.WriteLine("Valeur du pointeur\n");
fixed(int *pI=numbers) {
foreach(int a in numbers){
Console.WriteLine("{0} : {1}",
(int)(pI+count), *((int*)pI+count));
++count;
}
}
}
}
}
此代码展示了
fixed
关键字的完整使用,通过指针输出数组元素的地址和值。
2.4 固定类对象示例
public class Starter{
public unsafe static void Main() {
ZClass obj=new ZClass();
fixed(int *pA=&obj.fielda) {
}
}
}
public class ZClass {
public int fielda=5;
}
在这个示例中,使用
fixed
关键字固定
ZClass
对象的内存,获取其整型成员的指针。
3. stackalloc 命令
stackalloc
命令用于在栈上动态分配内存,而非在堆上。其分配的内存生命周期与当前函数相同,且必须在非安全上下文中执行,只能用于初始化局部变量。CLR 会检测该命令导致的缓冲区溢出。
3.1 stackalloc 语法
type * stackalloc type[expression]
stackalloc
返回非托管类型,表达式的计算结果应为整数值,表示要分配的元素数量,返回内存分配的基指针。该内存是固定的,不会被垃圾回收器释放,函数结束时会自动释放。
3.2 代码示例
using System;
namespace Donis.CSharpBook{
public unsafe class Starter{
public static void Main(){
char* pChar=stackalloc char[26];
char* _pChar=pChar;
for(int count=0;count<26;++count) {
(*_pChar)=(char)(((int)('A'))+count);
++_pChar;
}
for(int count=0;count<26;++count) {
Console.Write(pChar[count]);
}
}
}
}
这段代码使用
stackalloc
在栈上分配 26 个字符的内存,然后将字母表字符赋值给每个元素并输出。
4. 平台调用(PInvoke)
可在托管代码中使用平台调用(PInvoke)调用非托管函数。由于托管内存和非托管内存的组织方式可能不同,可能需要对参数和返回类型进行封送处理。在 .NET 环境中,这项工作由互操作性封送器负责。
4.1 互操作性封送器
互操作性封送器负责在托管内存和非托管内存之间传输数据。它会自动传输在两种环境中具有相同表示形式的数据,如整数。这些在两种环境中相同的类型称为可直接复制到本机的类型( blittable types)。而像字符串这样的非可直接复制到本机的类型(non - blittable types),没有等效的非托管类型,需要封送器进行转换。封送器会为许多非可直接复制到本机的类型分配默认的非托管类型,开发者也可使用
MarshalAsAttribute
显式地将非可直接复制到本机的类型封送为特定的非托管类型。
4.2 DllImport 属性
DllImportAttribute
为调用非托管 DLL 中导出的函数提供必要信息,必须提供包含入口点的 DLL 名称。该属性属于
System.Runtime.InteropServices
命名空间,有多个选项用于配置托管环境以导入函数。库会在运行时动态加载,函数指针也会在运行时初始化。由于该属性在运行时评估,大多数配置错误在编译时不会显示,而是在运行时出现。
4.3 DllImportAttribute 语法
[DllImport(options)] accessibilité static extern typeretour nomfonction(paramètres)
选项用于配置导入,库名是唯一必需的选项,若库不在环境变量
path
中,需提供完整路径。访问修饰符表示函数的可见性,如公共或受保护。导入的函数必须是静态和外部的,其余部分构成函数的托管签名。
4.4 代码示例
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main(){
IntPtr hDC=API.GetDC(IntPtr.Zero);
int v=API.GetDeviceCaps(hDC, API.HORZRES);
Console.WriteLine("Taille verticale de la fenêtre {0}mm.", v);
int h=API.GetDeviceCaps(hDC, API.HORZRES);
Console.WriteLine("Taille horizontale de la fenêtre {0}mm.", h);
int resp=API.ReleaseDC(IntPtr.Zero, hDC);
if(resp!=1) {
Console.WriteLine("Erreur à la libération de hdc");
}
}
}
public static class API {
[DllImport("user32.dll")] public static extern
IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")] public static extern
int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]public static extern
int GetDeviceCaps(IntPtr hDC, int nIndex);
public const int HORZSIZE=4; // Taille horizontale en pixels
public const int VERTSIZE=6; // Taille verticale en pixels
public const int HORZRES=8; // Taille horizontale en millimètres
public const int VERTRES=10; // Taille verticale en millimètres
}
}
此代码导入三个 Win32 API 函数,用于获取屏幕的垂直和水平尺寸。
4.5 DllImportAttribute 其他选项
- EntryPoint :显式指定导入函数的名称,当导入名称不明确时,该选项必不可少。
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
string caption="Visual C# 2005";
string text="Hello, world!";
API.ShowMessage(0, text, caption, 0);
}
}
public class API {
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int ShowMessage(int hWnd,
string text, string caption, uint type);
}
}
在这个示例中,导入
MessageBox
函数,但托管函数名为
ShowMessage
。
-
CallingConvention
:定义函数的调用约定,默认是
Winapi
,对应 Win32 环境的标准调用约定。
| 成员 | 描述 |
| — | — |
| Cdecl | 调用者从栈中移除参数,适用于可变参数列表的函数 |
| FastCall | 此调用约定不受支持 |
| StdCall | 被调用方法从栈中移除参数,常用于 API,是平台调用非托管函数的默认约定 |
| ThisCall | 函数的第一个参数是
this
指针,后跟常规参数,该指针缓存在
ECX
寄存器中,用于访问非托管类实例的成员 |
| WinApi | 当前平台的默认调用约定,Win32 环境下是
StdCall
,Windows CE .NET 下是
Cdecl
|
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
int val1=5, val2=10;
API.printf("%d+%d=%d", val1, val2, val1+val2);
}
}
public class API {
[DllImport("msvcrt.dll", CharSet=CharSet.Ansi,
CallingConvention=CallingConvention.Cdecl)]
public static extern int printf(string formatspecifier,
int lhs, int rhs, int total);
}
}
此代码导入
printf
函数,使用
Cdecl
调用约定。
-
ExactSpelling
:指定符号根据函数的确切拼写解析。许多 Win32 API 函数名实际上是宏,对应真正的 API 方法,可能有后缀
A
或
W
。
ExactSpelling
默认值为
false
。
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
int hProcess=API.GetModuleHandleW(null);
}
}
public class API {
[DllImport("kernel32.dll", ExactSpelling=true)]
public static extern int GetModuleHandleW(string filename);
}
}
此代码导入
GetModuleHandleW
函数,设置
ExactSpelling
为
true
以避免名称替换。
-
PreserveSig
:在符号解析时保留方法的签名。COM 函数通常返回
HRESULT
结构,对应调用结果代码,实际返回值是
[out, retval]
属性关联的参数。在托管代码中,
HRESULT
被消耗,
[out,retval]
参数对应返回值。对于 COM 函数,托管签名不能保留,需映射到 COM 签名;非 COM 函数的签名应保留,默认值为
true
。
public class API {
[DllImport("ole32.dll", PreserveSig=false)]
public static extern int SomeFunction();
}
此示例中,
PreserveSig
为
false
,签名不保留。
-
SetLastError
:要求 CLR 缓存命名的 Win32 API 的错误代码。大多数 Win32 API 函数失败时返回
false
,开发者可调用
GetLastError
获取完整错误代码,在托管代码中使用
Marshal.GetLastWin32Error
。默认值为
false
。
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
bool resp=API.CreateDirectory(@"c*:\file.txt",
IntPtr.Zero);
if(resp==false) {
StringBuilder message;
int errorcode=Marshal.GetLastWin32Error();
API.FormatMessage(
API.FORMAT_MESSAGE_ALLOCATE_BUFFER |
API.FORMAT_MESSAGE_FROM_SYSTEM |
API.FORMAT_MESSAGE_IGNORE_INSERTS,
IntPtr.Zero, errorcode,
0, out message, 0, IntPtr.Zero);
Console.WriteLine(message);
}
}
}
public class API {
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool CreateDirectory(
string lpPathName, IntPtr lpSecurityAttributes);
[DllImport("kernel32.dll", SetLastError=false)]
public static extern System.Int32 FormatMessage(
System.Int32 dwFlags,
IntPtr lpSource,
System.Int32 dwMessageId,
System.Int32 dwLanguageId,
out StringBuilder lpBuffer,
System.Int32 nSize,
IntPtr va_list);
public const int FORMAT_MESSAGE_ALLOCATE_BUFFER=256;
public const int FORMAT_MESSAGE_IGNORE_INSERTS=512;
public const int FORMAT_MESSAGE_FROM_STRING=1024;
public const int FORMAT_MESSAGE_FROM_HMODULE=2048;
public const int FORMAT_MESSAGE_FROM_SYSTEM=4096;
public const int FORMAT_MESSAGE_ARGUMENT_ARRAY=8192;
public const int FORMAT_MESSAGE_MAX_WIDTH_MASK=255;
}
}
此代码导入
CreateDirectory
和
FormatMessage
函数,设置
CreateDirectory
的
SetLastError
为
true
,捕获并处理错误代码。
-
CharSet
:指示在非托管内存中正确解释字符串,可能影响
ExactSpelling
选项。它是一个枚举,有三个成员,默认值是
CharSet.Ansi
。
| 值 | 描述 |
| — | — |
| CharSet.Ansi | 字符串应封送为 ANSI 格式 |
| CharSet.Unicode | 字符串应封送为 Unicode 格式 |
| CharSet.Auto | 根据当前平台在运行时选择适当的转换 |
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
int hProcess=API.GetModuleHandle(null);
}
}
public class API {
[DllImport("kernel32.dll", CharSet=CharSet.Ansi)]
public static extern int GetModuleHandle(string filename);
}
}
此代码将字符串封送为 ANSI 格式。
-
BestFitMapping
:在 Windows 98 或 Windows Me 环境中,用于在托管和非托管函数之间传输文本时,进行 Unicode 到 ANSI 的字符映射。若为
true
,启用最佳映射,无直接对应字符时,Unicode 字符映射到 ANSI 代码页中最接近的字符,无关联时映射为
?
,默认值为
true
。
-
ThrowOnUnmappableChar
:在 Windows 98 和 Windows Me 的 Unicode 到 ANSI 转换中,若字符无对应映射,可要求抛出异常。值为
true
时,出现此情况会抛出异常并将字符转换为
?
;值为
false
时不抛出异常。
5. 可直接复制到本机的类型和非可直接复制到本机的类型
5.1 可直接复制到本机的类型
可直接复制到本机的类型在托管内存和非托管内存中有相同表示,无需互操作性封送器干预。由于任何转换都有成本,这类类型比非可直接复制到本机的类型更高效。常见的可直接复制到本机的类型包括
System.Byte
、
System.SByte
、
System.Int16
、
System.UInt16
、
System.Int32
、
System.UInt32
、
System.Int64
、
System.IntPtr
、
System.UIntPtr
、
System.Single
和
System.Double
。可直接复制到本机的类型的向量以及只包含可直接复制到本机的类型的值类型也被视为可直接复制到本机的类型。
5.2 非可直接复制到本机的类型
非可直接复制到本机的类型在托管内存和非托管内存中的表示不同。部分此类类型会由互操作性封送器自动转换,其他则需要显式封送处理。字符串类和用户定义的类就是非可直接复制到本机的类型的例子。托管字符串可封送为不同的非托管字符串,如
LPSTR
、
LPTSTR
、
LPWSTR
等。类除非进行了格式化,否则不是可直接复制到本机的类型。格式化后的类封送为格式化的值类型时是可直接复制到本机的类型。
6. 格式化类型
格式化类型是用户定义的类型,其中成员在内存中的布局是显式指定的。这些类型以
StructLayoutAttribute
为前缀,该属性根据
LayoutKind
枚举定义成员的布局。
| 值 | 描述 |
| — | — |
| LayoutKind.Auto | 运行时自动选择在非托管内存中对象成员的适当布局,使用此枚举成员定义的对象不能在托管代码外公开 |
| Sequential | 对象的成员按顺序排列,导出到非托管内存时遵循其出现的顺序。必要时,成员根据
StructLayoutAttribute.Pack
中指定的压缩方式排列,可能不连续 |
| LayoutKind.Explicit | 允许开发者使用
FieldOffsetAttribute
指定对象每个成员在非托管内存中的精确位置,适合在非托管代码中表示 C 或 C++ 的联合类型 |
6.1 代码示例
public class API
{
[DllImport("user32.dll")]
public static extern bool GetWindowRect(
IntPtr hWnd,
out Rect windowRect);
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int left;
public int top;
public int right;
public int bottom;
}
}
此代码中,
API
类包含非托管 API
GetWindowRect
,用于获取屏幕上客户区的位置。
Rect
结构体使用
StructLayout(LayoutKind.Sequential)
进行格式化,是可直接复制到本机的类型。
API.Rect client = new API.Rect();
API.GetWindowRect(this.Handle, out client);
string temp=string.Format("Left {0} : Top {1} : "+
"Right {2} : Bottom {3}", client.left,
client.top, client.right, client.bottom);
MessageBox.Show(temp);
此代码调用
GetWindowRect
函数并显示结果。
class API2
{
[DllImport("user32.dll")]
public static extern bool GetWindowRect(
IntPtr hWnd,
Rect windowRect);
[StructLayout(LayoutKind.Sequential)]
public class Rect
{
public int left;
public int top;
public int right;
public int bottom;
}
}
此代码定义了
API2
类,使用类
Rect
代替结构体,类是引用类型,默认通过引用传递,调用时无需
out
关键字。
API2.Rect client = new API2.Rect();
API2.GetWindowRect(this.Handle, client);
string temp = string.Format("Left {0} : Top {1} : " +
"Right {2} : Bottom {3}", client.left,
client.top, client.right, client.bottom);
MessageBox.Show(temp);
此代码调用
API2
类的
GetWindowRect
函数并显示结果。
6.2 模拟联合类型
C# 没有联合类型,但可使用
StructLayoutAttribute
的
LayoutKind.Explicit
选项在非托管内存中模拟联合类型。
[StructLayout(LayoutKind.Explicit)]
struct ZStruct {
[FieldOffset(0)] int fielda;
[FieldOffset(0)] short fieldb;
[FieldOffset(0)] bool fieldc;
}
在这个示例中,
ZStruct
结构体的所有字段使用相同的偏移量,模拟联合类型。
7. 方向属性
方向属性用于显式控制封送处理的方向。每个方法参数可关联
InAttribute
、
OutAttribute
或两者,以修改封送处理,等同于 IDL 中的
[in]
、
[out]
和
[in,out]
。
InAttribute
和
OutAttribute
在 C# 中也有对应的关键字。
| 关键字 | 属性 | IDL |
| — | — | — |
| 无 | InAttribute | [in] |
| Ref | InAttribute 和 OutAttribute | [in,out] |
| Out | OutAttribute | [out] |
方向属性的默认值取决于参数类型和关联的关键字。
8. StringBuilder 类
字符串是不可变的,其大小是动态计算的。非托管 API 可能需要固定长度且可修改的字符串,有些非托管 API 会在运行时分配内存来初始化字符串。这种情况下,不应使用字符串类型,而应选择
System.Text
命名空间中的
StringBuilder
类。
StringBuilder
对象是固定大小且可修改的,还可使用非托管 API 中创建的内存进行初始化。
8.1 代码示例
public class API
{
[DllImport("user32.dll")]
public static extern int GetWindowText(
IntPtr hWnd, ref string lpString, int nMaxCount);
}
public class API2
{
[DllImport("user32.dll")]
public static extern int GetWindowText(
IntPtr hWnd, StringBuilder lpString, int nMaxCount);
}
此代码两次导入非托管 API
GetWindowText
,
API
类使用字符串参数,
API2
类使用
StringBuilder
参数。
private void btnGetText_Click(object sender, EventArgs e)
{
string windowtext=null;
API.GetWindowText(this.Handle, ref windowtext,
10);
MessageBox.Show(windowtext);
}
private void btnGetText2_Click(object sender, EventArgs e)
{
StringBuilder windowtext=new StringBuilder();
API2.GetWindowText(this.Handle, windowtext,
25);
MessageBox.Show(windowtext.ToString());
}
在这个示例中,
btnGetText_Click
方法调用
API
类的
GetWindowText
方法,由于使用字符串参数,尝试修改时会抛出异常;
btnGetText2_Click
方法调用
API2
类的
GetWindowText
方法,使用
StringBuilder
参数,调用成功。
9. 非托管回调
部分非托管 API 会接收函数指针形式的回调作为参数,调用该指针来调用托管调用者中的函数。回调通常用于迭代,如非托管 API
EnumWindows
会使用回调来遍历顶级窗口的句柄。
9.1 实现非托管回调的步骤
- 查找回调函数的非托管签名。
- 定义对应的托管签名。
- 实现用作回调的函数,该函数应包含对回调的响应。
- 创建委托,用回调函数初始化。
- 使用委托作为回调参数调用非托管 API。
9.2 代码示例
class API
{
[DllImport("user32.dll")]
public static extern bool EnumWindows(
APICallback lpEnumFunc,
System.Int32 lParam);
public delegate bool APICallback(int hWnd, int lParam);
}
在这个示例中,
API
类导入
EnumWindows
函数,定义了
APICallback
委托作为回调函数的托管签名。
private void btnHandle_Click(object sender, EventArgs e)
{
API.EnumWindows(new API.APICallback(GetWindowHandle), 0);
}
bool GetWindowHandle(int hWnd, int lParam)
{
string temp = string.Format("{0:0000000}", hWnd);
listBox1.Items.Add(temp);
return true;
}
在这个示例中,
btnHandle_Click
方法调用
EnumWindows
函数,使用
GetWindowHandle
作为回调函数,将每个窗口句柄添加到列表框中。
10. 显式封送处理
显式封送处理有时是必要的,用于将非可直接复制到本机的参数、字段和返回类型转换为适当的非托管类型。封送处理对字符串非常有用,因为字符串在非托管内存中有多种可能的表示形式,默认情况下,字符串封送为
LPSTR
。可使用
MarshalAsAttribute
显式地将托管类型封送为特定的非托管类型。
UnmanagedType
枚举定义了
MarshalAsAttribute
可用的非托管类型。
| 成员 | 描述 |
| — | — |
| AnsiBStr | 以长度为前缀的 ANSI 字符串 |
| AsAny | 动态类型,在运行时确定对象类型 |
| Bool | 4 字节的布尔值 |
| BStr | 以长度为前缀的 Unicode 字符串 |
| ByValArray | 按值封送数组,
SizeConst
定义元素数量 |
| ByValTStr | 内联显示在结构体中的固定长度字符数组 |
| Currency | COM 的货币类型 |
| CustomMarshaler | 与
MarshalAsAttribute.MarshalType
或
MarshalAsAttribute.MarshalTypeRef
一起使用的自定义封送器 |
| Error | HRESULT |
| FunctionPtr | C 类型的函数指针 |
| I1 | 1 字节的整数 |
| I2 | 2 字节的整数 |
| I4 | 4 字节的整数 |
| I8 | 8 字节的整数 |
| IDispatch | COM 的 IDispatch 指针 |
| Interface | COM 的接口指针 |
| IUnknown | IUnknown 接口指针 |
| LPArray | 指向非托管数组第一个元素的指针 |
| LPStr | 以空字符结尾的 ANSI 字符串 |
| LPStruct | 指向非托管结构体的指针 |
| LPTStr | 依赖于平台的字符串 |
| LPWStr | Unicode 字符串 |
| R4 | 4 字节的浮点数 |
| R8 | 8 字节的浮点数 |
| SafeArray | 自描述数组,包含数组数据的类型、秩和边界 |
| Struct | 封送格式化的引用类型和值类型 |
| SysInt | 依赖于平台的整数(Win32 环境中为 32 位) |
| SysUInt | 依赖于平台的无符号整数(Win32 环境中为 32 位) |
| TBStr | 以依赖于平台的长度为前缀的字符串 |
| U1 | 1 字节的无符号整数 |
| U2 | 2 字节的无符号整数 |
| U4 | 4 字节的无符号整数 |
| U8 | 8 字节的无符号整数 |
| VariantBool | 2 字节的 VARIANT_BOOL 类型 |
| VBByRefStr | 特定于 Microsoft Visual Basic |
10.1 代码示例
using System;
using System.Runtime.InteropServices;
namespace Donis.CSharpBook{
public class Starter{
public static void Main() {
API.OSVERSIONINFO info=new API.OSVERSIONINFO();
info.dwOSVersionInfoSize=Marshal.SizeOf(info);
bool resp=API.GetVersionEx(ref info);
if(resp==false) {
Console.WriteLine("GetVersion a échoué");
}
Console.WriteLine("{0}.{1}.{2}",
info.dwMajorVersion,
info.dwMinorVersion,
info.dwBuildNumber);
}
}
public class API {
[DllImport("kernel32.dll")] public static extern
bool GetVersionEx(ref OSVERSIONINFO lpVersionInfo);
[StructLayout(LayoutKind.Sequential)]
public struct OSVERSIONINFO {
public System.Int32 dwOSVersionInfoSize;
public System.Int32 dwMajorVersion;
public System.Int32 dwMinorVersion;
public System.Int32 dwBuildNumber;
public System.Int32 dwPlatformId;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )]
public String szCSDVersion;
}
}
}
在这个示例中,
GetVersionEx
函数用于获取当前操作系统的信息,
szCSDVersion
字段使用
MarshalAs
属性封送为 128 个字符的数组。
11. 固定大小的缓冲区
前面的示例中,
MarshalAs
属性定义了 128 个字符或字节的固定大小字段。C# 2.0 提供了
fixed
关键字作为替代方案,其主要目的是集成非托管代码中的类型。
综上所述,非安全代码编程涉及指针操作、内存分配、平台调用、封送处理等多个方面,需要开发者仔细处理,以确保代码的正确性和性能。在实际开发中,应根据具体需求选择合适的方法和工具,合理使用非安全代码,避免出现内存泄漏、指针无效等问题。
11.1 使用 fixed 关键字创建固定大小缓冲区示例
unsafe struct FixedBufferExample
{
public fixed char buffer[128];
}
在上述代码中,定义了一个结构体
FixedBufferExample
,其中使用
fixed
关键字创建了一个大小为 128 的字符数组
buffer
。这就是一个固定大小的缓冲区,其大小在编译时就已经确定,并且在结构体实例化时会分配相应的内存空间。
11.2 操作固定大小缓冲区的代码示例
unsafe class Program
{
static void Main()
{
FixedBufferExample example = new FixedBufferExample();
for (int i = 0; i < 10; i++)
{
example.buffer[i] = (char)('A' + i);
}
for (int i = 0; i < 10; i++)
{
Console.Write(example.buffer[i]);
}
Console.WriteLine();
}
}
在这个示例中,首先实例化了
FixedBufferExample
结构体。然后使用
for
循环向固定大小的缓冲区
buffer
中写入字符,从
'A'
开始依次递增。最后再使用一个
for
循环将缓冲区中的前 10 个字符输出到控制台。
12. 非安全代码编程的注意事项
12.1 内存管理
-
避免内存泄漏
:在使用
stackalloc或其他内存分配方式时,要确保在不再使用内存时,其能被正确释放。例如,stackalloc分配的内存会在函数结束时自动释放,但如果使用其他自定义的内存分配方式,需要开发者手动管理内存释放。 - 防止悬空指针 :不要返回指向局部变量的指针,因为局部变量在函数结束后会被销毁,指针会变成悬空指针,访问悬空指针会导致未定义行为。
12.2 指针操作
- 边界检查 :在进行指针运算时,如指针加法、减法等,要确保不会越界访问内存。例如,在操作数组指针时,要确保指针不会超出数组的边界。
- 空指针检查 :在使用指针之前,要检查指针是否为空,避免对空指针进行解引用操作,这会导致程序崩溃。
12.3 平台调用
-
参数和返回类型匹配
:在使用
DllImport进行平台调用时,要确保托管代码中的参数和返回类型与非托管函数的参数和返回类型相匹配。否则,可能会导致数据截断、类型不匹配等问题。 -
错误处理
:对于可能失败的非托管函数调用,要进行错误处理。例如,使用
SetLastError选项获取错误代码,并根据错误代码进行相应的处理。
12.4 封送处理
-
选择合适的封送方式
:对于非可直接复制到本机的类型,要根据具体情况选择合适的封送方式。例如,对于字符串类型,要根据非托管函数的要求选择
CharSet选项,确保字符串能正确封送。 - 自定义封送器 :在某些复杂的情况下,可能需要使用自定义封送器来处理特殊的类型转换。
13. 非安全代码编程的应用场景
13.1 性能优化
在对性能要求极高的场景下,非安全代码编程可以通过直接操作内存和指针,避免托管代码的一些开销,从而提高程序的执行效率。例如,在处理大规模数据时,使用指针操作数组可以减少数组索引的开销。
13.2 与非托管代码交互
当需要调用非托管的 DLL 中的函数时,就需要使用平台调用和封送处理等非安全代码编程技术。例如,调用 Win32 API 来实现一些系统级的功能,如获取屏幕尺寸、创建目录等。
13.3 实现底层算法
在实现一些底层算法时,非安全代码编程可以更方便地操作内存和数据结构。例如,实现自定义的内存分配器、哈希表等数据结构。
14. 非安全代码编程的流程图示例
graph TD;
A[开始] --> B[指针操作]
B --> C{是否需要内存分配}
C -- 是 --> D[使用 stackalloc 或其他方式分配内存]
C -- 否 --> E[继续指针操作]
D --> F{是否需要平台调用}
E --> F
F -- 是 --> G[使用 DllImport 进行平台调用]
F -- 否 --> H[进行其他操作]
G --> I{是否需要封送处理}
H --> I
I -- 是 --> J[进行封送处理]
I -- 否 --> K[执行函数并处理结果]
J --> K
K --> L{是否完成操作}
L -- 否 --> B
L -- 是 --> M[结束]
这个流程图展示了非安全代码编程的一般流程。从开始指针操作,根据需求进行内存分配、平台调用和封送处理等操作,最后根据操作是否完成决定是否继续循环或结束程序。
15. 总结
非安全代码编程是 C# 中一项强大但需要谨慎使用的技术。它涉及指针操作、内存管理、平台调用和封送处理等多个复杂的方面。通过合理运用这些技术,可以提高程序的性能,实现与非托管代码的交互,以及实现一些底层算法。
在实际编程中,开发者需要充分了解每种技术的特点和使用场景,严格遵循相关的注意事项,避免出现内存泄漏、指针无效、类型不匹配等问题。同时,要根据具体的需求选择合适的方法和工具,确保代码的正确性和稳定性。
希望通过本文的介绍,读者能够对非安全代码编程有更深入的理解,并在实际开发中合理运用这些技术。
超级会员免费看
10万+

被折叠的 条评论
为什么被折叠?



