.net平台定义了两种主要数据类型:值类型和引用类型,其实还有第三种数据类型:指针类型。使用指针,可以绕开clr的内存管理机制。(说明:在c#中使用指针,需要有相关c/c++指针操作基础)
1、c#中指针相关的操作符和关键字
| 操作符/关键字 | 作用 |
| * | 该操作符用于创建一个指针变量,和在c/c++中一样。也可用于指针间接寻址(解除引用) |
| & | 该操作符用于获取内存中变量的地址 |
| -> | 该操作符用于访问一个由指针表示的类型的字段,和在c++中一样 |
| [] | 在不安全的上下文中,[]操作符允许我们索引由指针变量指向的位置 |
| ++,-- | 在不安全的上下文中,递增和递减操作符可用于指针类型 |
| +,- | 在不安全的上下文中,加减操作符可用于指针类型 |
| ==, !=, <, >, <=, >= | 在不安全的上下文中,比较和相等操作符可用于指针类型 |
| stackalloc | 在不安全的上下文中,stackalloc关键字可用于直接在栈上分配c#数组,类似crt中的_alloca函数 |
| fixed | 在不安全的上下文中,fixed关键字可用于临时固定一个变量以使它的地址可被找到 |
2、在c#中使用指针,需要启用“允许不安全代码”设置
选择项目属性->生成,钩上“允许不安全代码”

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

除了声明代码块为不安全代码外,也可以直接构建“不安全的”结构、类型成员和函数。
unsafe struct point
{
public int x;
public int y;
public point* next;
public point* previous;
}
unsafe static void calcpoint(point* point)
{
//
}
也可以在导入非托管 dll 的函数声明中使用unsafe
[dllimport("msvcrt.dll", callingconvention = callingconvention.cdecl)]
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;
}
} //换成类
class point
{
public int x;
public int y;
}
4、*和&操作符
在不安全的上下文中,可以使用 * 操作符构建数据类型相对应的指针类型(指针类型、值类型和引用类型,示例代码中的type),使用 & 操作符获取被指向的内存地址。
type* identifier; void* identifier; //允许但不推荐
下面是使用*操作符进行指针类型声明
int* p | p 是指向整数的指针。 |
int** p | p 是指向整数的指针的指针。 |
int*[] p | p 是指向整数的指针的一维数组。 |
char* p | p 是指向字符的指针。 |
void* p | p 是指向未知类型的指针。 |
注意:
1、无法对 void* 类型的指针应用间接寻址运算符。 但是,你可以使用强制转换将 void 指针转换为任何其他指针类型,反过来也是可以的。
2、指针类型不从object类继承,并且指针类型与 object 之间不存在转换。 此外,装箱和取消装箱不支持指针。
下面的代码演示了如何声明指针类型:
static void main(string[] args)
{
int []a = { 1, 2, 3, 4, 4 };
unsafe
{
//临时固定一个变量以使它的地址可被找到
fixed (int* p = &a[0])
{
int* p2 = p;
console.writeline(*p2);
p2++;
console.writeline(*p2);
p2++;
console.writeline(*p2);
}
}
}输出结果如下:
1
2
3
下面的代码演示了如何使用指针类型进行数据交换:
static void main(string[] args)
{
int a = 1;
int b = 2;
unsafe
{
unsafeswap(&a, &b);
}
console.writeline(a);
console.writeline(b);
}
/// <summary>
/// 使用指针
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
static unsafe void unsafeswap(int* a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
/// <summary>
/// 不使用指针的安全版本
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
static void safeswap(ref int a,ref int b)
{
int temp = a;
a = b;
b = temp;
}输出结果如下:
2
1
5、通过指针访问字段
定义如下结构体
struct point
{
public int x;
public int y;
public override string tostring()
{
return $"x:{x},y:{y}";
}
}如果声明一个point类型的指针,就需要使用指针字段访问操作符(->)来访问公共成员(和c++一样),也可以使用指针间接寻址操作符(*)来解除指针的引用,使其也可以使用 (.)操作符访问字段(和c++一样)。
static unsafe void main(string[] args)
{
//通过指针访问成员
point point = new point();
point* p = &point;
p->x = 10;
p->y = 5;
console.writeline(p->tostring());
//通过指针间接寻址访问成员
point point2; //不使用 new 运算符的情况下对其进行实例化,需要在首次使用实例之前必须初始化所有实例字段。
point* p2 = &point2;
(*p2).x = 128;
(*p2).y = 256;
console.writeline((*p2).tostring());
}运行结果如下:
x:10,y:5
x:128,y:256
6、stackalloc关键字
在不安全上下文中,可能需要声明一个直接从调用栈分配内存的本地变量(不受.net垃圾回收器控制)。c#提供了与crt函数_alloca等效的stackalloc关键字来满足这个需求。
static unsafe void main(string[] args)
{
char* p = stackalloc char[3];
for (int i = 0; i < 3; i++)
{
p[i] = (char)(i+65); //a-c
}
console.writeline(*p);
console.writeline(p[0]);
console.writeline(*(++p));
console.writeline(p[0]);
console.writeline(*(++p));
console.writeline(p[0]);
}输出结果如下:
a
a
b
b
c
c
7、fixed关键字
在上面的示例中,我们可以看到,通过stackalloc关键字,在不安全上下文中分配一大块内存非常方便。但这块内存是在栈上的,当分配方法返回的时候,被分配的内存立即被清理。
假设有如下情况:
声明一个引用类型pointref和一个值类型point
class pointref
{
public int x;
public int y;
public override string tostring()
{
return $"x:{x},y:{y}";
}
}
struct point
{
public int x;
public int y;
public override string tostring()
{
return $"x:{x},y:{y}";
}
}调用者声明了一个pointref类型的变量,内存将被分配在垃圾回收器堆上。如果一个不安全的上下文要与这个对象(或这个堆上的任何对象)交互,就可能会出现问题,因为垃圾回收可随时发生。设想一下,恰好在清理堆的时候访问point成员,这就很
为了将不安全上下文中的引用类型变量固定,c#提供了fixed关键字,fixed语句设置指向托管类型的指针并在代码执行过程中固定该变量。换句说话:fixed关键字可以锁定内存中的引用变量。这样在语句的执行过程中,该变量地址保持不变。
事实上,也只有使用fixed关键字,c#编译器才允许指针指向托管变量。
static unsafe void main(string[] args)
{
pointref pointref = new pointref();
point point = new point();
int a = &pointref.x; //编译不通过
int *b = &point.x; //编译通过
fixed(int *c = &pointref.x)
{
//编译通过
}
}说明:
在fixed中初始化多个变量也是可以的
//同时声明多个指针变量的语法跟c++中的不一样,需要注意
fixed(int *e = &(pointref.x) , f = &(pointref.y) )
{
}8、sizeof关键字
在不安全上下文中,sizeof关键字用于获取值类型(不是引用类型)的字节大小。sizeof可计算任何由system.valuetype派生实体的字节数。
static void main(string[] args)
{
unsafe
{
//不安全版本
console.writeline(sizeof(int));
console.writeline(sizeof(float));
console.writeline(sizeof(point));
}
//安全版本
console.writeline(marshal.sizeof(typeof(int)));
console.writeline(marshal.sizeof(typeof(float)));
console.writeline(marshal.sizeof(typeof(point)));
}9、避免使用指针
事实上在c#中,指针并不是新东西。因为在代码中可以自由使用引用 ,而引用就是一个类型安全的指针。指针只是一个存储地址的变量,这和引用其实是一个原理。引用的主要作用是使c#更易于使用,防止用户无意中执行某些破坏内存中内容的操作。
使用指针后,可以进行低级的内存访问,但这是有代价的,使用指针的语法比引用类型的语法复杂得多,而且指针使用起来也比较困难,需要较高的编程技巧和强力。如果不仔细,就容易在程序中引入细微的,难以查找的错误。另外,如果使用指针,就必须授予代码运行库的代码访问安全机制的高级别信任,否则就不能执行它。
msdn上有如下关于指针的说明:
在公共语言运行时 (clr) 中,不安全代码是指无法验证的代码。 c# 中的不安全代码不一定是危险的;只是 clr 无法验证该代码的安全性。 因此,clr 将仅执行完全信任的程序集中的不安全代码。 如果你使用不安全代码,你应该负责确保代码不会引发安全风险或指针错误。
大多数情况下,可以使用system.intptr或ref关键字来替代指针完成我们想要的操作。
下面使用示例代码说明一下:(仅供演示)
这里还是以memcpy函数为例,假设我有一个point结构的实例,要对这个point进行拷贝。
声明point结构
struct point
{
public int x;
public int y;
}使用system.intptr:
/// <summary>
/// 使用intptr
/// </summary>
/// <param name="pdst"></param>
/// <param name="psrc"></param>
/// <param name="count"></param>
/// <returns></returns>
[dllimport("msvcrt.dll", entrypoint = "memcpy", callingconvention = callingconvention.cdecl)]
private static extern unsafe int memcpyi(intptr pdst, intptr psrc, int count); static void memcpyintptr()
{
var p = new point() { x = 200,y = 10};
console.writeline(p.x + " " + p.y);
var size = marshal.sizeof(p);
intptr ptrsrc = marshal.allochglobal(size);
intptr ptrdest = marshal.allochglobal(size);
//将结构体point转换成ptrsrc
marshal.structuretoptr(p, ptrsrc, false);
//memcpy
memcpyi(ptrdest, ptrsrc, size);
//再转换成结构体
point p2 = new point();
//先输出一次进行对比
console.writeline(p2.x + " " + p2.y);
p2 = (point)marshal.ptrtostructure(ptrdest, typeof(point));
console.writeline(p2.x + " " + p2.y);
}运行结果如下:
200 10
0 0
200 10
使用指针:
/// <summary>
/// 使用指针
/// </summary>
/// <param name="pdst"></param>
/// <param name="psrc"></param>
/// <param name="count"></param>
/// <returns></returns>
[dllimport("msvcrt.dll", entrypoint = "memcpy", callingconvention = callingconvention.cdecl)]
private static extern unsafe int memcpyp(void* pdst, void* psrc, int count); static unsafe void memcpypointer()
{
point p = new point() { x = 200, y = 10 };
point p2 = new point();
console.writeline(p.x + " " + p.y);
console.writeline(p2.x + " " + p2.y);
point* psrc = &p;
point* pdest = &p2;
memcpyp((void*)pdest, (void*)psrc, sizeof(point));
p2 = *pdest;
console.writeline(p2.x + " " + p2.y);
}运行结果如下:
200 10
0 0
200 10
下面介绍使用指针传递时的另外一种情况,这种情况我们可以使用ref来代替指针完成操作。
先用c++封装一个库,导出如下函数,用来打印一个整形数组
extern "c" __declspec(dllexport) void printarray(int* pa,int size);
extern "c" __declspec(dllexport) void printarray(int* pa,int size)
{
for (size_t i = 0; i < size; i++)
{
std::cout << *pa << std::endl;
pa++;
}
}使用ref:
[dllimport("demo_lib.dll",entrypoint = "printarray")]
private static extern void printarrayref(ref int pa,int size); static void printarrayref()
{
int[] array = new int[] { 1,2,3};
//使用ref关键字传的是引用,ref[0]其实就是传的首地址
printarrayref(ref array[0], array.length);
}运行结果:
1
2
3
使用指针:
[dllimport("demo_lib.dll", entrypoint = "printarray")]
private static extern unsafe void printarraypointer(int* pa, int size); static unsafe void printarraypointer()
{
int size = 3;
int* array = stackalloc int[3];
for (int i = 0; i < size; i++)
{
array[i] = i+1;
}
printarraypointer(array, size);
}运行结果:
1
2
3
以上就是在c#中使用指针的示例代码的详细内容,更多关于c#使用指针的资料请关注代码网其它相关文章!
发表评论