五、不安全的代码

本文详细介绍了C#中指针的使用,包括使用unsafe关键字编写不安全代码,指针的声明与操作,如取地址、间接寻址运算符,以及指针算术。此外,还讨论了指针类型转换、void指针、指针算术运算、sizeof运算符、结构指针和类成员指针。指针的使用可以用于调用需要指针的API函数,但也带来了潜在的安全风险和复杂性,因此应谨慎使用。
摘要由CSDN通过智能技术生成

如前所述,C#非常擅长于对开发人员隐藏大部分基本内存管理,因为它使用了垃圾收集器和引用。但是,有时需要直接访问内存。例如,由于性能问题,要在外部(非.NET环境)的DLL中访问一个函数,该函数需要把一个指针当作参数来传递(许多Windows API函数就是这样)。本节将讨论C#直接访问内存的内容的功能。

1. 用指针直接访问内存

下面把指针当作一个新论题来介绍,而实际上,指针并不是新东西。因为在代码中可以自由使用引用,而引用就是一个类型安全的指针。前面已经介绍了表示对象和数组的变量实际上存储相应数据(被引用者)的内存地址。指针只是一个以与引用相同的方式存储地址的变量。其区别是C#不允许直接访问在引用变量中包含的地址。有了引用后,从语法上看,变量就可以存储引用的实际内容。

C#引用主要用于使C#语言易于使用,防止用户无意中执行某些破坏内存中内容的操作。另一方面,使用指针,就可以访问实际的内存地址,执行新类型的操作。例如,给地址加上4个字节,就可以查看甚至修改存储新地址中的数据。

下面是使用指针的两个主要原因:

  • 向后兼容性——尽管.NET运行库提供了许多工具,但仍可以调用本地的Windows API函数。对于某些操作,这可能是完成任务的唯一方式。这些API函数都是用C++或C#语言编写的,通常要求把指针作为其参数。但在许多情况下,还可以使用DllImport声明,以避免使用指针,例如,使用System.IntPtr类。
  • 性能——在一些情况下,速度是最重要的,而指针可以提供最优性能。假定用户知道自己在做什么,就可以确保以最高效的方式访问或处理数据。但是,注意在代码的其他区域中,不使用指针,也可以对性能进行必要的改进。请使用代码配置文件,查找代码中的瓶颈,Visual Studio中就包含一个代码配置文件

但是,这种低级的内存访问也是有代价的。使用指针的语法比引用类型的语法更复杂。而且,指针使用起来比较困难,需要非常高的编程技巧和很强的能力,仔细考虑代码所完成的逻辑操作,才能成功地使用指针。如果不仔细,使用指针就很容易在程序中引入细微的、难以查找的错误。例如,很容易重写其他变量,导致栈溢出,访问某些没有存储变量的内存区域,甚至重写.NET运行库所需要的代码信息,因而使程序崩溃。

尽管有这些问题,但指针在编写高效的代码时是一种非常强大和灵活的工具。

注意:

这里强烈建议不要轻易使用指针,否则代码不仅难以编写和调试,而且无法通过CLR施加的内存类型安全检查。

(1) 用unsafe关键字编写不安全的代码

因为使用指针会带来相关的风险,所以C#只允许在特别标记的代码块中使用指针。标记代码所用的关键字是unsafe。下面的代码把一个方法标记为unsafe:

            unsafe int GetSomeNumber()
            {
                //code that can use pointers
            }

任何方法都可以标记为unsafe——无论该方法是否应用了其他修饰符(例如,静态方法、虚方法等)。在这种方法中,unsafe修饰符还会应用到方法的参数上,允许把指针用作参数。还可以把整个类或结构标记为unsafe,这表示假设所有的成员都是不安全的:

    unsafe class MyClass
    {
        //any method in this class can now use pointers
    }

同样,可以把成员标记为unsafe:

    class MyClass2
    {
        unsafe int* pX;//declaration of a pointer field in a class
    }

也可以把方法中的一块代码标记为unsafe:

        //code that doesn't use pointers
        void MyMethod()
        {
            unsafe
            {
                //unsafe code that uses pointers here
            }
            //more 'safe' code that doesn't use pointers
        }

但要注意,不能把局部变量本身标记为unsafe:

        int MyMtthod()
        {
            unsafe int* pX;//Wrong
        }

如果要使用不安全的局部变量,就需要在不安全的方法或语句块中声明和使用它。在使用指针前还有一步要完成。C#编译器会拒绝不安全的代码,除非告诉编译器代码包含不安全的代码块。可以通过设置csproj项目文件的AllowUnsafeBlocks,如下所示,或者在Visual Studio Build Project Properties 设置中选择Allow Unsafe Code复选框,配置不安全的代码:

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

(2) 指针的语法

把代码块标记为unsafe后,就可以使用下面的语法声明指针:

                //unsafe code that uses pointers here
                int* pWidth,pHeight;
                double* pRessult;
                byte*[] pFlags;

这段代码声明了4个变量,pWidth和pHeight是整数指针,pResult是double型指针,pFlags是字节型的数组指针。我们常常在指针变量名的前面使用前缀p来表示这些变量是指针。在变量声明中,符号*表示声明一个指针,换言之,就是存储特定类型的变量的地址。

声明了指针类型的变量后,就可以用与一般变量相同的方式使用它们,但首先需要学习另外两个运算符:

  • &表示"取地址",并把一个值数据类型转换为指针,例如,int转换为int*。这个运算符称为寻址运算符。
  • *表示"获取地址的内容",把一个指针转换为值数据类型(例如,float*转换为float)。这个运算符称为"间接寻址运算符"(有时称为""取消引用运算符)。

从这些定义中可以看出,&和*的作用是相反的。

注意:

符号&和*也表示按位AND(&)和乘法(*)运算符,为什么还可以以这种方式使用它们?答案是在实际使用时它们是不会混淆的,用户和编译器总是知道在什么情况下这两个符号有什么含义,因为按照指针的定义,这些符号总是以一元运算符的形式出现——它们只作用于一个变量,并出现在代码中该变量的前面。另一方面,按位AND和乘法运算符是二元运算符,它们需要两个操作数。

下面的代码说明了如何使用这些运算符:

                int x = 10;
                int* pX,pY;
                pX = &x;
                pY = pX;
                *pY = 20;

首先声明一个整数x,其值是10。接着声明两个整数指针pX和pY。然后把pX设置为指向x(换言之,把pX的内容设置为x的地址)。然后把pX的值赋予pY,所以pY也指向x。最后,在语句*pY = 20中,把值20赋予pY指向的地址包含的内容。实际上是把x的内容改为20,因为pY指向x。注意在这里,变量pY和x之间没有任何关系。只是此时pY碰巧指向存储x的存储单元而已。

要进一步理解这个过程,假定x存储在栈的存储单元0x12F8C4~0x12F8C7中(十进制就是1243332~1243335),即有4个存储单元,因为一个int占用4个字节)。因为栈向下分配内存,所以变量pX存储在0x12F8C0~0x12F8C3的位置上,pY存储在0x12F8BC~0x12F8BF的位置上。注意,pX和pY也分别占用4个字节。这不是因为一个int占用4个字节,而是因为在32位处理器上,需要用4个字节存储一个地址(在64位处理器上,需要用8个字节存储一个地址)。利用这些地址,在执行完上述代码后,栈应如图17-5所示。

注意:

 这个示例使用int说明该过程,其中int存储在32位处理器中栈的连续空间上,但并不是所有的数据类型都会存储在连续的空间中。原因是32位处理器最擅长于在4个字节的内存块中检索数据。这种计算机上的内存会分解为4个字节的块,在Windows上,每个块有时称为DWORD,因为这是32位无符号int数在.NET出现之前的名字。这是从内存中获取DWORD的最高效的方式——跨越DWORD边界存储数据通常会降低硬件的性能。因此,.NET运行库通常会给某些数据类型填充一些空间,使它们占用的内存是4的倍数。例如,short数据占用两个字节,但如果把一个short放在栈中,栈指针仍会向下移动4个字节,而不是两个字节,这样,下一个存储在栈中的变量就仍从DWORD的边界开始存储。

可以把指针声明为任意一种类型——即任何预定义的类型uint、int和byte等,也可以声明为一个结构。但是不能把指针声明为一个类或数组,因为这么做会使垃圾收集器出现问题。为了正常工作,垃圾收集器需要知道在堆上创建了什么类的实例,它们在什么地方。但如果代码开始使用指针处理类,就很容易破坏堆中.NET运行库为垃圾收集器维护的与类相关的信息。在这里,垃圾收集器可以访问的任何数据类型称为托管类型,而指针只能声明为非托管类型,因为垃圾收集器不能处理它们。

(3) 将指针强制转换为整数类型

由于指针实际上存储了一个表示地址的整数,因此任何指针中的地址都可以和任何整数类型之间相互转换。指针到整数类型的转换必须是显示指定的,隐式的转换是不允许的。例如,编写下面的代码是合法的:

                int x = 10;
                int* pX,pY;
                pX = &x;
                pY = pX;
                *pY = 20;
                ulong y = (ulong)pX;
                int* pD = (int*)y;

把指针pX中包含的地址强制转换为一个ulong,存储在变量y中。接着把y强制转换回一个int*,存储在新变量pD中。因此p

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值