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
以上的示例程序可以在这里下载
参考资料:
Unsafe code - C# language specification | Microsoft Learn
不安全代码、数据指针和函数指针 - C# reference | Microsoft Learn