18.C#基础之不安全代码(完成)

     前面有写到过,核心C#语言没有将指针引入它所支持的数据类型,从而与C和C++有着显著的区别。作为代替,C#提供了各种引用类型,并能创建可由垃圾回收器管理的对象。这就使得C#比C或C++安全的多。

     在核心C#语言中,干脆就不可能有未初始化的变量、"虚"、超过数组边界对其进行索引的表达式,这样C和C++的一系列错误就不会发生在C#中。但尽管如此,但仍有一些场合需要指针类型。例如与底层操作系统进行交互、访问内存映射设备,或实现一些以时间为关键的算法时,没有指针就很难完成,为了应对这种情况,C#提供了编写不安全代码的能力。

     在不安全代码中,可以声明和操作指针,可以在指针和整型间执行转换,还可以获取变量的地址等,从某种意义上说,编写不安全代码就像在C#程序中编写C代码。虽然它的名字时不安全代码,但无论从开发人员还是用户角度,不安全代码事实上都是一种"安全"功能。

     不安全代码必须用修饰符unsafe明确标记。

  18.1不安全上下文

     C#的不安全功能仅用于不安全上下文,不安全上下文是通过在类型或成员的声明中包含一个unsafe修饰符或通过使用不安全代码引入:

       *在类、结构、接口或委托的声明可以包含unsafe修饰符,在这种情况,该类型声明的整个文本范围被认为是不安全上下文;

       *字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数的声明包含unsafe,即该成员在整个文本范围是不安全上下文;

       *不安全语句使得可以在块内使用不安全上下文,该语句关联的块的整个文本范围被认为是不安全上下文。

     上面就是在结构声明中指定unsafe,所以可以使用指针类型。还可以这么写:

     这样字段被认为是不安全上下文。unsafe除了建立不安全上下文从而使用指针类型外,对类型和成员没有其他影响。比如:

     A类使用unsafe修饰符,B类继承重写F时,不用重新指定unsafe修饰符。除非B中的F方法本身需要访问不安全功能。但指针是方法签名的一部分时:

  18.2指针类型

     在不安全上下文的类型有三种:

     指针类型中,在"*"前面指定的类型称为该指针类型的目标类型,他表示该指针类型的值所指向的变量的类型。

     与引用不同,指针不受垃圾回收器跟踪,所以指针不能指向或包含引用结构;但引用的目标可以包含指针!

     非托管类型是任何不是引用的类型,并且在任何嵌套级别都不包含引用类字段的类型。具体就是:

     这里有一些指针类型的示例:

     与C和C++不同,在C#中,当在同一声明中声明多个指针时,"*"只与基础类型写在一起,例如:int* pi,pj;    //与int *pi,*pj不同

     类型为T*的一个指针的值:表示类型为T的一个变量的地址。指针间接寻址运算符*可用于访问此变量。

     和引用类型,指针也可以是null。如果间接寻址运算符应用于null指针,则行为将由实现自己定义。

     void*类型表示指向位置类型的指针。由于目标类型是未知的,所以间接寻址运算符不能应用于void*类型的指针,也不能对这样的指针执行任何算术运算。但void*类型的指针可以强制转换为任何其他指针类型,反之亦然。

     指针类型是个单独类别的类型,不是从object类继承的,所以不支持装箱和拆箱操作;但允许不同指针之间及指针类型与整型之间进行转换。

     指针类型也可以用做易失的类型。

     虽然指针可以作为ref或out参数,但这样做可能会导致未定义行为。以为指针可能被设置为指向一个局部变量,当调用方法返回时,该局部变量可能已不存在;又或者指针曾指向一个固定对象,但调用方法返回时,该对象不再是固定的了。

     在不安全上下文,可以使用以下几种构造函数指针:

  18.3固定变量和可移动变量

     address-of运算符和fixed语句将变量划分成两个类别:固定变量、可移动变量。

        固定变量:驻留在不受垃圾回收器操作影响的存储位置中(示例包括局部变量、值参数、由取消指针引用而创建的变量)

        可移动变量:驻留在会被垃圾回收器重定位或处置的存储位置中(示例包括对象中的字段、数组的元素)

     &运算符允许不受限制地获取固定变量的地址;而可移动变量会受垃圾回收器的重定位或处置,所以只能用fixed获取,且该地址只在此fixed语句的生存期内有效。

     准确地说,固定变量是下列之一:

     注:静态字段属于可移动变量。

  18.4指针转换

     在不安全上下文中,可供使用的隐式转换的集合也扩展为包括以下隐式指针转换:

     在不安全上下文中,可供使用的隐式转换的集合也扩展为包括以下隐式指针转换:

     当一个指针被转换为另一个指针类型时,如果没有将得到的指针正确地对指向类型对齐,则当结果被取消引用时,该行为将是未定义的。一般情况下,"正确对象"的概念是可传递的;即指向类型A的指针被正确地与指向B的指针对齐,而指向类型B的指针又被正确的与指向类型C的指针对齐,那么指向A的指针与C的指针对齐。

     当一个指针类型被转换为指向字节的指针时,转换后的指针将指向原来所指变量的地址中的最低寻址字节。连续增加该变换后的指针,将产生指向该变量的其他字节的指针。

  18.5表达式中的指针

     在不安全上下文中,表达式可能产生指针类型的结果。但在不安全上下文意外,表达式为指针类型会导致编译时错误。在不安全上下文中,非数组创建基本表达式和一元表达式产生式允许使用下列附加构造:

   18.5.1 指针间接寻址

     一元*运算符:表示指针间接寻址并且用于获取指针所指向的变量。计算*p得到的结果(其中p为指针类型T*的表达式)是类型T的一个变量。

   18.5.2 指针成员访问

     在P->I形式的指针成员访问中,P必须是除void*意外的某个指针类型的表达式,而I必须表示P所指向的类型的可访问成员。P->I形式的指针成员访问的计算方式和(*P).I完全相同。比如:

     所以可以这么写:

   18.5.3 指针元素访问

     在P[E]形式的指针元素访问中,P必须是除void*意外的指针类型的表达式,而E则必须是在可以隐式转换为int,uint,long,ulong类型的表达式。P[E]形式的指针元素访问的计算方式和*(P+E)完全相同。比如:

     在for循环中初始化字符缓冲区就可以这么写:

     指针元素访问运算符不能检查是否发生访问越界错误,而且当访问超出边界的元素时,行为是未定义的,这和C、C++相同。

   18.5.4 address-of运算符

     如果给定类型为T,且属于固定变量的表达式E,构造&E将计算由E给出的变量的地址。计算的结果是一个类型为T*的值;

                      如果E属于易失字段/可移动的变量,则会发生编译时错误,在可移动变量时,可以用固定语句临时"固定"再该变量取地址。

     &运算符不要求它的参数明确赋值,但在执行&操作后,该运算符所应用于的那个变量在此操作发生的执行路径中被认为是已经明确赋值的。&运算符的明确赋值规则可以避免局部变量的冗余初始化。例如外部API要求获取指向结构的指针,而由此API来填充该结构,对此类API进行的调用通常会传递局部结构变量的地址。

  18.5.5 指针增加和指针减少

     如果指针递增/递减运算的结果超过指针类型的域,则结果是由实现定义的,但不会产生异常。

   18.5.6 指针算法

     如果给定指针类型T*的表达式和类型int、uint、long、ulong的表达式N,表达式P+N,P-N的结果是一个属于类型T*的指针值;

     如果给定指针类型T*的两个表达式P和Q,表达式P-Q将先计算P和Q给出的地址之间的差。然后用sizeof(T)去除这个差,计算结果类型始终是long。

   18.5.7 指针比较

     由于存在从任何指针类型到void*类型的隐式转换,所以可以使用这些运算符来比较任何指针类型的操作数。

   18.5.8 sizeof运算符

     sizeof运算符返回给定类型的变量占用的字节数。被指定未sizeof的操作数的类型必须是"非托管类型"。

     sizeof运算符的结果是int类型的值。

     对于所有其他类型,sizeof运算符的结果是由实现来定义的,并且属于值而不是常数。当sizeof应用于具有结构类型的操作数时,结果时该类型变量所占的字节总数。

  18.6固定语句

     在不安去上下文中,嵌入语句产生式允许使用一个附加结构即fixed语句,该语句用于固定可移动变量,从而使该变量的地址在语句的持续时间内保持不变。

     如上,每个固定指针声明符声明一个给定指针类型的局部变量,并使用由相应的固定指针初始值设定项计算的地址初始化该局部变量。在fixed语句中声明的局部变量的可访问范围仅限于在该变量声明右边的所有固定指针初始值设定项中,和在该fixed语句的嵌入语句中。由fixed语句声明的局部变量被设为只读,如果嵌入语句试图修改此局部变量或将它作为ref/out参数传递都会发生编译时错误。

     固定指针初始值设定项可以是:

            1."&"标记,后加一个变量引用,它引用非托管类型T的可移动变量,前提是类型T*可以隐式转换为fixed语句中给出的指针类型。在这种情况,初始值设定项将计算给定变量的地址,而fixed语句在生存期内将保证该变量的地址不变;

            2.元素类型为非托管类型T的数组类型的表达式,前提是类型T*可以隐式转换为fixed语句中给出的指针类型。在这种情况,初始值设定项将计算数组中第一个元素的地址,而fixed语句在生存期内保证整个数组的地址保持不变;如果数组表达式为null或数组具有零个元素,则fixed语句的行为由实现来定义;

            3.string类型的表达式,前提是类型char*可以隐式转换为fixed语句中给出的指针类型。在这种情况,初始值设定项将计算字符串中第一个字符的地址,而fixed语句在生存期内将保证整个字符串的地址不变;如果字符串表达式为null,则fixed语句的行为是实现定义。

     对于每个由固定指针初始值设定项计算的地址,fixed语句确保由该地址引用的变量在fixed语句的生存期内不会被垃圾回收器重定位或处理。

     固定对象可能导致堆中产生存储碎片,处于这个原因,只有在绝对必要时才应当固定对象,而且固定的时间越短越好。

     上面第一句语句固定获取一个静态字段的地址;第二个获取一个实例字段的地址;第三局获取一个数组元素的地址;这几种情况,直接使用常规&运算符都是错误的,这是因为这些变量都属于可移动变量。

     第四句和第三句产生的结果是相同的。一般情况,对于数组实例a,在fixed语句中指定&a[0]与只指定a等效。

     接下来使用string:

  18.7堆栈分配

     在不安全上下文中方,局部变量声明可以包含一个从调用堆栈中分配内存的堆栈分配初始值设定项。

     在这产生式,非托管类型标识将在新分配的位置中存储的项的类型,而表达式则标识这些项的数目。合在一起,它们指定所需的分配大小。堆栈分配的大小不能为负,不然会导致编译时错误。

     stackallocT[E]形式的堆栈分配初始值设定项要求:T必须为非托管类型,E必须是int类型的表达式。该构造从调用堆栈中分配E*sizeof(T)个字节,并返回一个指向新分配的块的、类型T*的指针;如果E为负值,则其行为是未定义;如果为零,则不进行任何分配,并且返回的指针由实现来定义。如果没有足够的内存分配给定大小的块,则拖出System.StackOverflowException。

     新分配的内存的内容是未定义的。

     在catch或finally块中不允许使用堆栈分配初始值设定项。

     无法显式释放利用stackalloc分配的内存。在函数成员的执行期间创建的所有堆栈分配内存块都在该函数成员返回时自动丢弃。

     上面这是在IntToString方法中使用了stackalloc初始值设定项,以在堆栈上分配一个16个字符的缓冲区,此缓冲区在该方法返回时被自动丢弃。

  18.8动态内存分配

     除stackalloc运算符外,C#不提供其他预定义构造来管理那些不受垃圾回收器控制的内存。这些服务通常是由支持类库提供或直接从底层操作系统导入的。

     这是个Memory类的示例,此示例通过Memory.Alloc分配了256字节的内存,并且使用从0增加到255的值初始化该内存块。它随后分配一个具有256个元素的字节数组并使用Memmory.Copy将内存块的内容复制到此字节数组中;最后使用Free释放内存块,并将字节数组的内容输出到控制台上。

posted @ 2018-12-22 12:31  狂风过境的风  阅读(792)  评论(0编辑  收藏  举报