• P/Invoke各种总结(五、在C#中使用指针类型)


    C#向开发人员隐藏了大部分基本内存管理操作,因为它使用了垃圾回收器和引用。但是,有时候我们也需要直接访问内存,例如:进行平台调用,性能优化等等。

    .Net平台定义了两种主要数据类型:值类型和引用类型,其实还有第三种数据类型:指针类型。使用指针,可以绕开CLR的内存管理机制。(说明:在C#中使用指针,需要有相关C/C++指针操作基础)

    1、C#中指针相关的操作符和关键字

    操作符/关键字 作用
    * 该操作符用于创建一个指针变量,和在C/C++中一样。也可用于指针间接寻址(解除引用)
    & 该操作符用于获取内存中变量的地址
    -> 该操作符用于访问一个由指针表示的类型的字段,和在C++中一样
    [] 在不安全的上下文中,[]操作符允许我们索引由指针变量指向的位置
    ++,-- 在不安全的上下文中,递增和递减操作符可用于指针类型
    +,- 在不安全的上下文中,加减操作符可用于指针类型
    ==, !=, <, >, <=, >= 在不安全的上下文中,比较和相等操作符可用于指针类型
    stackalloc 在不安全的上下文中,stackalloc关键字可用于直接在栈上分配C#数组,类似CRT中的_alloca函数 
     fixed 在不安全的上下文中,fixed关键字可用于临时固定一个变量以使它的地址可被找到

     2、在C#中使用指针,需要启用“允许不安全代码”设置

    选择项目属性->生成,钩上“允许不安全代码”

    3、unsafe关键字

    只有在unsafe所包含的代码区块中,才能使用指针。类似lock关键字的语法结构

    除了声明代码块为不安全代码外,也可以直接构建“不安全的”结构、类型成员和函数。

    1   unsafe struct Point
    2         {
    3             public int x;
    4             public int y;
    5             public Point* next;
    6             public Point* previous;
    7         }
    1    unsafe static void CalcPoint(Point* point)
    2         {
    3             //
    4         }

     也可以在导入非托管 DLL 的函数声明中使用unsafe

    1  [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    2  private static extern unsafe int memcpy(void* dest, void* src, int count);

    注意:

    指针不能指向引用或包含引用的结构,因为无法对对象引用进行垃圾回收,即使有指针指向它也是如此。 垃圾回收器并不跟踪是否有任何类型的指针指向对象。

    下面的示例代码可以说明:

        /// <summary>
        /// 声明一个Point结构体
        /// </summary>
        struct Point
        {
            public int x;
            public int y;      
        }
    
          static void Main(string[] args)
            {
                unsafe
                {
                    //编译正常
                    Point p = new Point();
                    Point* pp = &p;
                }
            }
    1  //换成类
    2  class Point
    3     {
    4         public int x;
    5         public int y;      
    6     }

     4、*和&操作符

    在不安全的上下文中,可以使用 操作符构建数据类型相对应的指针类型(指针类型、值类型和引用类型,示例代码中的type),使用 操作符获取被指向的内存地址。

    1 type* identifier;
    2 void* identifier; //允许但不推荐

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

    1 int* p1, p2, p3;   // 正常
    2 int *p1, *p2, *p3;   // 错误

     下面是使用*操作符进行指针类型声明

    int* p p 是指向整数的指针。
    int** p p 是指向整数的指针的指针。
    int*[] p p 是指向整数的指针的一维数组。
    char* p p 是指向字符的指针。
    void* p p 是指向未知类型的指针。

    注意:

    1、无法对 void* 类型的指针应用间接寻址运算符。 但是,你可以使用强制转换将 void 指针转换为任何其他指针类型,反过来也是可以的。

    2、指针类型不从object类继承,并且指针类型与 object 之间不存在转换。 此外,装箱和取消装箱不支持指针。 

    下面的代码演示了如何声明指针类型:

     1 static void Main(string[] args)
     2         {
     3             int []a = { 1, 2, 3, 4, 4 };
     4 
     5             unsafe
     6             {
     7                 //临时固定一个变量以使它的地址可被找到
     8                 fixed (int* p = &a[0])
     9                 {
    10                     int* p2 = p;
    11                     Console.WriteLine(*p2);
    12                     p2++;
    13                     Console.WriteLine(*p2);
    14                     p2++;
    15                     Console.WriteLine(*p2);
    16                 }
    17             }
    18 
    19         }

     输出结果如下:

    1

    2

    3

    下面的代码演示了如何使用指针类型进行数据交换:

     1 static void Main(string[] args)
     2         {
     3             int a = 1;
     4             int b = 2;
     5 
     6             unsafe
     7             {
     8                 UnsafeSwap(&a, &b);
     9             }
    10 
    11             Console.WriteLine(a);
    12             Console.WriteLine(b);
    13         }
    14 
    15         /// <summary>
    16         /// 使用指针
    17         /// </summary>
    18         /// <param name="a"></param>
    19         /// <param name="b"></param>
    20         static unsafe void UnsafeSwap(int* a,int *b)
    21         {
    22             int temp = *a;
    23             *a = *b;
    24             *b = temp;
    25         }
    26 
    27         /// <summary>
    28         /// 不使用指针的安全版本
    29         /// </summary>
    30         /// <param name="a"></param>
    31         /// <param name="b"></param>
    32         static void SafeSwap(ref int a,ref int b)
    33         {
    34             int temp = a;
    35             a = b;
    36             b = temp;
    37         }

    输出结果如下:

    2

    1

    5、通过指针访问字段

    定义如下结构体

     1  struct Point
     2         {
     3             public int x;
     4             public int y;
     5 
     6             public override string ToString()
     7             {
     8                 return $"x:{x},y:{y}";
     9             }
    10         }

    如果声明一个Point类型的指针,就需要使用指针字段访问操作符(->)来访问公共成员(和C++一样),也可以使用指针间接寻址操作符(*)来解除指针的引用,使其也可以使用 (.)操作符访问字段(和C++一样)。

     1 static unsafe void Main(string[] args)
     2         {
     3             //通过指针访问成员
     4             Point point = new Point();
     5             Point* p = &point;
     6             p->x = 10;
     7             p->y = 5;
     8             Console.WriteLine(p->ToString());
     9 
    10             //通过指针间接寻址访问成员
    11             Point point2; //不使用 new 运算符的情况下对其进行实例化,需要在首次使用实例之前必须初始化所有实例字段。
    12             Point* p2 = &point2;
    13             (*p2).x = 128;
    14             (*p2).y = 256;
    15             Console.WriteLine((*p2).ToString());
    16         }

    运行结果如下:

    x:10,y:5
    x:128,y:256

    6、stackalloc关键字

    在不安全上下文中,可能需要声明一个直接从调用栈分配内存的本地变量(不受.Net垃圾回收器控制)。C#提供了与CRT函数_alloca等效的stackalloc关键字来满足这个需求。

     1 static unsafe void Main(string[] args)
     2         {
     3             char* p = stackalloc char[3];
     4 
     5             for (int i = 0; i < 3; i++)
     6             {
     7                 p[i] = (char)(i+65); //A-C
     8             }
     9 
    10             Console.WriteLine(*p);
    11             Console.WriteLine(p[0]);
    12 
    13             Console.WriteLine(*(++p));
    14             Console.WriteLine(p[0]);
    15 
    16             Console.WriteLine(*(++p));
    17             Console.WriteLine(p[0]);
    18         }

    输出结果如下:

    A
    A
    B
    B
    C
    C

    7、fixed关键字

    在上面的示例中,我们可以看到,通过stackalloc关键字,在不安全上下文中分配一大块内存非常方便。但这块内存是在栈上的,当分配方法返回的时候,被分配的内存立即被清理。

    假设有如下情况:

    声明一个引用类型PointRef和一个值类型Point

     1     class PointRef
     2         {
     3             public int x;
     4             public int y;
     5 
     6             public override string ToString()
     7             {
     8                 return $"x:{x},y:{y}";
     9             }
    10         }
    11 
    12         struct Point
    13         {
    14             public int x;
    15             public int y;
    16 
    17             public override string ToString()
    18             {
    19                 return $"x:{x},y:{y}";
    20             }
    21         }

    调用者声明了一个PointRef类型的变量,内存将被分配在垃圾回收器堆上。如果一个不安全的上下文要与这个对象(或这个堆上的任何对象)交互,就可能会出现问题,因为垃圾回收可随时发生。设想一下,恰好在清理堆的时候访问Point成员,这就很

    为了将不安全上下文中的引用类型变量固定,C#提供了fixed关键字,fixed语句设置指向托管类型的指针并在代码执行过程中固定该变量。换句说话:fixed关键字可以锁定内存中的引用变量。这样在语句的执行过程中,该变量地址保持不变。

    事实上,也只有使用fixed关键字,C#编译器才允许指针指向托管变量。

     1 static unsafe void Main(string[] args)
     2         {
     3             PointRef pointRef = new PointRef();
     4             Point point = new Point();
     5 
     6             int a = &pointRef.x;  //编译不通过
     7 
     8             int *b = &point.x;    //编译通过
     9 
    10             fixed(int *c = &pointRef.x)
    11             {
    12                 //编译通过
    13             }
    14         }

     说明:

    在fixed中初始化多个变量也是可以的

    1             //同时声明多个指针变量的语法跟C++中的不一样,需要注意
    2             fixed(int *e = &(pointRef.x) , f = &(pointRef.y) )
    3             {
    4 
    5             }

    8、sizeof关键字

    在不安全上下文中,sizeof关键字用于获取值类型(不是引用类型)的字节大小。sizeof可计算任何由System.ValueType派生实体的字节数。

     1    static  void Main(string[] args)
     2         {
     3             unsafe
     4             {
     5                 //不安全版本
     6                 Console.WriteLine(sizeof(int));
     7                 Console.WriteLine(sizeof(float));
     8                 Console.WriteLine(sizeof(Point));
     9             }
    10 
    11             //安全版本
    12             Console.WriteLine(Marshal.SizeOf(typeof(int)));
    13             Console.WriteLine(Marshal.SizeOf(typeof(float)));
    14             Console.WriteLine(Marshal.SizeOf(typeof(Point)));
    15 
    16         }

    9、避免使用指针

    事实上在C#中,指针并不是新东西。因为在代码中可以自由使用引用 ,而引用就是一个类型安全的指针。指针只是一个存储地址的变量,这和引用其实是一个原理。引用的主要作用是使C#更易于使用,防止用户无意中执行某些破坏内存中内容的操作。

    使用指针后,可以进行低级的内存访问,但这是有代价的,使用指针的语法比引用类型的语法复杂得多,而且指针使用起来也比较困难,需要较高的编程技巧和强力。如果不仔细,就容易在程序中引入细微的,难以查找的错误。另外,如果使用指针,就必须授予代码运行库的代码访问安全机制的高级别信任,否则就不能执行它。

    MSDN上有如下关于指针的说明:

    在公共语言运行时 (CLR) 中,不安全代码是指无法验证的代码。 C# 中的不安全代码不一定是危险的;只是 CLR 无法验证该代码的安全性。 因此,CLR 将仅执行完全信任的程序集中的不安全代码。 如果你使用不安全代码,你应该负责确保代码不会引发安全风险或指针错误。

    大多数情况下,可以使用System.Intptr或ref关键字来替代指针完成我们想要的操作。

    下面使用示例代码说明一下:(仅供演示)

    这里还是以memcpy函数为例,假设我有一个Point结构的实例,要对这个Point进行拷贝。

    声明Point结构

    1 struct Point
    2     {
    3         public int x;
    4         public int y;
    5     }

    使用System.IntPtr:

    1         /// <summary>
    2         /// 使用IntPtr
    3         /// </summary>
    4         /// <param name="pDst"></param>
    5         /// <param name="pSrc"></param>
    6         /// <param name="count"></param>
    7         /// <returns></returns>
    8         [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl)]
    9         private static extern unsafe int memcpyi(IntPtr pDst, IntPtr pSrc, int count);
     1         static void MemCpyIntPtr()
     2         {
     3             var p = new Point() { x = 200,y = 10};
     4             Console.WriteLine(p.x + " " + p.y);
     5 
     6             var size = Marshal.SizeOf(p);
     7             
     8             IntPtr ptrSrc = Marshal.AllocHGlobal(size);
     9             IntPtr ptrDest = Marshal.AllocHGlobal(size);
    10 
    11             //将结构体Point转换成ptrSrc
    12             Marshal.StructureToPtr(p, ptrSrc, false);
    13 
    14             //memcpy
    15             memcpyi(ptrDest, ptrSrc, size);
    16 
    17             //再转换成结构体
    18             Point p2 = new Point();
    19             //先输出一次进行对比
    20             Console.WriteLine(p2.x + " " + p2.y);
    21 
    22             p2 = (Point)Marshal.PtrToStructure(ptrDest, typeof(Point));
    23             Console.WriteLine(p2.x + " " + p2.y);
    24 
    25         }

    运行结果如下:

    200 10
    0 0
    200 10

    使用指针:

    1         /// <summary>
    2         /// 使用指针
    3         /// </summary>
    4         /// <param name="pDst"></param>
    5         /// <param name="pSrc"></param>
    6         /// <param name="count"></param>
    7         /// <returns></returns>
    8         [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl)]
    9         private static extern unsafe int memcpyp(void* pDst, void* pSrc, int count);
     1         static unsafe void MemCpyPointer()
     2         {
     3             Point p = new Point() { x = 200, y = 10 };
     4             Point p2 = new Point();
     5 
     6             Console.WriteLine(p.x + " " + p.y);
     7             Console.WriteLine(p2.x + " " + p2.y);
     8 
     9             Point* pSrc = &p;
    10             Point* pDest = &p2;
    11 
    12             memcpyp((void*)pDest, (void*)pSrc, sizeof(Point));
    13 
    14             p2 = *pDest;
    15             Console.WriteLine(p2.x + " " + p2.y);
    16         }

    运行结果如下:

    200 10
    0 0
    200 10

    下面介绍使用指针传递时的另外一种情况,这种情况我们可以使用ref来代替指针完成操作。

    先用C++封装一个库,导出如下函数,用来打印一个整形数组

     1 extern "C" __declspec(dllexport) void PrintArray(int* pa,int size);
     2 
     3 
     4 extern "C" __declspec(dllexport) void PrintArray(int* pa,int size)
     5 {
     6     for (size_t i = 0; i < size; i++)
     7     {
     8         std::cout << *pa << std::endl;
     9         pa++;
    10     }
    11 }

    使用ref:

    1 [DllImport("demo_lib.dll",EntryPoint = "PrintArray")]
    2 private static extern void PrintArrayRef(ref int pa,int size);
    1         static void PrintArrayRef()
    2         {
    3             int[] array = new int[] { 1,2,3};
    4 
    5             //使用ref关键字传的是引用,ref[0]其实就是传的首地址
    6             PrintArrayRef(ref array[0], array.Length);
    7         }

    运行结果:

    1
    2
    3

    使用指针:

    1         [DllImport("demo_lib.dll", EntryPoint = "PrintArray")]
    2         private static extern unsafe void PrintArrayPointer(int* pa, int size);
     1         static unsafe void PrintArrayPointer()
     2         {
     3             int size = 3;
     4             int* array = stackalloc int[3];
     5 
     6             for (int i = 0; i < size; i++)
     7             {
     8                 array[i] = i+1;
     9             }
    10             PrintArrayPointer(array, size);
    11         }

    运行结果:

    1
    2
    3

    以上的示例程序可以在这里下载

    参考资料:

    https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/language-specification/unsafe-code#pointer-types

    https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/unsafe-code-pointers/

  • 相关阅读:
    关于MATLAB处理大数据坐标文件2017628
    回溯算法
    [leetcode] 046. Permutations 解题报告
    [leetcode] 226. Invert Binary Tree 解题报告
    [leetcode] 121. Best Time to Buy and Sell Stock 解题报告
    [leetcode] 112. Path Sum 解题报告
    [leetcode] 190. Reverse Bits 解题报告
    [leetcode] 189. Rotate Array 解题报告
    [leetcode] 100. Same Tree 解题报告
    [leetcode] 88. Merge Sorted Array 解题报告
  • 原文地址:https://www.cnblogs.com/zhaotianff/p/12918533.html
Copyright © 2020-2023  润新知