56、非安全代码编程全解析

非安全代码编程全解析

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 实现非托管回调的步骤

  1. 查找回调函数的非托管签名。
  2. 定义对应的托管签名。
  3. 实现用作回调的函数,该函数应包含对回调的响应。
  4. 创建委托,用回调函数初始化。
  5. 使用委托作为回调参数调用非托管 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# 中一项强大但需要谨慎使用的技术。它涉及指针操作、内存管理、平台调用和封送处理等多个复杂的方面。通过合理运用这些技术,可以提高程序的性能,实现与非托管代码的交互,以及实现一些底层算法。

在实际编程中,开发者需要充分了解每种技术的特点和使用场景,严格遵循相关的注意事项,避免出现内存泄漏、指针无效、类型不匹配等问题。同时,要根据具体的需求选择合适的方法和工具,确保代码的正确性和稳定性。

希望通过本文的介绍,读者能够对非安全代码编程有更深入的理解,并在实际开发中合理运用这些技术。

下载前可以先看下教程 https://pan.quark.cn/s/a4b39357ea24 在网页构建过程中,表单(Form)扮演着用户与网站之间沟通的关键角色,其主要功能在于汇集用户的各类输入信息。 JavaScript作为网页开发的核心技术,提供了多样化的API和函数来操作表单组件,诸如input和select等元素。 本专题将详细研究如何借助原生JavaScript对form表单进行视觉优化,并对input输入框与select下拉框进行功能增强。 一、表单基础1. 表单组件:在HTML语言中,<form>标签用于构建一个表单,该标签内部可以容纳多种表单组件,包括<input>(输入框)、<select>(下拉框)、<textarea>(多行文本输入区域)等。 2. 表单参数:诸如action(表单提交的地址)、method(表单提交的协议,为GET或POST)等属性,它们决定了表单的行为特性。 3. 表单行为:诸如onsubmit(表单提交时触发的动作)、onchange(表单元素值变更时触发的动作)等事件,能够通过JavaScript进行响应式处理。 二、input元素视觉优化1. CSS定制:通过设定input元素的CSS属性,例如border(边框)、background-color(背景色)、padding(内边距)、font-size(字体大小)等,能够调整其视觉表现。 2. placeholder特性:提供预填的提示文字,以帮助用户明确输入框的预期用途。 3. 图标集成:借助:before和:after伪元素或者额外的HTML组件结合CSS定位技术,可以在输入框中嵌入图标,从而增强视觉吸引力。 三、select下拉框视觉优化1. 复选功能:通过设置multiple属性...
【EI复现】基于深度强化学习的微能源网能量管理与优化策略研究(Python代码实现)内容概要:本文围绕“基于深度强化学习的微能源网能量管理与优化策略”展开研究,重点探讨了如何利用深度强化学习技术对微能源系统进行高效的能量管理与优化调度。文中结合Python代码实现,复现了EI级别研究成果,涵盖了微电网中分布式能源、储能系统及负荷的协调优化问题,通过构建合理的奖励函数与状态空间模型,实现对复杂能源系统的智能决策支持。研究体现了深度强化学习在应对不确定性可再生能源出力、负荷波动等挑战中的优势,提升了系统运行的经济性与稳定性。; 适合人群:具备一定Python编程基础和机器学习背景,从事能源系统优化、智能电网、强化学习应用等相关领域的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于微能源网的能量调度与优化控制,提升系统能效与经济效益;②为深度强化学习在能源管理领域的落地提供可复现的技术路径与代码参考;③服务于学术研究与论文复现,特别是EI/SCI级别高水平论文的仿真实验部分。; 阅读建议:建议读者结合提供的Python代码进行实践操作,深入理解深度强化学习算法在能源系统建模中的具体应用,重点关注状态设计、动作空间定义与奖励函数构造等关键环节,并可进一步扩展至多智能体强化学习或与其他优化算法的融合研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值