.NET(C#) 中使用指针

在 .NET 中,C# 默认情况下是一种安全的、不支持直接使用指针的语言。然而,C# 支持在特定情况下使用不安全代码(即可以使用指针的代码)。使用不安全代码意味着放弃了 C# 和 .NET 运行时提供的一些安全保障,因此必须谨慎使用。不安全代码可能会增加程序出错的风险,特别是与内存管理相关的错误,如堆损坏、内存泄漏等。指针操作通常只在性能至关重要的情况下使用,或者在需要与系统级别或非托管代码互操作时使用。

1、 开启不安全代码支持

要在 C# 项目中使用指针,首先需要在项目设置中启用不安全代码。这通常在项目属性中设置。

1)Visual Studio 设置

在项目上右键属性,在点击 "生成" ,勾选允许不安代码,有些vs中那个选项可能是英文的,需要注意一下,如下图,


2)修改.csproj 文件

在 .csproj 文件中添加以下代码:

<PropertyGroup>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

2、使用 unsafe 关键字

在 C# 中,任何包含指针操作的代码块都需要被 unsafe 关键字标记。这可以是一个方法、一个代码块或整个类。

1)使用unsafe代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
             unsafe
             {
                 int[] numbers = { 10, 20, 30, 40, 50 };
                 // 固定数组,防止垃圾回收器移动它,获取数组的指针
                 fixed (int* p = numbers)
                 {
                     // 输出数组中的每个元素
                     for (int i = 0; i < numbers.Length; i++)
                     {
                         Console.WriteLine("Element {0}: {1}", i, *(p + i));   
                      }
                  }
              }
              Console.ReadKey();
        }
    }
}

 2)使用unsafe关键字

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication
{
    class Program
    {
            // 定义一个标记了 unsafe 的方法
        public unsafe static void UnsafeMethod()
        {
            int value = 10;
            int* pValue = &value; // 获取变量的地址
    
            Console.WriteLine("Value before change: " + value);
            
            // 使用指针修改变量的值
            *pValue = 20;
            Console.WriteLine("Value after change: " + value);
        }
        static void Main(string[] args)
        {
              // 调用 unsafe 方法
              UnsafeMethod();
              Console.ReadKey();
        }
    }
}

3、指针操作

声明指针使用 * 操作符声明指针变量,例如 int* p;  ,获取变量地址:使用 & 操作符获取变量的地址,例如 p = &myVar;。访问指针指向的值使用 * 操作符访问或修改指针指向的值,例如 *p = 5;。 

1)基础操作

使用不安全代码时,程序员负责确保代码的正确性和内存安全性。错误的指针操作可能会导致程序崩溃或数据损坏。fixed 语句限制了垃圾回收器对固定对象的处理,因此应谨慎使用,以免影响性能。

using System;
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {    
             //用指针操作栈上的值类型
             int maxValue = 10;
             int* p = &maxValue;
             *p = 20;
             Console.WriteLine(p->ToString());eadKey();
        }
    }
    //要用指针操作托管堆上的值类型,需要用到 fixed关键字
    public unsafe class Coder
     {
        public int Age;
        public void SetAge(int age)
        {
            fixed (int* p = &Age)
            {
                *p = age;
            }
        }
    }
}

2)分配释放内存

使用指针操作内存时,可以通过 Marshal 类来分配和释放非托管内存。这是因为在 .NET 中,内存管理通常是自动的,但在使用指针进行非托管内存操作时,需要手动处理内存的分配和释放。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
             // 分配非托管内存
              int size = sizeof(int);
             IntPtr pMemory =  System.Runtime.InteropServices.Marshal.AllocHGlobal(size);
     
             try
             {
                 // 将 IntPtr 转换为指针
                 int* pInt = (int*)pMemory;
     
                 // 在分配的内存中存储数据
                 *pInt = 123;
     
                 // 读取内存中的数据
                 Console.WriteLine("Data: " + *pInt);
             }
             finally
             {
                 // 释放非托管内存
                  System.Runtime.InteropServices.Marshal.FreeHGlobal(pMemory);
             }
              Console.ReadKey();
        }
    }

}
             System.Runtime.InteropServices.Marshal.FreeHGlobal(handle);

  3)用IDispose接口管理内存

实现 IDisposable 接口允许你定义一个 Dispose 方法,用于释放类所占用的非托管资源。这是一种确定性的清理方式,可以控制何时释放资源。

   public unsafe class UnmanagedMemory : IDisposable
        {
            public int Count { get; private set; }
            private byte* Handle;
            private bool _disposed = false;
            public UnmanagedMemory(int bytes)
            {
                Handle = (byte*) System.Runtime.InteropServices.Marshal.AllocHGlobal(bytes);
                Count = bytes;
            }
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(true);
            }
            protected virtual void Dispose( bool isDisposing )
            {
                if (_disposed) return;
                if (isDisposing)
                {
                    if (Handle != null)
                    {
                        System.Runtime.InteropServices.Marshal.FreeHGlobal((IntPtr)Handle);
                    }
                }
                _disposed = true;
            }
            ~UnmanagedMemory()
           {
              Dispose( false );
           }
        }

  使用方法:

 using (UnmanagedMemory memory = new UnmanagedMemory(10))
            {
                int* p = (int*)memory.Handle;
                *p = 20;
                Console.WriteLine(p->ToString());
            }

4)使用stackalloc关键字

stackalloc 关键字用于在栈上分配内存块,通常用于分配小型数组。与在堆上分配内存(如使用 new 关键字)不同,stackalloc 分配的内存不需要垃圾回收,因此可以提高性能。但是,它只应该用于较小的内存分配,因为栈空间相对有限。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
             /// 分配一个大小为 10 的整数数组
            int* array = stackalloc int[10];
    
            // 初始化数组
            for (int i = 0; i < 10; i++)
            {
                array[i] = i;
            }
    
            // 打印数组内容
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(array[i]);
            }
       }
    }

}

4、C# 指针操作的几个缺点

在 C# 中使用指针操作(通常在 unsafe 代码块中进行)确实提供了更直接的内存访问和潜在的性能优势,但它也带来了一些明显的缺点和风险。

1)只能用来操作值类型
指针在 C# 中主要用于操作值类型(如 int, float, char 等)。.NET中,引用类型的内存管理全部是由GC管理的,无法取得其地址,因此,无法用指针来操作引用类型。
2)泛型不支持指针类型
泛型的一个主要优点是提供类型安全。但是,由于指针操作本质上是不安全的,因此 C# 不允许在泛型类或方法中使用指针类型。这意味着你不能创建一个泛型类或方法来处理通用的指针操作,限制了泛型的灵活
3)函数指针
C# 中有delegate,delegate 支持支持指针类型,lambda 表达式也支持指针。委托是 C# 中一个非常强大的功能,它允许将方法作为参数传递给其他方法,类似于 C 和 C++ 中的函数指针。委托可以引用具有特定参数列表和返回类型的任何方法。C# 提供 delegate 类型来定义安全函数指针对象。

推荐阅读
cjavapy编程之路首页