C#指针和不安全代码

C#指针和不安全代码

一、引言

C#非常擅长对开发人员隐藏大部分的基本内存管理,因为它使用了垃圾回收器和引用。但是有时我们需要对一些内存进行访问那么该怎么办?我们可以用C#中的指针。

C#中也是有指针。指针的操作效率虽然高但是不安全。因为我们可以通过地址的操作来修改内存中的一些数据,而这些内存可能被别的程序使用。这样就有可能造成一定的隐患。

例如:
int i =100;
int* pt=&i; //将i的地址取出赋值给pt指针。
*(pt+1)=100; //将pt指针的地址加上1后,将此地址的内容赋值为100.
问题就在第三段,因为pt原先是指向i的,i是程序申请的变量空间,所有合法,但是将pt+1的执行的内存不一定是合法的,所以赋值100有可能会出现错误。

也正是因为指针的不安全性,所以在C#中不经常用指针。而是用引用来代替指针。
C#的引用其实就是一个类型安全的指针。

既然指针不安全为什么还要使用它呢?
1、向后兼容性。
许多的外部工具或其他语言编写的Dll中很多都会用到以指针作为参数来传递。为了适应这些要求,有时不得不用到指针。
2、性能
不得不说,由于指针是直接指向内存的,所以它的效率非常的高。所以在对性能、速度要求很高的场合可以考虑用指针。当然如果有更安全的解决方法,就尽量不使用指针。

二、unsafe关键字

使用指针需要注意什么?
因为指针是不安全的,所以想要使用指针就得使用一个关键字
unsafe
C#只允许在特别标记的代码块中使用指针。这种标记就是unsafe。

1、不安全方法

unsafe int GetSomeNumber()
{
//代码体
}

2、不安全类

unsafe class MyClass
{
//类体
}

3、类中不安全成员

class MyClass
{
unsafe int*pX; //类中不安全指针
}

4、不安全代码块

void MyMethod()
{
unsafe int *pX; //错误不能把局部变量本身标记成unsafe
unsafe
{
//不安全代码块
}
//其余没用到指针的就是安全代码
}

如果想要使用不安全的局部变量,要么将之放在不安全方法中或者不安全类中,要么将之放在不安全代码块中。

三、指针的使用

如下所示:
int * pWidth,pHeight; //定义两个指针变量,分别指向不同的int型4字节的空间
double* pResult; //定义一个指针变量,指向1个8字节double空间

一般情况我们在变量的前面添加p来表示他是个指针,这样有助良好代码风格的养成。

**注意上面的int * pWidth,pHeight;这种声明方式和C++不同,C++/C中声明是int pWidth,pHeight;所以C#和C++的声明有一点不同。

1、示例

下面的代码进一步补充一些操作符
int x=10; //声明变量
int * pX,pY; //声明指针变量
pX=&x; //获取x变量的地址
pY=pX; //将pX的值赋值给pY
*pY=20; //修改pY指向内存的值
以上所有的变量都是都是存储在栈中的,没有存在托管堆中的数据。
指针在栈中实际上就是一个4节的数据,只不过此数据恰好就是指向某个内存字节的地址而已。
例如pX里面的值是123455的话,那表示它指向的字节地址就是123455。

重点要说明一点,C#不能把指针声明为一个类或数组,这样做会使垃圾回收器出现问题。

2、将指针强制转换为整数类型

上面我们提到过,指针实际上就是存储了一个表示地址的整数而已。所以这个整数可以转换为普通的整数或其他任意整数类型。

例如:
int x=10; //声明变量
int * pX,pY; //声明指针变量
pX=&x; //获取x变量的地址
pY=pX; //将pX的值赋值给pY
pY=20; //修改pY指向内存的值
uint y=(uint)pX; //可以转换成普通整型数据
int
pD=(int*)y; //还可以将普通整型数据转换为指针

3、指针类型之间的强制转换

byte* aByte=8;
byte* pByte=&aByte;
double pDouble=(double)pByte; //将byte类型的指针转换为double指针

上面的代码虽然合法但也是非常的不安全,因为byte的指针指向的是1个字节的空间,而一旦转换成double类型的指针类型,就会使之指向8个字节的空间,其中只有一个字节是合法的,其余七个字节的地址都是非法的。

4、void*指针

如果想要维护一个指针但是不希望指定它指向的数据类型,就可以吧指针声明为void:
int *pointerToInt;
void pointerToVoid;
pointterToVoid=(void
)pointerToInt;

那么问题是,为什么要用void类型的指针,它用在什么地方?
它的主要用途就是调用需要void参数的Api函数上。一般情况下C#使用void指针的情况还不是很多。

5、指针的运算

可以通过指针来访问内存,那么同样的可以将指针进行加减来进行。
可以将指针加减整数,这样指针的值就会发生变化,但是这种变化与指针的类型有一定的关系,而不是仅仅的加减数字的关系。

例如:
uint u=3;
byte b=8;
double d=10.0;
uint pU=&u; //定义uint类型指针,其指向的为4个字节的空间
byte
pB=&b; // 定义byte类型的指针,其指向的为1个字节的空间
double*pD=&d; //定义double类型的指针,其指向8个字节的空间

假设上面的指针中的地址为指针里面存的是起始地址:
pU:1233336
pB:1243325
pD:1243352

如果将每个指针都加上2的话,那么pU、pB、pD的值会是怎样的呢。
最终三个指针变量的值会变成:
pU:1233344 //1233336+24 不是1233338
pB:1243327 //1243325+2
1
pD:1243352 //1243368+2*8 不是1243354

为什么指针加2结果并非是简单的加2呢?
归根结底还是和指针的指向的内存字节数有关,因为int指针指向的4字节,所以它加上2其实是加上2*4=8。这样做非常合理,不会占用已分配的空间。

6、结构指针

当然结构体也可以使用指针的,结构指针的工作方式和值类型的指针的工作方式完全相同。但是这有一个条件,结构体内部不能包含引用类型,否则会导致垃圾处理器出现错误。

struct myStruct
{
public long x;
public float f;
}

就可以给他定义一个指针
myStruct *pStruct;
然后对其进行初始化:
myStruct Struct=new myStruct();
pStruct=&Struct;
也可以通过指针访问结构的成员值:
(*pStruct).X=4;
(*pStruct).Y=3.4f;
但是这个语法有点复杂。因此C#定义了另一个运算符,用一种比较简单的语法,通过指针访问结构的成员那就是->

例如:
pStruct->X=4;
pStruct->Y=3.4f;

7、类成员指针

前面说过,指针不能引用在类中,因为类是引用类型。但是它可以通过fixed关键字来告诉垃圾回收器,可能有引用某些对象的成员的指针,所以这些实例不能移动。

class myClass
{
public long x;
public float f;
}

myClass myObject=new myClass();
long pL=&(myObject.x); //出错
float
pF=&(myObject.f); //出错

尽管x和f都是非托管类型,但是他们嵌入在对象中,所以他们存储在托管堆上。
未解决这个问题,引出一个新的关键字fixed。

myClass myObjec=new myClass();
fixed(long pLt=&(myObject.x))
{
//代码体
}
fixed (float
pF=&(myObject.y))
{
//代码体
}

8、使用指针优化性能

数组我们都熟悉。在C#中数组其实是个对象,他们都是System.Array类的实例。它保存在托管堆中。这自然就会增加系统的开销。那么有没有办法想C语言那样将数组保存在栈中,这样操作起来的效率就会高的多呢?

当然有。要想使数组保存在栈中,必须使用关键字stackalloc。次关键字指示.NET运行库在栈上分配一定量的内存。在调用stackalloc命令时,需要为它提供两条信息:
1、要存储的数据类型
2、需要从存储的数据项数目

举例:
decimal *pDecimal=stackalloc decimal[20];
上面的示例指定了数据类型为decimal,数目为20。这条指令只是分配栈内存。它不会试图把内存初始化为任何默认值,这正好符合我们的目的。因为要创建一个高性能的数组,给它不必要地初始化相应值会降低性能。

发布了50 篇原创文章 · 获赞 0 · 访问量 851

猜你喜欢

转载自blog.csdn.net/weixin_40786497/article/details/104182471