unity的C#学习——不安全代码(声明不安全代码块:实现C/C++指针的创建与相关操作)


C# 不安全代码

不安全代码(Unsafe code)是指使用了指针变量的代码块,或者执行需要访问非托管代码(unmanaged code)的操作指针是一个变量,其值为另一个变量的地址,即内存位置的直接地址。在C#中,不安全代码必须使用 unsafe 修饰符标记,并需要在编译时启用 AllowUnsafeCode 选项。

为了编译不安全代码,您必须切换到命令行编译器指定 /unsafe 命令行。
例如,为了编译包含不安全代码的名为 prog1.cs 的程序,需在命令行中输入命令:

csc /unsafe prog1.cs

要在 Visual Studio 中启用 AllowUnsafeCode,请按照以下步骤操作:

  1. 打开你的 C# 项目。
  2. 右键单击项目并选择“属性”。
  3. 选择“建构”选项卡。
  4. 在“高级”下拉菜单中,将“允许不安全代码”选项设置为“是”。
  5. 单击“确定”保存更改。

不安全代码通常用于以下几个方面:

  • 调用非托管代码(例如使用DllImport调用C++ DLL)
  • 实现某些底层功能,例如指针操作和内存管理
  • 对于性能关键的代码,例如高性能算法和图形渲染。

1、不安全代码的基本语法

不安全代码的基本语法主要涉及 unsafe 关键字和指针变量的声明和使用:

1.1 声明不安全代码块

使用 unsafe 关键字声明不安全代码块,例如:

unsafe
{
    
    
    // 不安全的代码
}

1.2 声明指针变量

使用 * 运算符声明指针变量,例如:

int* ptr;

这里的 ptr 是指向 int 类型变量的指针。下标展示了常见指针类型声明的实例:

实例 描述
int* p p 是指向整数的指针。
double* p p 是指向双精度数的指针。
float* p p 是指向浮点数的指针。
int** p p 是指向整数的指针的指针。
int*[] p p 是指向整数的指针的一维数组。
char* p p 是指向字符的指针。
void* p p 是指向未知类型的指针。

在一条语句中声明多个指针时,* 仅与基础类型一起写入,而不是用作每个指针名称的前缀 。 例如:

int* p1, p2, p3;     // 正确  
int *p1, *p2, *p3;   // 错误 

1.3 操作指针变量

变量使用 & 运算符,可以获取变量的地址,例如:

int x = 10;
int* ptr = &x;

这里的 ptr 指向 x 的地址。

指针使用 * 运算符,可以访问指针所指向的变量的值(通常称之为“解引用指针”),例如:

int y = *ptr;

这里的 y 等于 x 的值。

两个运算符也可以结合使用,从而创建一个指向相同变量的指针,下面是一个示例代码:

unsafe 
{
    
    
    int value = 10;
    int* ptr = &value;  // 定义指针变量
    int* ptr2 = &(*ptr);  // 获取指针ptr的地址,并定义新指针ptr2
}

2、不安全代码的类型转换

在 C# 中,指针和引用类型之间的类型转换需要使用强制类型转换。可以使用 (type)expression 的语法进行强制类型转换,其中 type 是要转换为的类型,而 expression 是要转换的表达式。

例如,将 int* 转换为 long*,可以使用以下语法:

int* ptr = ...;
long* lptr = (long*)ptr;

此外还可以使用类型转换方法,详见上面链接的C#数据类型转换部分。

2.1 错误用法示例

需要注意的是,进行指针类型转换时需要非常小心,因为指针类型的转换很容易导致不安全的内存访问。下面是一个 int 类型的指针转换为 float 类型的指针,然后通过指针修改 float 类型的值的示例代码:

unsafe static void Main(string[] args)
{
    
    
    int intValue = 10;

    // 创建一个指向 int 变量的指针
    int* intPtr = &intValue;

    // 将 int 类型的指针转换为 float 类型的指针
    float* floatPtr = (float*)intPtr;

    // 通过 float 类型的指针修改值
    *floatPtr = 20.5f;

    Console.WriteLine("intValue = {0}", intValue);  // 输出:intValue = 1092616192
    Console.WriteLine("*floatPtr = {0}", *floatPtr);  // 输出:*floatPtr = 20.5
}

上述代码中通过 float 类型的指针修改了 intValue 变量的内存,这是一种类型不匹配的类型转换,导致内存错误,intValue 的值变为一串混乱的数字,我们应该避免这样的操作。

2.2 正确用法示例

下面的实例演示了正确的类型转换用法,使用到 ToString 类型转换方式,和强制转换符 (int)

using System;
namespace UnsafeCodeApplication
{
    
    
   class Program
   {
    
    
      public static void Main()
      {
    
    
         unsafe
         {
    
    
            int var = 20;
            int* p = &var;
            Console.WriteLine("Data is: {0} " , var);
            Console.WriteLine("Data is: {0} " , p->ToString());
            Console.WriteLine("Address is: {0} " , (int)p);
         }
         Console.ReadKey();
      }
   }
}

上面使用了不安全代码来创建一个指向 int 类型变量的指针,并使用 -> 运算符访问该指针所指向的变量,并使用 ToString() 方法将其转换为字符串输出。同时,它还将变量地址强转为 int 类型后打印。

  • -> 运算符用于访问通过指针间接引用的结构体或类的成员。它是一个简便的语法,等同于用 * 运算符访问指针,然后再用 . 运算符访问成员。
  • 例如,对于一个指向结构体的指针 ptr,可以使用 ptr->member 来访问结构体中的成员 member,这等同于使用 (*ptr).member
  • 所以上述代码相当于访问p指针指向 int 类型的实例,再访问该实例的 ToString() 方法,等同语使用 (*p).ToString()

当上面的代码被编译和执行时,它会产生下列结果:

Data is: 20
Data is: 20
Address is: 77128984


3、固定托管对象的地址

使用 fixed 关键字创建指向托管对象(如数组)的指针,并在关键字的作用域内固定托管对象的(首)地址为指针所指的内存位置,以确保 GC 不会移动该托管对象。例如:

unsafe
{
    
    
    int[] arr = new int[10];
    fixed (int* p = arr)
    {
    
    
        // 操作指向数组的指针 p
    }
}

这里的 p 指向数组 arr 的首地址,且该数组地址在 fixed 代码块的作用域内固定,不会被 GC 移动。

GC是垃圾回收(Garbage Collection)的缩写,是指计算机程序运行时,自动检测和回收不再使用的内存资源的机制。

在固定了数组的内存地址后,我们就可以通过指针操作数组的元素,下面的实例演示了这点:

using System;

namespace UnsafeCodeApplication
{
    
    
   class TestPointer
   {
    
    
      public unsafe static void Main()
      {
    
    
         int[] list = {
    
    10, 100, 200};

         // 使用fixed关键字创建指向list数组的指针
         fixed (int* ptr = list)
         {
    
    
            /* 显示指针中数组地址 */
            for (int i = 0; i < 3; i++)
            {
    
    
               // 打印第i个元素的地址
               Console.WriteLine("Address of list[{0}]={1}", i, (int)(ptr + i));

               // 打印第i个元素的值
               Console.WriteLine("Value of list[{0}]={1}", i, *(ptr + i));
            }
         }

         Console.ReadKey();
      }
   }
}

在上述代码中,数组名称 int[] list指向数组的指针 int *p 是不同的变量类型

  • 我们可以增加指针变量 p,因为它在内存中不是固定的;
  • 但是数组首地址在内存被固定,所以我们不能增加变量 list

当上面的代码被编译和执行时,它会产生下列结果:

Address of list[0] = 31627168
Value of list[0] = 10
Address of list[1] = 31627172
Value of list[1] = 100
Address of list[2] = 31627176
Value of list[2] = 200


4、指针作为方法的参数

指针可以作为方法的参数,使得方法可以直接修改指向内存位置的数据(效果等同于引用传参)。但要注意的是,如果方法的参数中包含指针类型,那么在方法声明中需要添加 unsafe 关键字,表示该方法包含不安全代码。

  • 在声明了 unsafe 的方法内部,就不需要再声明 unsafe 代码块了,因为方法的作用域已经被标记为不安全,可以直接使用指针等不安全的操作

下面是一个实例代码:

using System;

namespace UnsafeCodeApplication
{
    
    
    class TestPointer
    {
    
    
        // swap 方法:交换两个整型指针所指向的变量的值
        public unsafe void swap(int* p, int *q)
        {
    
    
            int temp = *p; // 用临时变量保存 p 指针所指向的值
            *p = *q;       // 用 q 指针所指向的值更新 p 指针所指向的值
            *q = temp;     // 用临时变量中的值更新 q 指针所指向的值
        }

        public unsafe static void Main()
        {
    
    
            TestPointer p = new TestPointer();
            int var1 = 10;   // 定义整型变量 var1,并赋值为 10
            int var2 = 20;   // 定义整型变量 var2,并赋值为 20
            int* x = &var1;  // 定义指向 var1 变量的指针 x
            int* y = &var2;  // 定义指向 var2 变量的指针 y

            // 输出变量交换前的值
            Console.WriteLine("Before Swap: var1:{0}, var2: {1}", var1, var2);

            // 调用 swap 方法,将 x 和 y 作为参数传入
            p.swap(x, y);

            // 输出变量交换后的值
            Console.WriteLine("After Swap: var1:{0}, var2: {1}", var1, var2);

            Console.ReadKey();
        }
    }
}

当上面的代码被编译和执行时,它会产生下列结果:

Before Swap: var1: 10, var2: 20
After Swap: var1: 20, var2: 10


5、不安全代码的安全性问题

不安全代码具有以下安全性问题:

  • 内存访问越界:不安全代码使用指针访问内存,如果指针指向的内存区域超出了程序分配的内存区域范围,就会出现内存访问越界的问题。内存访问越界可能会导致程序崩溃、数据损坏等问题。

  • 空指针引用:在不安全代码中,指针可能为 null,这就意味着指针指向的内存地址是无效的。如果程序尝试访问空指针所指向的内存区域,就会出现空指针引用的问题。

  • 内存泄漏:在不安全代码中,程序需要手动分配和释放内存,如果程序忘记释放内存,就会出现内存泄漏的问题。内存泄漏可能会导致程序占用过多的内存,最终导致系统崩溃或变慢。

  • 缓冲区溢出:在不安全代码中,程序使用指针访问数组或缓冲区时,如果程序没有对数组或缓冲区的长度进行检查,就可能会出现缓冲区溢出的问题。缓冲区溢出可能会导致程序崩溃、数据损坏或安全漏洞。

因此,在编写和使用不安全代码时,应该非常小心,确保代码的正确性和安全性。可以使用代码静态分析工具或手动代码审查等方式来减少不安全代码的风险。此外,不应该滥用不安全代码,应该尽可能使用安全的代码编写方式来避免潜在的安全问题。


6、使用不安全代码实现高性能算法

使用不安全代码可以实现高性能算法,因为它可以直接访问和修改内存中的数据,而不需要经过语言的类型检查和其他安全性检查。这使得不安全代码可以更快地执行,因为它可以避免一些额外的开销。下面是一些使用不安全代码实现高性能算法的例子:

  • 图像处理算法:在图像处理中,需要处理大量的像素数据。使用不安全代码可以直接访问像素数据,从而实现更快的图像处理算法。

  • 数组操作算法:在某些算法中,需要频繁地访问数组元素。使用不安全代码可以直接访问数组元素,避免了数组边界检查等开销,从而实现更快的数组操作算法。

  • 高精度计算算法:在某些高精度计算算法中,需要频繁地进行位操作和指针操作。使用不安全代码可以更方便地进行这些操作,从而实现更快的高精度计算算法。

需要注意的是,使用不安全代码需要谨慎,因为它可能会导致内存泄漏和其他安全问题。在编写不安全代码时,需要确保代码的正确性和安全性,以避免出现意外的问题。

猜你喜欢

转载自blog.csdn.net/qq_50571974/article/details/129954938