C# 语言规范_版本5.0 (第18章 不安全代码)

时间:2021-12-22 17:45:48

1. 不安全代码

**(注:此章对于跨多语言编程开发非常重要,如遇异常无法完成跨语言,建议使用此种方式。)

如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C++ 有着显著的区别。作为替代,C# 提供了各种引用类型,并能够创建可由垃圾回收器管理的对象。这种设计结合其他功能,使 C# 成为比 C 或 C++ 安全得多的语言。在核心 C# 语言中,不可能有未初始化的变量、“虚”指针或者超过数组的界限对其进行索引的表达式。这样,以往总是不断地烦扰 C 和 C++ 程序的一系列错误就不会再出现了。

尽管实际上对 C 或 C++ 中的每种指针类型构造,C# 都设置了与之对应的引用类型,但仍然会有一些场合需要访问指针类型。例如,当需要与基础操作系统进行交互、访问内存映射设备,或实现一些以时间为关键的算法时,若没有访问指针的手段,就不可能或者至少很难完成。为了满足这样的需求,C# 提供了编写不安全代码 (unsafe code) 的能力。

在不安全代码中,可以声明和操作指针,可以在指针和整型之间执行转换,还可以获取变量的地址,等等。在某种意义上,编写不安全代码很像在 C# 程序中编写 C 代码。

无论从开发人员还是从用户角度来看,不安全代码事实上都是一种“安全”功能。不安全代码必须用修饰符 unsafe 明确地标记,这样开发人员就不会误用不安全功能,而执行引擎将确保不会在不受信任的环境中执行不安全代码。

1.1 不安全上下文

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

  • 类、结构、接口或委托的声明可以包含一个 unsafe 修饰符,在这种情况下,该类型声明的整个文本范围(包括类、结构或接口的体)被认为是不安全上下文。
  • 在字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数或静态构造函数的声明中,也可以包含一个 unsafe 修饰符,在这种情况下,该成员声明的整个文本范围被认为是不安全上下文。
  • unsafe-statement 使得可以在 block 内使用不安全上下文。该语句关联的 block 的整个文本范围被认为是不安全上下文。

下面显示了关联的语法扩展。为简洁起见,将使用省略号 (...) 来表示出现在前面章节中的产生式。

class-modifier:
...
unsafe

struct-modifier:
...
unsafe

interface-modifier:
...
unsafe

delegate-modifier:
...
unsafe

field-modifier:
...
unsafe

method-modifier:
...
unsafe

property-modifier:
...
unsafe

event-modifier:
...
unsafe

indexer-modifier:
...
unsafe

operator-modifier:
...
unsafe

constructor-modifier:
...
unsafe

destructor-declaration:
attributesopt   externopt   unsafeopt   ~   identifier  
(   )    destructor-body
attributesopt   unsafeopt   externopt   ~   identifier  
(   )   
destructor-body

static-constructor-modifiers:
externopt   unsafeopt   static
unsafeopt   externopt   static
externopt   static   unsafeopt
unsafeopt   static   externopt
static   externopt   unsafeopt
static   unsafeopt   externopt

embedded-statement:
...
unsafe-statement

unsafe-statement:
unsafe  
block

在下面的示例中

public unsafe
struct Node
{
public int Value;
public Node* Left;
public Node* Right;
}

在结构声明中指定的 unsafe
修饰符导致该结构声明的整个文本范围成为不安全上下文。因此,可以将 Left 和 Right 字段声明为指针类型。上面的示例还可以编写为

public struct
Node
{
public int Value;
public unsafe Node* Left;
public unsafe Node* Right;
}

此处,字段声明中的 unsafe
修饰符导致这些声明被认为是不安全上下文。

除了建立不安全上下文从而允许使用指针类型外,unsafe 修饰符对类型或成员没有影响。在下面的示例中

public class
A
{
public unsafe virtual void F() {
     char* p;
     ...
}
}

public class
B: A
{
public override void F() {
     base.F();
     ...
}
}

A 中 F 方法上的 unsafe 修饰符直接导致 F 的文本范围成为不安全上下文并可以在其中使用语言的不安全功能。在 B 中对 F 的重写中,不需要重新指定 unsafe
修饰符,除非 B 中的 F 方法本身需要访问不安全功能。

当指针类型是方法签名的一部分时,情况略有不同

public unsafe
class A
{
public virtual void F(char* p) {...}
}

public class
B: A
{
public unsafe override void F(char* p)
{...}
}

此处,由于 F 的签名包括指针类型,因此它只能写入不安全上下文中。然而,为设置此不安全上下文,既可以将整个类设置为不安全的(如 A 中的情况),也可以仅在方法声明中包含一个 unsafe 修饰符(如 B 中的情况)。

1.2 指针类型

在不安全上下文中,type(第 4 章)可以是 pointer-type,也可以是 value-type 或 reference-type。但是,pointer-type也可以在不安全上下文外部的 typeof 表达式(第 7.6.10.6 节)中使用,因为此类不是不安全的。

type:
...
pointer-type

pointer-type 可表示为 unmanaged-type 后接一个 * 标记,或者关键字 void 后接一个 * 标记:

pointer-type:
unmanaged-type   *
void  
*

unmanaged-type:
type

指针类型中,在 * 前面指定的类型称为该指针类型的目标类型 (referent type)。它表示该指针类型的值所指向的变量的类型。

与引用(引用类型的值)不同,指针不受垃圾回收器跟踪(垃圾回收器并不知晓指针和它们指向的数据)。出于此原因,不允许指针指向引用或者包含引用的结构,并且指针的目标类型必须是 unmanaged-type。

unmanaged-type 为不是 reference-type 或构造类型的任何类型,不在任何嵌套级别上包含 reference-type 或构造类型字段。换句话说,unmanaged-type 是下列类型之一:

  • sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 或 bool。
  • 任何 enum-type。
  • 任何 pointer-type。
  • 非构造类型且仅包含unmanaged-type 的字段的任何用户定义 struct-type。

将指针和引用进行混合使用时的基本规则是;引用(对象)的目标可以包含指针,但指针的目标不能包含引用。

下表给出了一些指针类型的示例:

示例

说明

byte*

指向 byte
的指针

char*

指向 char
的指针

int**

指向 int 的指针

int*[]

由指向 int 的指针构成的一维数组

void*

指向未知类型的指针

对于某个给定实现,所有的指针类型都必须具有相同的大小和表示形式。

与 C 和 C++ 不同,在 C# 中,当在同一声明中声明多个指针时,* 只与基础类型写在一起,而不充当每个指针名称的前缀标点符号。例如

int* pi, pj;   // NOT as int *pi, *pj;

类型为 T* 的一个指针的值表示类型为 T 的一个变量的地址。指针间接寻址运算符 *(第 18.5.1 节)可用于访问此变量。例如,给定 int* 类型的变量 P,则表达式 *P 表示在 P 中包含的地址处找到的 int 变量。

与对象引用类似,指针可以是 null。如果将间接寻址运算符应用于 null 指针,则其行为将由实现自己定义。值为 null 的指针表示为将该指针的所有位都置零。

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

指针类型是一个单独类别的类型。与引用类型和值类型不同,指针类型不从 object 继承,而且不存在指针类型和 object 之间的转换。具体而言,指针不支持装箱和取消装箱(第 4.3 节)操作。但是,允许在不同指针类型之间以及指针类型与整型之间进行转换。第 18.4 节中对此进行了介绍。

pointer-type 不能用作类型实参(第 4.4 节),且类型推断(第 7.5.2 节)在泛型方法调用期间失败,因为该调用会将类型实参推断为指针类型。

pointer-type 不能用作动态绑定操作的构成表达式的类型(第 7.2.2 节)。

pointer-type 可用作易失字段的类型(第 10.5.3 节)。

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

using System;

class Test
{
static int value = 20;

unsafe static void F(out int* pi1, ref int*
pi2) {
     int i = 10;
     pi1 = &i;

fixed (int* pj = &value) {
        // ...
        pi2 = pj;
     }
}

static void Main() {
     int i = 10;
     unsafe {
        int* px1;
        int* px2 = &i;

F(out px1, ref px2);

Console.WriteLine("*px1 = {0}, *px2
= {1}",
            *px1, *px2);  // undefined behavior
     }
}
}

方法可以返回某一类型的值,而该类型可以是指针。例如,给定一个指向连续的 int值序列的指针、该序列的元素个数,和另外一个 int 值,下面的方法将在该整数序列中查找与该 value 匹配的值,若找到匹配项,则返回该匹配项的地址;否则,它将返回 null:

unsafe static
int* Find(int* pi, int size, int value) {
for (int i = 0; i < size; ++i) {
     if (*pi == value)
        return pi;
     ++pi;
}
return null;
}

在不安全上下文中,可以使用下列几种构造操作指针:

  • * 运算符可用于执行指针间接寻址(第 18.5.1 节)。
  • -> 运算符可用于通过指针访问结构的成员(第 18.5.2 节)。
  • [] 运算符可用于索引指针(第 18.5.3 节)。
  • & 运算符可用于获取变量的地址(第 18.5.4 节)。
  • ++ 和 -- 运算符可以用于递增和递减指针(第 18.5.5 节)。
  • + 和 - 运算符可用于执行指针算术运算(第 18.5.6 节)。
  • ==、!=、<、>、<= 和 => 运算符可以用于比较指针(第 18.5.7 节)。
  • stackalloc 运算符可用于从调用堆栈中分配内存(第 18.7 节)。
  • fixed 语句可用于临时固定一个变量,以便可以获取它的地址(第 18.6 节)。

1.3 固定和可移动变量

address-of
运算符(第 18.5.4 节)和 fixed 语句(第 18.6 节)将变量分为两种类别:固定变量 (Fixed variables) 和可移动变量 (moveable variables)。

固定变量驻留在不受垃圾回收器的操作影响的存储位置中。(固定变量的示例包括局部变量、值参数和由取消指针引用而创建的变量。)另一方面,可移动变量则驻留在会被垃圾回收器重定位或释放的存储位置中。(可移动变量的示例包括对象中的字段和数组的元素。)

& 运算符(第
18.5.4 节)允许不受限制地获取固定变量的地址。但是,由于可移动变量会受到垃圾回收器的重定位或释放,因此可移动变量的地址只能使用 fixed 语句(第 18.6 节)获取,而且该地址只在此 fixed 语句的生存期内有效。

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

  • 用引用局部变量或值参数的 simple-name(第 7.6.2 节)表示的变量(如果该变量未由匿名函数捕获)。
  • 用 V.I 形式的 member-access(第 7.6.4 节)表示的变量,其中 V 是 struct-type 的固定变量。
  • 用 *P 形式的 pointer-indirection-expression(第 18.5.1 节)、P->I 形式的
     pointer-member-access(第 18.5.2 节)或 P[E] 形式的 pointer-element-access(第 18.5.3 节)表示的变量。

所有其他变量都属于可移动变量。

请注意静态字段属于可移动变量。还请注意即使赋予 ref 或 out 形参的实参是固定变量,它们仍属于可移动变量。最后请注意,由取消指针引用而产生的变量总是属于固定变量。

1.4 指针转换

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

  • 从任何
    pointer-type 到 void* 类型。
  • 从 null 文本到任何 pointer-type。

另外,在不安全上下文中,可供使用的显式转换的集合(第
6.2 节)也扩展为包括以下显式指针转换:

  • 从任何
    pointer-type 到任何其他 pointer-type。
  • 从 sbyte、byte、short、ushort、int、uint、long 或 ulong 到任何 pointer-type。
  • 从任何
    pointer-type 到 sbyte、byte、short、ushort、int、uint、long 或 ulong。

最后,在不安全上下文中,标准隐式转换(第 6.3.1 节)的集包括下列指针转换:

  • 从任何
    pointer-type 到 void* 类型。
  • 从 null 文本到任何 pointer-type。

两个指针类型之间的转换永远不会更改实际的指针值。换句话说,从一个指针类型到另一个指针类型的转换不会影响由指针给出的基础地址。

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

请考虑下列情况,其中具有一个类型的变量被通过指向一个不同类型的指针访问:

char c = 'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
int i = *pi;       // undefined
*pi = 123456;      // undefined

当一个指针类型被转换为指向字节的指针时,转换后的指针将指向原来所指变量的地址中的最低寻址字节。连续增加该变换后的指针(最大可达到该变量所占内存空间的大小),将产生指向该变量的其他字节的指针。例如,下列方法将 double 型变量中的八个字节的每一个显示为一个十六进制值:

using System;

class Test
{
unsafe static void Main() {
      double d = 123.456e23;
     unsafe {
       
byte* pb = (byte*)&d;
        for (int i = 0; i <
sizeof(double); ++i)
       Console.Write("{0:X2} ", *pb++);
        Console.WriteLine();
     }
}
}

当然,产生的输出取决于字节存储顺序 (Endianness)。

指针和整数之间的映射由实现定义。但是,在具有线性地址空间的 32 位和 64 位 CPU 体系结构上,指针和整型之间的转换通常与 uint 或 ulong 类型的值与这些整型之间的对应方向上的转换具有完全相同的行为。

1.4.1 指针数组

可以在不安全上下文中构造指针数组。只有一部分适用于其他数组类型的转换适用于指针数组:

  • 从任意
    array-type 到 System.Array 及其实现的接口的隐式引用转换(第 6.1.6 节)也适用于指针数组。但是,由于指针类型不可转换为
    object,因此只要尝试通过 System.Array 或其实现的接口访问数组元素,就会导致在运行时出现异常。
  • 从一维数组类型 S[] 到 System.Collections.Generic.IList<T> 及其泛型基接口的隐式和显式引用转换(第 6.1.6、6.2.4 节)从不适用于指针数组,因为指针类型不能用作类型实参,且不存在从指针类型到非指针类型的转换。
  • 从 System.Array 及其实现的接口到任意 array-type 的显式引用转换(第 6.2.4 节)均适用于指针数组。
  • 从System.Collections.Generic.IList<S> 及其基接口到一维数组类型 T[] 的隐式引用转换(第 6.2.4节)在任何情况下均不适用于指针数组,因为指针类型不能用作类型实参,且不存在从指针类型到非指针类型的转换。

这些限制意味着通过第 8.8.4 节中给出的数组对 foreach
语句进行的扩展不能用于指针数组。而下列形式的 foreach 语句

foreach (V v in x) embedded-statement

(其中 x  的类型为具有 T[,,…,], n 形式的数组类型,n 为维度数减 1,T 或 V 为指针类型)使用嵌套 for 循环扩展,如下所示:

{
T[,,…,] a = x;
V v;
for (int i0 = a.GetLowerBound(0); i0
<= a.GetUpperBound(0); i0++)
for (int i1 = a.GetLowerBound(1); i1
<= a.GetUpperBound(1); i1++)

for (int in = a.GetLowerBound(n); in <=
a.GetUpperBound(n); in++) {
     v  = (V)a.GetValue(i0,i1,…,in);
         embedded-statement
}
}

变量 a, i0,
i1, … in 对 x 或 embedded-statement 或该程序的任何其他源代码均不可见或不可访问。变量 v 在嵌入语句中是只读的。如果不存在从 T(元素类型)到 V 的显式转换(第 18.4 节),则会出错且不会执行下面的步骤。如果 x 具有值 null,则将在运行时引发 System.NullReferenceException。

1.5 表达式中的指针

在不安全上下文中,表达式可能产生指针类型的结果,但是在不安全上下文以外,表达式为指针类型属于编译时错误。准确地说,在不安全上下文以外,如果任何 simple-name(第 7.6.2 节)、member-access(第 7.6.4 节)、invocation-expression(第 7.6.5 节)或 element-access(第 7.6.6 节)属于指针类型,则将发生编译时错误。

在不安全上下文中,primary-no-array-creation-expression(第 7.6 节)和 unary-expression(第 7.7 节)产生式允许使用下列附加构造:

primary-no-array-creation-expression:
...
pointer-member-access
pointer-element-access
sizeof-expression

unary-expression:
...
pointer-indirection-expression
addressof-expression

以下几节对这些构造进行了描述。相关的语法暗示了不安全运算符的优先级和结合性。

1.5.1 指针间接寻址

pointer-indirection-expression 包含一个星号 (*),后接一个 unary-expression。

pointer-indirection-expression:
*  
unary-expression

一元 * 运算符表示指针间接寻址并且用于获取指针所指向的变量。计算 *P得到的结果(其中 P 为指针类型 T* 的表达式)是类型为 T 的一个变量。将一元 * 运算符应用于 void* 类型的表达式或者应用于不是指针类型的表达式属于编译时错误。

将一元 * 运算符应用于 null 指针的效果是由实现定义的。具体而言,不能保证此操作会引发 System.NullReferenceException。

如果已经将无效值赋给指针,则一元 * 运算符的行为是未定义的。通过一元 * 运算符取消指针引用有时会产生无效值,这些无效值包括:没能按所指向的类型正确对齐的地址(请参见第 18.4 节中的示例)和超过生存期的变量的地址。

出于明确赋值分析的目的,通过计算 *P 形式的表达式产生的变量被认为是初始化赋过值的(第 5.3.1 节)。

1.5.2 指针成员访问

pointer-member-access 包含一个 primary-expression,后接一个“->”标记,最后是一个 identifier 和一个可选 type-argument-list。

pointer-member-access:
primary-expression   ->   identifier  type-argument-listopt

在 P->I 形式的指针成员访问中,P 必须是除 void* 以外的某个指针类型的表达式,而 I 必须表示 P 所指向的类型的可访问成员。

P->I 形式的指针成员访问的计算方式与 (*P).I 完全相同。有关指针间接寻址运算符 (*) 的说明,请参见第 18.5.1 节。有关成员访问运算符 (.) 的说明,请参见第 7.6.4 节。

在下面的示例中

using System;

struct Point
{
public int x;
public int y;

public
override string ToString() {
     return "(" + x +
"," + y + ")";
}
}

class Test
{
static void Main() {
     Point point;
     unsafe {
        Point* p = &point;
        p->x = 10;
        p->y = 20;
        Console.WriteLine(p->ToString());
     }
}
}

-> 运算符用于通过指针访问结构中的字段和调用结构中的方法。由于 P->I 操作完全等效于 (*P).I,因此 Main 方法可以等效地编写为:

class Test
{
static void Main() {
     Point point;
     unsafe {
        Point* p = &point;
        (*p).x = 10;
        (*p).y = 20;
        Console.WriteLine((*p).ToString());
     }
}
}

1.5.3 指针元素访问

pointer-element-access 包括一个 primary-no-array-creation-expression,后接一个用“[”和“]”括起来的表达式。

pointer-element-access:
primary-no-array-creation-expression   [   expression   ]

在形式为 P[E] 的指针元素访问中,P 必须为除 void* 之外的指针类型表达式,E 必须为可以隐式转换为 int、uint、long 或 ulong 的表达式。

P[E] 形式的指针元素访问的计算方式与 *(P + E) 完全相同。有关指针间接寻址运算符 (*) 的说明,请参见第 18.5.1 节。有关指针加法运算符 (+) 的说明,请参见第 18.5.6 节。

在下面的示例中

class Test
{
static void Main() {
     unsafe {
        char* p = stackalloc char[256];
        for (int i = 0; i < 256; i++)
p[i] = (char)i;
     }
}
}

指针元素访问用于在 for 循环中初始化字符缓冲区。由于 P[E] 操作完全等效于 *(P + E),因此示例可以等效地编写为:

class Test
{
static void Main() {
     unsafe {
         char*
p = stackalloc char[256];
        for (int i = 0; i < 256; i++)
*(p + i) = (char)i;
     }
}
}

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

1.5.4 address-of 运算符

addressof-expression 包含一个“&”符 (&),后接一个 unary-expression。

addressof-expression:
&  
unary-expression

如果给定类型为 T 且属于固定变量(第 18.3 节)的表达式E,构造 &E 将计算由 E 给出的变量的地址。计算的结果是一个类型为 T* 的值。如果 E 不属于变量,如果 E 属于只读局部变量,或如果 E 表示可移的变量,则将发生编译时错误。在最后一种情况中,可以先利用固定语句(第 18.6 节)临时“固定”该变量,再获取它的地址。如第 7.6.4 节中所述,如果在实例构造函数或静态构造函数之外,在结构或类中定义了 readonly 字段,则该字段被认为是一个值,而不是变量。因此,无法获取它的地址。与此类似,无法获取常量的地址。

& 运算符不要求它的参数先被明确赋值,但是在执行了 & 操作后,该运算符所应用于的那个变量在此操作发生的执行路径中被“认为是”已经明确赋值的。这意味着,由程序员负责确保在相关的上下文中对该变量实际进行合适的初始化。

在下面的示例中

using
System;

class Test
{
static void Main() {
     int i;
     unsafe {
        int* p = &i;
        *p = 123;
     }
     Console.WriteLine(i);
}
}

初始化 p 的代码执行了 &i 操作,此后 i 被认为是明确赋值的。对 *p 的赋值实际上是初始化了 i,但设置此初始化是程序员的责任,而且如果移除此赋值语句,也不会发生编译时错误。

上述 & 运算符的明确赋值规则可以避免局部变量的冗余初始化。例如,许多外部 API 要求获取指向结构的指针,而由此 API 来填充该结构。对此类 API 进行的调用通常会传递局部结构变量的地址,而如果没有上述规则,则将需要对此结构变量进行冗余初始化。

1.5.5 指针递增和递减

在不安全上下文中,++ 和 ‑‑ 运算符(第 7.6.9 节和第 7.7.5 节)可以应用于除 void* 以外的所有类型的指针变量。因此,为每个指针类型 T* 都隐式定义了下列运算符:

T* operator ++(T* x);

T* operator --(T* x);

这些运算符分别产生与 x + 1 和 x - 1(第 18.5.6 节)相同的结果。换句话说,对于 T* 类型的指针变量,++ 运算符将该变量的地址加上 sizeof(T),而 ‑‑ 运算符则将该变量的地址减去 sizeof(T)。

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

1.5.6 指针算术运算

在不安全上下文中,+ 和 - 运算符(第 7.8.4 节和第 7.8.5 节)可以应用于除 void* 以外的所有指针类型的值。因此,为每个指针类型 T* 都隐式定义了下列运算符:

T* operator
+(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);

T* operator
+(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);

T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);

long
operator –(T* x, T* y);

给定指针类型 T* 的表达式 P 和类型为 int、uint、long 或 ulong 的表达式 N,表达式 P + N 和 N + P 将计算类型为 T* 的指针值,该值等于由 P 给出的地址加上 N * sizeof(T)。与此类似,表达式 P - N 将计算类型为 T* 的指针值,该值等于由 P 给出的地址减去 N * sizeof(T)。

给定指针类型 T* 的两个表达式 P 和 Q,表达式 P – Q 将先计算 P 和 Q 给出的地址之间的差,然后用 sizeof(T) 去除该差值。计算结果的类型始终为 long。实际上,P - Q 将按 ((long)(P) - (long)(Q)) / sizeof(T) 进行计算。

例如:

using
System;

class Test
{

static void Main() {
     unsafe {
        int* values = stackalloc int[20];
        int* p = &values[1];
        int* q = &values[15];
        Console.WriteLine("p - q =
{0}", p - q);
        Console.WriteLine("q - p =
{0}", q - p);
     }
}
}

生成以下输出:

p - q = -14
q - p = 14

如果在执行上述指针算法时,计算结果超越该指针类型的域,则将以实现所定义的方式截断结果,但是不会产生异常。

1.5.7 指针比较

在不安全上下文中,==、!=、<、>、<= 和 => 运算符(第 7.10 节)可以应用于所有指针类型的值。指针比较运算符有:

bool
operator ==(void* x, void* y);

bool
operator !=(void* x, void* y);

bool
operator <(void* x, void* y);

bool
operator >(void* x, void* y);

bool
operator <=(void* x, void* y);

bool
operator >=(void* x, void* y);

由于存在从任何指针类型到 void* 类型的隐式转换,因此可以使用这些运算符来比较任何指针类型的操作数。比较运算符像比较无符号整数一样比较两个操作数给出的地址。

1.5.8 sizeof 运算符

sizeof 运算符返回由给定类型的变量占用的字节数。被指定为 sizeof 的操作数的类型必须为 unmanaged-type(第 18.2 节)。

sizeof-expression:
sizeof   (   unmanaged-type   )

sizeof 运算符的结果是 int 类型的值。对于某些预定义类型,sizeof 运算符将产生如下表所示的常量值。

表达式

结果

sizeof(sbyte)

1

sizeof(byte)

1

sizeof(short)

2

sizeof(ushort)

2

sizeof(int)

4

sizeof(uint)

4

sizeof(long)

8

sizeof(ulong)

8

sizeof(char)

2

sizeof(float)

4

sizeof(double)

8

sizeof(bool)

1

对于所有其他类型,sizeof 运算符的结果是由实现定义的,并且属于值而不是常量。

一个结构所属的各个成员以什么顺序被装入该结构中,没有明确规定。

出于对齐的目的,在结构的开头、结构内以及结构的结尾处可以插入一些未命名的填充位。这些填充位的内容是不确定的。

当 sizeof 应用于具有结构类型的操作数时,结果是该类型变量所占的字节总数(包括所有填充位在内)。

1.6 fixed 语句

在不安全上下文中,embedded-statement(第 8 章)产生式允许使用一个附加结构即 fixed 语句,该语句用于“固定”可移动变量,从而使该变量的地址在语句的持续时间内保持不变。

embedded-statement:
...
fixed-statement

fixed-statement:
fixed  
(  
pointer-type  
fixed-pointer-declarators   )   embedded-statement

fixed-pointer-declarators:
fixed-pointer-declarator
fixed-pointer-declarators   ,   fixed-pointer-declarator

fixed-pointer-declarator:
identifier   =  
fixed-pointer-initializer

fixed-pointer-initializer:
&  
variable-reference
expression

如上述产生式所述,每个 fixed-pointer-declarator 声明一个给定 pointer-type 的局部变量,并使用由相应的 fixed-pointer-initializer 计算的地址初始化该局部变量。在 fixed 语句中声明的局部变量的可访问范围仅限于:在该变量声明右边的所有 fixed-pointer-initializer 中,以及在该 fixed 语句的 embedded-statement 中。由 fixed 语句声明的局部变量被视为只读。如果嵌入语句试图修改此局部变量(通过赋值或 ++ 和 ‑‑ 运算符)或者将它作为 ref 或 out 参数传递,则将出现编译时错误。

fixed-pointer-initializer 可以是下列之一:

  • “&”标记,后接一个 variable-reference(第 5.3.3 节),它引用非托管类型 T 的可移动变量(第 18.3 节),前提是类型 T* 可以隐式转换为 fixed 语句中给出的指针类型。在这种情况下,初始值设定项将计算给定变量的地址,而 fixed 语句在生存期内将保证该变量的地址不变。
  • 元素类型为非托管类型 T 的 array-type 的表达式,前提是类型 T* 可隐式转换为 fixed 语句中给出的指针类型。在这种情况下,初始值设定项将计算数组中第一个元素的地址,而 fixed 语句在生存期内将保证整个数组的地址保持不变。如果数组表达式为 null 或者数组具有零个元素,则 fixed 语句的行为由实现定义。
  • string 类型的表达式,前提是类型 char* 可以隐式转换为 fixed 语句中给出的指针类型。在这种情况下,初始值设定项将计算字符串中第一个字符的地址,而 fixed 语句在生存期内将保证整个字符串的地址不变。如果字符串表达式为 null,则 fixed 语句的行为由实现定义。
  • 引用可移动变量的固定大小缓冲区成员的 simple-name 或 member-access,前提是固定大小缓冲区成员的类型可以隐式转换为 fixed 语句中给出的指针类型。这种情况下,初始值设定项计算出指向固定大小缓冲区(第 18.7.2 节)第一个元素的指针,并且该固定大小缓冲区保证在
    fixed 语句的持续时间内保留在某个固定地址。

对于每个由 fixed-pointer-initializer 计算的地址,fixed 语句确保由该地址引用的变量在 fixed 语句的生存期内不会被垃圾回收器重定位或者释放。例如,如果由 fixed-pointer-initializer 计算的地址引用对象的字段或数组实例的元素,fixed 语句将保证包含该字段或元素的对象实例本身也不会在该语句的生存期内被重定位或者释放。

确保由 fixed 语句创建的指针在执行这些语句之后不再存在是程序员的责任。例如,当 fixed 语句创建的指针被传递到外部 API 时,确保 API 不会在内存中保留这些指针是程序员的责任。

固定对象可能导致堆中产生存储碎片(因为它们无法移动)。出于该原因,只有在绝对必要时才应当固定对象,而且固定对象的时间越短越好。

下面的示例

class Test
{
static int x;
int y;

unsafe static void F(int* p) {
     *p = 1;
}

static void Main() {
     Test t = new Test();
     int[] a = new int[10];
     unsafe {
        fixed (int* p = &x) F(p);
        fixed (int* p = &t.y) F(p);
        fixed (int* p = &a[0]) F(p);
        fixed (int* p = a) F(p);
     }
}
}

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

上面示例中的第四个 fixed 语句生成与第三个语句类似的结果。

此 fixed 语句示例将使用 string:

class Test
{
static string name = "xx";

unsafe static void F(char* p) {
     for (int i = 0; p[i] != '\0'; ++i)
        Console.WriteLine(p[i]);
}

static void Main() {
     unsafe {
        fixed (char* p = name) F(p);
        fixed (char* p = "xx")
F(p);
     }
}
}

在不安全上下文中,一维数组的数组元素按递增索引顺序存储,从索引 0 开始,到索引 Length – 1 结束。对于多维数组,数组元素按这样的方式存储:首先增加最右边维度的索引,然后是左边紧邻的维度,依此类推直到最左边。在获取指向数组实例 a 的指针 p 的 fixed 语句内,从 p 到 p + a.Length - 1 范围内的每个指针值均表示数组中的一个元素的地址。与此类似,从 p[0] 到 p[a.Length - 1] 范围内的变量表示实际的数组元素。已知数组的存储方式,可以将任意维度的数组都视为线性的。

例如:

using System;

class Test
{
static void Main() {
     int[,,] a = new int[2,3,4];
     unsafe {
        fixed (int* p = a) {
            for (int i = 0; i <
a.Length; ++i) // treat as linear
               p[i] = i;
        }
     }

for (int i = 0; i < 2; ++i)
        for (int j = 0; j < 3; ++j) {
            for (int k = 0; k < 4; ++k)
               Console.Write("[{0},{1},{2}]
= {3,2} ", i, j, k, a[i,j,k]);
           Console.WriteLine();
        }
}
}

生成以下输出:

[0,0,0]
=  0 [0,0,1] =  1 [0,0,2] = 
2 [0,0,3] =  3
[0,1,0] =  4 [0,1,1] =  5 [0,1,2] = 
6 [0,1,3] =  7
[0,2,0] =  8 [0,2,1] =  9 [0,2,2] = 10 [0,2,3] = 11
[1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15
[1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19
[1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23

在下面的示例中

class Test
{
unsafe static void Fill(int* p, int
count, int value) {
     for (; count != 0; count--) *p++ =
value;
}

static void Main() {
     int[] a = new int[100];
     unsafe {
        fixed (int* p = a) Fill(p, 100,
-1);
     }
}
}

使用一条 fixed 语句固定一个数组,以便可以将该数组的地址传递给一个采用指针作为参数的方法。

在下面的示例中:

unsafe struct
Font
{
public int size;
public fixed char name[32];
}

class Test
{
unsafe static void PutString(string s, char*
buffer, int bufSize) {
     int len = s.Length;
     if (len > bufSize) len = bufSize;
     for (int i = 0; i < len; i++)
buffer[i] = s[i];
     for (int i = len; i < bufSize;
i++) buffer[i] = (char)0;
}

Font f;

unsafe static void Main()
{
     Test test = new Test();
     test.f.size = 10;
     fixed (char* p = test.f.name) {
        PutString("Times New
Roman", p, 32);
     }
}
}

一个固定语句用于固定一个结构的固定大小缓冲区,因此可以将该缓冲区的地址用作指针。

通过固定字符串实例产生的 char* 类型的值始终指向以 null 结尾的字符串。在获取指向字符串实例 s 的指针 p 的 fixed 语句内,从 p 到 p + s.Length - 1 范围内的指针值表示字符串中字符的地址,而指针值 p + s.Length 则始终指向一个 null 字符(值为 '\0' 的字符)。

通过固定指针修改托管类型的对象可能导致未定义的行为。例如,由于字符串是不可变的,因此程序员应确保指向固定字符串的指针所引用的字符不被修改。

这种字符串的自动空字符终止功能,大大方便了调用需要“C 风格”字符串的外部 API。但请注意,核心 C# 允许字符串实例包含空字符。如果字符串中存在此类空字符,则在将字符串视为空终止的 char* 时将出现截断。

1.7 固定大小缓冲区

固定大小缓冲区用于将“C 风格”的内联数组声明为结构的成员,且主要用于与非托管 API 交互。

1.7.1 固定大小缓冲区的声明

固定大小缓冲区 (fixed size buffer) 是一个成员,表示给定类型的变量的固定长度缓冲区的存储区。固定大小缓冲区声明引入了给定元素类型的一个或多个固定大小缓冲区。仅允许在结构声明中使用固定大小缓冲区,且只能出现在不安全上下文(第 18.1 节)中。

struct-member-declaration:

fixed-size-buffer-declaration

fixed-size-buffer-declaration:
attributesopt  
fixed-size-buffer-modifiersopt   fixed   buffer-element-type
          fixed-size-buffer-declarators   ;

fixed-size-buffer-modifiers:
fixed-size-buffer-modifier
fixed-size-buffer-modifier  
fixed-size-buffer-modifiers

fixed-size-buffer-modifier:
new
public
protected
internal
private
unsafe

buffer-element-type:
type

fixed-size-buffer-declarators:
fixed-size-buffer-declarator
fixed-size-buffer-declarator   ,   fixed-size-buffer-declarators

fixed-size-buffer-declarator:
identifier   [   constant-expression   ]

固定大小缓冲区声明可包括一组特性(第 17 节)、一个 new 修饰符(第 10.2.2 节)、四个访问修饰符(第 10.2.3 节)的一个有效组合和一个 unsafe 修饰符(第 18.1 节)。这些特性和修饰符适用于由固定大小缓冲区声明所声明的所有成员。同一个修饰符在一个固定大小缓冲区声明中出现多次是一个错误。

固定大小缓冲区声明不允许包含 static 修饰符。

固定大小缓冲区声明的缓冲区元素类型指定了由该声明引入的缓冲区的元素类型。缓冲区元素类型必须为下列预定义类型之一:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double 或 bool。

缓冲区元素类型后接一个固定大小缓冲区声明符的列表,该列表中的每个声明符引入一个新成员。固定大小缓冲区声明符由一个用于命名成员的标识符以及标识符后面由 [ 和 ] 标记括起来的常量表达式所组成。该常量表达式表示在由该固定大小缓冲区声明符引入的成员中的元素数量。该常量表达式的类型必须可隐式转换为类型 int,并且该值必须是非零的正整数。

固定大小缓冲区的元素保证在内存中按顺序放置。

声明多个固定大小缓冲区的固定大小缓冲区声明相当于单个固定大小缓冲区的带有相同特性和元素类型的多个声明。例如

unsafe struct A
{
   public fixed int x[5], y[10], z[100];
}

相当于

unsafe struct A
{
   public fixed int x[5];
   public fixed int y[10];
   public fixed int z[100];
}

1.7.2 表达式中的固定大小缓冲区

固定大小缓冲区成员的成员查找(第 7.3 节)过程与字段的成员查找完全相同。

可使用 simple-name(第 7.5.2 节)或 member-access(第 7.5.4 节)在表达式中引用固定大小缓冲区。

当固定大小缓冲区成员作为简单名称被引用时,其效果与 this.I 形式的成员访问相同,其中 I 为固定大小缓冲区成员。

在 E.I 形式的成员访问中,如果 E 为结构类型,并且在该结构类型中通过 I 的成员查找标识了一个固定大小成员,则如下计算并归类 E.I:

  • 如果表达式 E.I 不属于不安全上下文,则发生编译时错误。
  • 如果 E 归类为值类别,则发生编译时错误。
  • 否则,如果 E 为可移动变量(第 18.3 节)并且表达式 E.I 不是 fixed-pointer-initializer(第 18.6 节),则发生编译时错误。
  • 否则,E 引用固定变量,并且该表达式的结果为指向 E 中的固定大小缓冲区成员 I 的第一个元素的指针。结果为类型 S*,其中 S 为 I 的元素类型,并且归类为值。

可使用指针操作从第一个元素开始访问固定大小缓冲区的后续元素。与访问数组不同,访问固定大小缓冲区的元素是不安全操作,并且不进行范围检查。

下面的示例声明并使用了一个包含固定大小缓冲区成员的结构。

unsafe struct Font
{
public int size;
public fixed char name[32];
}

class Test
{
unsafe static void PutString(string s,
char* buffer, int bufSize) {
     int len = s.Length;
     if (len > bufSize) len = bufSize;
     for (int i = 0; i < len; i++)
buffer[i] = s[i];
     for (int i = len; i < bufSize;
i++) buffer[i] = (char)0;
}

unsafe
static void Main()
{
     Font f;
     f.size = 10;
     PutString("Times New
Roman", f.name, 32);
}
}

1.7.3 明确赋值 检查

固定大小缓冲区不接受明确赋值检查(第 5.3 节),并且为了对结构类型变量进行明确赋值检查,忽略固定大小缓冲区成员。

当包含固定大小缓冲区成员的最外层结构变量为静态变量、类实例的实例变量或数组元素时,该固定大小缓冲区的元素自动初始化为其默认值(第 5.2 节)。而在所有其他情况下,固定大小缓冲区的初始内容未定义。

1.8 堆栈分配

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

local-variable-initializer:

stackalloc-initializer

stackalloc-initializer:
stackalloc  
unmanaged-type   [   expression   ]

上述产生式中,unmanaged-type 表示将在新分配的位置中存储的项的类型,而 expression 则指示这些项的数目。合在一起,它们指定所需的分配大小。由于堆栈分配的大小不能为负值,因此将项的数目指定为计算结果为负值的 constant-expression 属于编译时错误。

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

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

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

无法显式释放利用 stackalloc 分配的内存。在函数成员的执行期间创建的所有堆栈分配内存块都将在该函数成员返回时自动丢弃。这对应于 alloca 函数,它是通常存在于 C 和 C++ 实现中的一个扩展。

在下面的示例中

using System;

class Test
{
static string IntToString(int value) {
     int n = value >= 0? value: -value;
     unsafe {
        char* buffer = stackalloc
char[16];
        char* p = buffer + 16;
        do {
            *--p = (char)(n % 10 + '0');
            n /= 10;
        } while (n != 0);
        if (value < 0) *--p = '-';
        return new string(p, 0,
(int)(buffer + 16 - p));
     }
}

static void Main() {
     Console.WriteLine(IntToString(12345));
     Console.WriteLine(IntToString(-999));
}
}

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

1.9 动态内存分配

除 stackalloc 运算符外,C# 不提供其他预定义构造来管理那些不受垃圾回收控制的内存。这些服务通常是由支持类库提供或者直接从基础操作系统导入的。例如,下面的 Memory 类阐释了可以如何从 C# 访问基础操作系统的有关堆处理的各种函数:

using System;
using System.Runtime.InteropServices;

public unsafe
class Memory
{
// Handle for the process heap. This
handle is used in all calls to the
// HeapXXX APIs in the methods below.

static int ph = GetProcessHeap();

// Private instance constructor to prevent
instantiation.

private Memory() {}

// Allocates a memory block of the given size.
The allocated memory is
// automatically initialized to zero.

public static void* Alloc(int size) {
     void* result = HeapAlloc(ph,
HEAP_ZERO_MEMORY, size);
     if (result == null) throw new
OutOfMemoryException();
     return result;
}

// Copies count bytes from src to dst. The
source and destination
// blocks are permitted to overlap.

public static void Copy(void* src, void* dst,
int count) {
     byte* ps = (byte*)src;
     byte* pd = (byte*)dst;
     if (ps > pd) {
        for (; count != 0; count--) *pd++
= *ps++;
     }
     else if (ps < pd) {
        for (ps += count, pd += count;
count != 0; count--) *--pd = *--ps;
     }
}

// Frees a memory block.

public static void Free(void* block) {
     if (!HeapFree(ph, 0, block)) throw
new InvalidOperationException();
}

// Re-allocates a memory block. If the
reallocation request is for a
// larger size, the additional region of
memory is automatically
// initialized to zero.

public static void* ReAlloc(void* block, int
size) {
     void* result = HeapReAlloc(ph,
HEAP_ZERO_MEMORY, block, size);
     if (result == null) throw new
OutOfMemoryException();
     return result;
}

// Returns the size of a memory block.

public static int SizeOf(void* block) {
     int result = HeapSize(ph, 0, block);
     if (result == -1) throw new
InvalidOperationException();
     return result;
}

// Heap API flags

const int HEAP_ZERO_MEMORY = 0x00000008;

// Heap API functions

[DllImport("kernel32")]
static extern int GetProcessHeap();

[DllImport("kernel32")]
static extern void* HeapAlloc(int hHeap,
int flags, int size);

[DllImport("kernel32")]
static extern bool HeapFree(int hHeap,
int flags, void* block);

[DllImport("kernel32")]
static extern void* HeapReAlloc(int
hHeap, int flags,
     void* block, int size);

[DllImport("kernel32")]
static extern int HeapSize(int hHeap, int
flags, void* block);
}

以下给出一个使用 Memory
类的示例:

class Test
{
static void Main() {
     unsafe {
        byte* buffer =
(byte*)Memory.Alloc(256);
        try {
            for (int i = 0; i < 256;
i++) buffer[i] = (byte)i;
            byte[] array = new byte[256];
            fixed (byte* p = array)
Memory.Copy(buffer, p, 256);
        }
        finally {
            Memory.Free(buffer);
        }
        for (int i = 0; i < 256; i++)
Console.WriteLine(array[i]);
     }
}
}

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