C#6.0语言规范(十八) 不安全代码
前面章节中定义的核心C#语言与C和C ++的区别在于它省略了作为数据类型的指针。相反,C#提供了引用和创建由垃圾收集器管理的对象的能力。这种设计与其他功能相结合,使C#成为比C或C ++更安全的语言。在核心C#语言中,根本不可能有一个未初始化的变量,一个“悬空”指针,或一个索引超出其边界的数组的表达式。因此消除了常规困扰C和C ++程序的所有类别的错误。
虽然C或C ++中的每个指针类型构造实际上都具有C#中的引用类型,但是有时候需要访问指针类型。例如,如果不访问指针,则可能无法实现与底层操作系统的接口,访问内存映射设备或实现时间关键算法。为了满足这种需求,C#提供了编写不安全代码的能力。
在不安全的代码中,可以声明和操作指针,执行指针和整数类型之间的转换,获取变量的地址,等等。从某种意义上说,编写不安全的代码就像在C#程序中编写C代码一样。
从开发人员和用户的角度来看,不安全代码实际上是一种“安全”功能。必须使用修饰符清楚地标记不安全的代码unsafe
,因此开发人员不可能意外地使用不安全的功能,并且执行引擎可以确保不安全的代码无法在不受信任的环境中执行。
不安全的背景
C#的不安全功能仅在不安全的上下文中可用。通过unsafe
在类型或成员的声明中包含修饰符或使用unsafe_statement来引入不安全的上下文:
- 类,结构,接口或委托的声明可以包含
unsafe
修饰符,在这种情况下,该类型声明的整个文本范围(包括类,结构或接口的主体)被视为不安全的上下文。 - 字段,方法,属性,事件,索引器,运算符,实例构造函数,析构函数或静态构造函数的声明可以包括
unsafe
修饰符,在这种情况下,该成员声明的整个文本范围被视为不安全的上下文。 - 一个unsafe_statement使得内使用不安全上下文的块。相关块的整个文本范围被认为是不安全的上下文。
相关的语法产生如下所示。
1 class_modifier_unsafe 2 : 'unsafe' 3 ; 4 5 struct_modifier_unsafe 6 : 'unsafe' 7 ; 8 9 interface_modifier_unsafe 10 : 'unsafe' 11 ; 12 13 delegate_modifier_unsafe 14 : 'unsafe' 15 ; 16 17 field_modifier_unsafe 18 : 'unsafe' 19 ; 20 21 method_modifier_unsafe 22 : 'unsafe' 23 ; 24 25 property_modifier_unsafe 26 : 'unsafe' 27 ; 28 29 event_modifier_unsafe 30 : 'unsafe' 31 ; 32 33 indexer_modifier_unsafe 34 : 'unsafe' 35 ; 36 37 operator_modifier_unsafe 38 : 'unsafe' 39 ; 40 41 constructor_modifier_unsafe 42 : 'unsafe' 43 ; 44 45 destructor_declaration_unsafe 46 : attributes? 'extern'? 'unsafe'? '~' identifier '(' ')' destructor_body 47 | attributes? 'unsafe'? 'extern'? '~' identifier '(' ')' destructor_body 48 ; 49 50 static_constructor_modifiers_unsafe 51 : 'extern'? 'unsafe'? 'static' 52 | 'unsafe'? 'extern'? 'static' 53 | 'extern'? 'static' 'unsafe'? 54 | 'unsafe'? 'static' 'extern'? 55 | 'static' 'extern'? 'unsafe'? 56 | 'static' 'unsafe'? 'extern'? 57 ; 58 59 embedded_statement_unsafe 60 : unsafe_statement 61 | fixed_statement 62 ; 63 64 unsafe_statement 65 : 'unsafe' block 66 ;
在这个例子中
1 public unsafe struct Node 2 { 3 public int Value; 4 public Node* Left; 5 public Node* Right; 6 }
unsafe
struct声明中指定的修饰符会导致struct声明的整个文本范围变为不安全的上下文。因此,可以将Left
和Right
字段声明为指针类型。上面的例子也可以写成
1 public struct Node 2 { 3 public int Value; 4 public unsafe Node* Left; 5 public unsafe Node* Right; 6 }
这里,unsafe
字段声明中的修饰符会使这些声明被视为不安全的上下文。
除了建立不安全的上下文,从而允许使用指针类型之外,unsafe
修饰符对类型或成员没有影响。在这个例子中
1 public class A 2 { 3 public unsafe virtual void F() { 4 char* p; 5 ... 6 } 7 } 8 9 public class B: A 10 { 11 public override void F() { 12 base.F(); 13 ... 14 } 15 }
方法中的unsafe
修饰符简单地使文本范围成为不安全的上下文,其中可以使用该语言的不安全特征。在in的覆盖中,不需要重新指定修饰符 - 当然,除非该方法本身需要访问不安全的功能。F
A
F
F
B
unsafe
F
B
当指针类型是方法签名的一部分时,情况略有不同
1 public unsafe class A 2 { 3 public virtual void F(char* p) {...} 4 } 5 6 public class B: A 7 { 8 public unsafe override void F(char* p) {...} 9 }
这里,因为F
签名包含指针类型,所以它只能写在不安全的上下文中。但是,可以通过使整个类不安全来引入不安全的上下文,例如A
,或者通过unsafe
在方法声明中包含一个修饰符,就像在案例中那样B
。
指针类型
在不安全的上下文中,类型(类型)可以是pointer_type以及value_type或reference_type。但是,pointer_type也可以用在不安全上下文之外的typeof
表达式(匿名对象创建表达式)中,因为这样的使用不是不安全的。
1 type_unsafe 2 : pointer_type 3 ;
POINTER_TYPE被写成unmanaged_type或关键字void
,随后*
的令牌:
1 pointer_type 2 : unmanaged_type '*' 3 | 'void' '*' 4 ; 5 6 unmanaged_type 7 : type 8 ;
前指定的类型*
中的指针类型称为所指类型的指针类型。它表示指针类型的值所指向的变量的类型。
与引用(引用类型的值)不同,垃圾收集器不跟踪指针 - 垃圾收集器不知道指针和它们指向的数据。因此,不允许指针指向引用或包含引用的结构,并且指针的引用类型必须是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。
- 任何用户定义的struct_type,它不是构造类型,仅包含unmanaged_type字段。
混合指针和引用的直观规则是允许引用(对象)的引用包含指针,但不允许指针的引用包含引用。
指针类型的一些示例在下表中给出:
例 | 描述 |
---|---|
byte* |
指针 byte |
char* |
指针 char |
int** |
指向指针的指针 int |
int*[] |
指向的单维数组 int |
void* |
指向未知类型的指针 |
对于给定的实现,所有指针类型必须具有相同的大小和表示。
与C和C ++不同,当在同一个声明中声明多个指针时,在C#中,*
它只与底层类型一起写入,而不是作为每个指针名称的前缀标点符号。例如
int* pi, pj; // NOT as int *pi, *pj;
具有类型的指针的值T*
表示类型变量的地址T
。指针间接运算符*
(指针间接)可用于访问此变量。例如,给定一个P
类型的变量int*
,表达式*P
表示在int
包含的地址中找到的变量P
。
像对象引用一样,指针可以是null
。将间接运算符应用于null
指针会导致实现定义的行为。具有值的指针null
由all-bits-zero表示。
该void*
类型表示指向未知类型的指针。因为引用类型是未知的,所以间接运算符不能应用于类型的指针void*
,也不能对这样的指针执行任何算术运算。但是,类型的指针void*
可以转换为任何其他指针类型(反之亦然)。
指针类型是一种单独的类型。与引用类型和值类型不同,指针类型不会继承,object
并且指针类型和指针之间不存在转换object
。特别是,指针不支持装箱和拆箱(装箱和拆箱)。但是,允许在不同指针类型之间以及指针类型和整数类型之间进行转换。这在Pointer转换中有所描述。
POINTER_TYPE不能用作类型参数(构造类型),和类型推断(类型推断)上,将已推断出类型参数是指针类型通用的方法调用失败。
POINTER_TYPE可以用作挥发性字段(类型挥发性字段)。
虽然指针可以作为被传递ref
或out
参数,但这样做可能会导致不确定的行为,由于指针可能被设置为指向本地变量不再存在所调用的方法返回时,或固定对象与它用于指向,已不再固定。例如:
1 using System; 2 3 class Test 4 { 5 static int value = 20; 6 7 unsafe static void F(out int* pi1, ref int* pi2) { 8 int i = 10; 9 pi1 = &i; 10 11 fixed (int* pj = &value) { 12 // ... 13 pi2 = pj; 14 } 15 } 16 17 static void Main() { 18 int i = 10; 19 unsafe { 20 int* px1; 21 int* px2 = &i; 22 23 F(out px1, ref px2); 24 25 Console.WriteLine("*px1 = {0}, *px2 = {1}", 26 *px1, *px2); // undefined behavior 27 } 28 } 29 }
方法可以返回某种类型的值,该类型可以是指针。例如,当给定指向int
s 的连续序列的指针,该序列的元素计数和其他一些int
值时,如果匹配发生,则以下方法返回该序列中该值的地址; 否则它返回null
:
1 unsafe static int* Find(int* pi, int size, int value) { 2 for (int i = 0; i < size; ++i) { 3 if (*pi == value) 4 return pi; 5 ++pi; 6 } 7 return null; 8 }
在不安全的上下文中,有几个构造可用于指针操作:
- 该
*
操作者可能被用来执行指针间接(间接指针)。 - 的
->
操作者可用于通过一个指针(访问结构的成员指针成员访问)。 - 的
[]
操作者可被用于索引的指针(指针元素访问)。 - 该
&
操作员可以被用于获得一个变量(的地址的地址的操作者)。 - 的
++
和--
运算符可以用于递增和递减指针(指针递增和递减)。 - 的
+
和-
运算符可能被用来执行指针运算(指针运算)。 - 的
==
,!=
,<
,>
,<=
,和=>
运算符可能被用来比较指针(指针比较)。 - 的
stackalloc
操作者可用于从调用堆栈(分配存储器固定大小的缓冲器)。 - 该
fixed
语句可用于临时修复变量,以便获取其地址(固定语句)。
固定和可移动变量
运算符的地址(运算符的地址)和fixed
语句(固定的语句)将变量分为两类:固定变量和可移动变量。
固定变量驻留在不受垃圾收集器操作影响的存储位置。(固定变量的示例包括局部变量,值参数和通过解除引用指针创建的变量。)另一方面,可移动变量驻留在由垃圾收集器进行重定位或处理的存储位置。(可移动变量的示例包括对象中的字段和数组的元素。)
的&
操作者(所述地址的操作者)允许不受限制要获得的固定变量的地址。但是,由于可移动变量需要由垃圾回收器进行重定位或处理,因此只能使用fixed
语句(固定语句)获取可移动变量的地址,并且该地址仅在该fixed
语句的持续时间内有效。
准确地说,固定变量是以下之一:
- 由simple_name(简单名称)引用的变量,引用局部变量或值参数,除非该变量由匿名函数捕获。
- 由表单的member_access(成员访问)产生的变量
V.I
,其中V
是struct_type的固定变量。 - 由表单的pointer_indirection_expression(指针间接),表单
*P
的pointer_member_access(指针成员访问)P->I
或表单的pointer_element_access(指针元素访问)产生的变量P[E]
。
所有其他变量都归类为可移动变量。
请注意,静态字段被分类为可移动变量。另请注意,即使为参数指定的参数是固定变量,a ref
或out
参数也会被归类为可移动变量。最后,请注意,通过取消引用指针生成的变量始终归类为固定变量。
指针转换
在不安全的上下文中,可用的隐式转换(隐式转换)集合被扩展为包括以下隐式指针转换:
- 从任何pointer_type到类型
void*
。 - 从
null
文字到任何pointer_type。
此外,在不安全的上下文中,可扩展的可用显式转换(显式转换)集扩展为包括以下显式指针转换:
- 从任何pointer_type到任何其他pointer_type。
- 从
sbyte
,byte
,short
,ushort
,int
,uint
,long
,或ulong
以任何POINTER_TYPE。 - 从任何POINTER_TYPE到
sbyte
,byte
,short
,ushort
,int
,uint
,long
,或ulong
。
最后,在不安全的上下文中,标准隐式转换集(标准隐式转换)包括以下指针转换:
- 从任何pointer_type到类型
void*
。
两种指针类型之间的转换永远不会改变实际指针值。换句话说,从一种指针类型到另一种指针类型的转换对指针给出的底层地址没有影响。
当一个指针类型转换为另一个指针类型时,如果生成的指针未针对指向类型正确对齐,则如果取消引用结果,则行为未定义。一般来说,“正确对齐”的概念是传递性的:如果指向类型的指针A
正确地对齐指向类型的指针B
,而指针又正确地对齐指向类型C
的指针,则指向类型的指针A
正确对齐指向类型的指针C
。
考虑以下情况,其中通过指向不同类型的指针访问具有一种类型的变量:
1 char c = 'A'; 2 char* pc = &c; 3 void* pv = pc; 4 int* pi = (int*)pv; 5 int i = *pi; // undefined 6 *pi = 123456; // undefined
当指针类型转换为指向byte的指针时,结果指向变量的最低寻址字节。结果的连续递增,直到变量的大小,产生指向该变量的剩余字节的指针。例如,以下方法将double中的八个字节中的每一个显示为十六进制值:
1 using System; 2 3 class Test 4 { 5 unsafe static void Main() { 6 double d = 123.456e23; 7 unsafe { 8 byte* pb = (byte*)&d; 9 for (int i = 0; i < sizeof(double); ++i) 10 Console.Write("{0:X2} ", *pb++); 11 Console.WriteLine(); 12 } 13 } 14 }
当然,产生的输出取决于字节序。
指针和整数之间的映射是实现定义的。但是,在具有线性地址空间的32 *和64位CPU架构上,指向或来自整数类型的指针的转换通常与这些整数类型的转换uint
或ulong
值分别完全相同。
指针数组
在不安全的上下文中,可以构造指针数组。在指针数组上只允许一些适用于其他数组类型的转换:
- 从任何array_type到它实现的接口的隐式引用转换(隐式引用转换)也适用于指针数组。但是,任何尝试访问数组元素或它实现的接口都会在运行时导致异常,因为指针类型不可转换为。
System.Array
System.Array
object
- 从一维数组类型到其通用基接口的隐式和显式引用转换(隐式引用转换,显式引用转换)从不适用于指针数组,因为指针类型不能用作类型参数,并且没有来自指针类型为非指针类型。
S[]
System.Collections.Generic.IList<T>
- 从它实现的接口到任何array_type的显式引用转换(显式引用转换)适用于指针数组。
System.Array
- 显式引用转换(显式引用转换)
System.Collections.Generic.IList<S>
和它的基本接口到一维数组类型T[]
永远不会应用于指针数组,因为指针类型不能用作类型参数,并且没有指针类型到非指针类型的转换。
这些限制意味着foreach语句中foreach
描述的数组语句的扩展不能应用于指针数组。相反,形式的foreach声明
foreach (V v in x) embedded_statement
其中type x
是表单的数组类型T[,,...,]
,N
是维数减1和T
或是V
指针类型,使用嵌套for循环进行扩展,如下所示:
1 { 2 T[,,...,] a = x; 3 for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++) 4 for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++) 5 ... 6 for (int iN = a.GetLowerBound(N); iN <= a.GetUpperBound(N); iN++) { 7 V v = (V)a.GetValue(i0,i1,...,iN); 8 embedded_statement 9 } 10 }
变量a
,i0
,i1
,...,iN
是不可见的或不可访问x
或embedded_statement或程序的任何其他源代码。该变量v
在嵌入语句中是只读的。如果没有从(元素类型)到显式转换(指针转换),则会产生错误并且不会采取进一步的步骤。如果有值,则在运行时抛出a 。T
V
x
null
System.NullReferenceException
表达式中的指针
在不安全的上下文中,表达式可能会产生指针类型的结果,但在不安全的上下文之外,表达式是指针类型的编译时错误。准确地说,在不安全的上下文之外,如果任何simple_name(简单名称),member_access(成员访问),invocation_expression(调用表达式)或element_access(元素访问)属于指针类型,则会发生编译时错误。
在不安全的上下文中,primary_no_array_creation_expression(主表达式)和unary_expression(一元运算符)产生允许以下附加构造:
1 primary_no_array_creation_expression_unsafe 2 : pointer_member_access 3 | pointer_element_access 4 | sizeof_expression 5 ; 6 7 unary_expression_unsafe 8 : pointer_indirection_expression 9 | addressof_expression 10 ;
这些结构将在以下部分中描述。语法隐含了不安全运算符的优先级和相关性。
指针间接
pointer_indirection_expression由星号(的*
),接着是unary_expression。
1 pointer_indirection_expression 2 : '*' unary_expression 3 ;
一元运算*
符表示指针间接,用于获取指针指向的变量。评估的结果*P
,其中P
是指针类型的表达式,是类型T*
的变量T
。将一元运算*
符应用于类型void*
的表达式或不是指针类型的表达式是一个编译时错误。
将一元运算*
符应用于null
指针的效果是实现定义的。特别是,无法保证此操作会抛出一个System.NullReferenceException
。
如果为指针分配了无效值,则一元运算*
符的行为未定义。在由一元运算*
符取消引用指针的无效值中,一个地址与指向的类型不一致(参见指针转换中的示例),以及变量在其生命周期结束后的地址。
出于明确赋值分析的目的,通过评估表单表达式生成的变量*P
被认为是最初分配的(最初分配的变量)。
指针成员访问
pointer_member_access由一个的primary_expression,接着是“ ->
”令牌,随后的标识符和可选的type_argument_list。
1 pointer_member_access 2 : primary_expression '->' identifier 3 ;
在表单的指针成员访问中P->I
,P
必须是指针类型以外的表达式void*
,并且I
必须表示该类型的可访问成员P
。
表单的指针成员访问P->I
完全被评估为(*P).I
。有关指针间接运算符(*
)的说明,请参阅指针间接寻址。有关成员访问运算符(.
)的说明,请参阅成员访问。
在这个例子中
1 using System; 2 3 struct Point 4 { 5 public int x; 6 public int y; 7 8 public override string ToString() { 9 return "(" + x + "," + y + ")"; 10 } 11 } 12 13 class Test 14 { 15 static void Main() { 16 Point point; 17 unsafe { 18 Point* p = &point; 19 p->x = 10; 20 p->y = 20; 21 Console.WriteLine(p->ToString()); 22 } 23 } 24 }
->
运算符用于通过一个指针访问字段并调用一个结构的方法。因为操作P->I
恰好等同于(*P).I
,所以该Main
方法同样可以编写:
1 class Test 2 { 3 static void Main() { 4 Point point; 5 unsafe { 6 Point* p = &point; 7 (*p).x = 10; 8 (*p).y = 20; 9 Console.WriteLine((*p).ToString()); 10 } 11 } 12 }
指针元素访问
pointer_element_access由一个的primary_no_array_creation_expression随后封闭在“表达[
”和“ ]
”。
1 pointer_element_access 2 : primary_no_array_creation_expression '[' expression ']' 3 ;
在以下形式的指针元素访问P[E]
,P
必须比其他的指针类型的表达式void*
,并且E
必须是可以被隐式转换为表达式int
,uint
,long
,或ulong
。
表单的指针元素访问P[E]
精确地评估为*(P + E)
。有关指针间接运算符(*
)的说明,请参阅指针间接寻址。有关指针加法运算符(+
)的说明,请参见指针运算。
在这个例子中
1 class Test 2 { 3 static void Main() { 4 unsafe { 5 char* p = stackalloc char[256]; 6 for (int i = 0; i < 256; i++) p[i] = (char)i; 7 } 8 } 9 }
指针元素访问用于初始化for
循环中的字符缓冲区。因为操作P[E]
恰好等同于*(P + E)
,所以可以同样编写示例:
1 class Test 2 { 3 static void Main() { 4 unsafe { 5 char* p = stackalloc char[256]; 6 for (int i = 0; i < 256; i++) *(p + i) = (char)i; 7 } 8 } 9 }
指针元素访问运算符不检查越界错误,并且未定义访问越界元素时的行为。这与C和C ++相同。
运算符的地址
一个addressof_expression由符号(&
),接着是unary_expression。
1 addressof_expression 2 : '&' unary_expression 3 ;
给定一个E
类型的表达式T
并将其归类为固定变量(固定和可移动变量),该构造&E
计算由给定的变量的地址E
。结果的类型是T*
和被分类为值。如果E
未归类为变量,则E
归类为编译时错误,如果归类为只读局部变量,或者E
表示可移动变量。在最后一种情况下,固定语句(固定语句)可用于在获取其地址之前临时“修复”该变量。如成员访问中所述,在实例构造函数或定义的结构或类的静态构造函数之外readonly
字段,该字段被视为值,而不是变量。因此,不能采取其地址。同样,不能采用常量的地址。
的&
操作者并不需要它的参数被明确赋值,但以下内容的&
操作中,变量被施加操作者在其中发生动作的执行路径视为已明确赋值。程序员有责任确保在这种情况下确实正确地初始化变量。
在这个例子中
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 int i; 7 unsafe { 8 int* p = &i; 9 *p = 123; 10 } 11 Console.WriteLine(i); 12 } 13 }
i
被认为是在&i
用于初始化的操作之后明确分配的p
。实际的赋值*p
初始化i
,但是包含这个初始化是程序员的责任,如果删除了赋值,则不会发生编译时错误。
&
存在对运算符的明确赋值的规则,从而可以避免局部变量的冗余初始化。例如,许多外部API采用指向由API填充的结构的指针。对此类API的调用通常会传递本地struct变量的地址,如果没有该规则,则需要对struct变量进行冗余初始化。
指针递增和递减
在不安全的上下文中,++
和--
运算符(Postfix递增和递减运算符以及前缀递增和递减运算符)可以应用于除了之外的所有类型的指针变量void*
。因此,对于每个指针类型T*
,隐式定义以下运算符:
1 T* operator ++(T* x); 2 T* operator --(T* x);
运算符产生相同的结果x + 1
和x - 1
,分别为(指针运算)。换句话说,对于类型的指针变量T*
,++
运算符将添加sizeof(T)
到变量中包含的地址,并且--
运算符sizeof(T)
从变量中包含的地址中减去。
如果指针递增或递减操作溢出指针类型的域,则结果是实现定义的,但不会产生异常。
指针算术
在不安全的上下文中,+
和-
运算符(加法运算符和减法运算符)可以应用于除了之外的所有指针类型的值void*
。因此,对于每个指针类型T*
,隐式定义以下运算符:
1 T* operator +(T* x, int y); 2 T* operator +(T* x, uint y); 3 T* operator +(T* x, long y); 4 T* operator +(T* x, ulong y); 5 6 T* operator +(int x, T* y); 7 T* operator +(uint x, T* y); 8 T* operator +(long x, T* y); 9 T* operator +(ulong x, T* y); 10 11 T* operator -(T* x, int y); 12 T* operator -(T* x, uint y); 13 T* operator -(T* x, long y); 14 T* operator -(T* x, ulong y); 15 16 long operator -(T* x, T* y);
给出的表达式P
指针类型的T*
和表达N
类型的int
,uint
,long
,或ulong
,表述P + N
和N + P
计算类型的指针值T*
从增加导致N * sizeof(T)
由给出的地址P
。同样,表达式P - N
计算类型的指针值,该值是从给定的地址中T*
减去N * sizeof(T)
的P
。
给定两个表达式,P
以及Q
指针类型T*
,表达式P - Q
计算由P
和给出的地址之间的差异Q
,然后将差异除以sizeof(T)
。结果的类型总是如此long
。实际上,P - Q
计算为((long)(P) - (long)(Q)) / sizeof(T)
。
例如:
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 unsafe { 7 int* values = stackalloc int[20]; 8 int* p = &values[1]; 9 int* q = &values[15]; 10 Console.WriteLine("p - q = {0}", p - q); 11 Console.WriteLine("q - p = {0}", q - p); 12 } 13 } 14 }
产生输出:
1 p - q = -14 2 q - p = 14
如果指针算术运算溢出指针类型的域,则结果将以实现定义的方式截断,但不会产生异常。
指针比较
在不安全的上下文中,==
,!=
,<
,>
,<=
,和=>
运算符(关系和类型的测试操作员)可以被应用于所有指针类型的值。指针比较运算符是:
1 bool operator ==(void* x, void* y); 2 bool operator !=(void* x, void* y); 3 bool operator <(void* x, void* y); 4 bool operator >(void* x, void* y); 5 bool operator <=(void* x, void* y); 6 bool operator >=(void* x, void* y);
因为从任何指针类型到void*
类型都存在隐式转换,所以可以使用这些运算符比较任何指针类型的操作数。比较运算符将两个操作数给出的地址进行比较,就好像它们是无符号整数一样。
sizeof运算符
的sizeof
运算符返回由给定类型的变量占用的字节的数目。指定为操作数的类型sizeof
必须是unmanaged_type(指针类型)。
1 sizeof_expression 2 : 'sizeof' '(' unmanaged_type ')' 3 ;
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
运算符的结果是实现定义的,并且被分类为值,而不是常量。
成员打包到结构中的顺序未指定。
出于对齐目的,在结构的开头,结构内和结构的末尾可能存在未命名的填充。用作填充的位的内容是不确定的。
当应用于具有结构类型的操作数时,结果是该类型的变量中的总字节数,包括任何填充。
固定的声明
在不安全的上下文中,embedded_statement(Statements)生成允许一个额外的构造,即fixed
语句,用于“修复”可移动变量,使其地址在语句的持续时间内保持不变。
1 fixed_statement 2 : 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement 3 ; 4 5 fixed_pointer_declarators 6 : fixed_pointer_declarator (',' fixed_pointer_declarator)* 7 ; 8 9 fixed_pointer_declarator 10 : identifier '=' fixed_pointer_initializer 11 ; 12 13 fixed_pointer_initializer 14 : '&' variable_reference 15 | expression 16 ;
每个fixed_pointer_declarator声明给定pointer_type的局部变量,并使用相应的fixed_pointer_initializer计算的地址初始化该局部变量。声明中fixed
声明的局部变量可以在该变量声明右侧的任何fixed_pointer_initializer中以及该语句的embedded_statement中访问fixed
。声明声明的局部变量fixed
被认为是只读的。如果嵌入语句试图修改此局部变量(通过赋值或发生编译时间错误++
和--
操作员)或它传递作为ref
或out
参数。
一个fixed_pointer_initializer可以是下列之一:
- 令牌“
&
”后跟一个variable_reference(用于确定明确赋值的精确规则)到非托管类型的可移动变量(固定和可移动变量)T
,前提是该类型T*
可隐式转换为fixed
语句中给出的指针类型。在这种情况下,初始化程序计算给定变量的地址,并保证变量在fixed
语句的持续时间内保持固定的地址。 - 具有非托管类型元素的array_type的表达式
T
,前提是该类型T*
可隐式转换为fixed
语句中指定的指针类型。在这种情况下,初始化程序计算数组中第一个元素的地址,并保证整个数组在fixed
语句的持续时间内保持固定的地址。如果数组表达式为null或者数组的元素为零,则初始化程序计算的地址等于零。 - 类型的表达式
string
,前提是类型char*
可以隐式转换为fixed
语句中给出的指针类型。在这种情况下,初始化程序计算字符串中第一个字符的地址,并保证整个字符串在fixed
语句的持续时间内保持固定的地址。fixed
如果字符串表达式为null ,则语句的行为是实现定义的。 - simple_name或member_access引用可移动变量的固定大小的缓冲部件,设置在固定大小缓冲部件的类型是隐式转换为给定的指针类型
fixed
声明。在这种情况下,初始化程序计算指向固定大小缓冲区的第一个元素的指针(表达式中的固定大小缓冲区),并且保证固定大小缓冲区在fixed
语句的持续时间内保持固定地址。
对于由fixed_pointer_initializer计算的每个地址,该fixed
语句确保该地址引用的变量在fixed
语句持续时间内不受垃圾收集器的重定位或处理。例如,如果fixed_pointer_initializer计算的地址引用了对象的字段或数组实例的元素,则该fixed
语句保证在语句的生命周期内不重定位或处置包含的对象实例。
程序员有责任确保fixed
语句创建的指针不会超出这些语句的执行。例如,当fixed
语句创建的指针传递给外部API时,程序员有责任确保API不保留这些指针的内存。
固定对象可能导致堆碎片(因为它们无法移动)。因此,只有在绝对必要时才能修复物体,然后才能在最短的时间内修复物体。
这个例子
1 class Test 2 { 3 static int x; 4 int y; 5 6 unsafe static void F(int* p) { 7 *p = 1; 8 } 9 10 static void Main() { 11 Test t = new Test(); 12 int[] a = new int[10]; 13 unsafe { 14 fixed (int* p = &x) F(p); 15 fixed (int* p = &t.y) F(p); 16 fixed (int* p = &a[0]) F(p); 17 fixed (int* p = a) F(p); 18 } 19 } 20 }
演示了该fixed
语句的几种用法。第一个语句修复并获取静态字段的地址,第二个语句修复并获取实例字段的地址,第三个语句修复并获取数组元素的地址。在每种情况下,使用常规&
运算符都是错误的,因为变量都被归类为可移动变量。
上例中的第四个fixed
语句产生与第三个类似的结果。
该fixed
语句示例使用string
:
1 class Test 2 { 3 static string name = "xx"; 4 5 unsafe static void F(char* p) { 6 for (int i = 0; p[i] != '\0'; ++i) 7 Console.WriteLine(p[i]); 8 } 9 10 static void Main() { 11 unsafe { 12 fixed (char* p = name) F(p); 13 fixed (char* p = "xx") F(p); 14 } 15 } 16 }
在不安全的上下文中,一维数组的数组元素以递增的索引顺序存储,从索引开始并以索引0
结束Length - 1
。对于多维数组,存储数组元素,使得最右边的维度的索引首先增加,然后是下一个左维度,依此类推到左边。内的fixed
,其获得一个指针的语句p
到数组实例a
中,指针值从p
到p + a.Length - 1
代表数组中的元素的地址。同样地,变量范围从p[0]
到p[a.Length - 1]
代表实际的数组元素。考虑到存储数组的方式,我们可以将任何维度的数组视为线性数组。
例如:
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 int[,,] a = new int[2,3,4]; 7 unsafe { 8 fixed (int* p = a) { 9 for (int i = 0; i < a.Length; ++i) // treat as linear 10 p[i] = i; 11 } 12 } 13 14 for (int i = 0; i < 2; ++i) 15 for (int j = 0; j < 3; ++j) { 16 for (int k = 0; k < 4; ++k) 17 Console.Write("[{0},{1},{2}] = {3,2} ", i, j, k, a[i,j,k]); 18 Console.WriteLine(); 19 } 20 } 21 }
产生输出:
1 [0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3 2 [0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7 3 [0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11 4 [1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15 5 [1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19 6 [1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23
在这个例子中
1 class Test 2 { 3 unsafe static void Fill(int* p, int count, int value) { 4 for (; count != 0; count--) *p++ = value; 5 } 6 7 static void Main() { 8 int[] a = new int[100]; 9 unsafe { 10 fixed (int* p = a) Fill(p, 100, -1); 11 } 12 } 13 }
一个fixed
语句用于修复数组,因此它的地址可以传递给一个带指针的方法。
在示例中:
1 unsafe struct Font 2 { 3 public int size; 4 public fixed char name[32]; 5 } 6 7 class Test 8 { 9 unsafe static void PutString(string s, char* buffer, int bufSize) { 10 int len = s.Length; 11 if (len > bufSize) len = bufSize; 12 for (int i = 0; i < len; i++) buffer[i] = s[i]; 13 for (int i = len; i < bufSize; i++) buffer[i] = (char)0; 14 } 15 16 Font f; 17 18 unsafe static void Main() 19 { 20 Test test = new Test(); 21 test.f.size = 10; 22 fixed (char* p = test.f.name) { 23 PutString("Times New Roman", p, 32); 24 } 25 } 26 }
固定语句用于修复结构的固定大小缓冲区,因此其地址可用作指针。
阿char*
通过固定字符串实例产生值总是指向一个空终止字符串。内的固定语句,其获得的指针p
为一个字符串实例s
中,指针值从p
给p + s.Length - 1
表示字符的地址字符串中,并且指针值p + s.Length
总是指向一个空字符(具有值的字符'\0'
)。
通过固定指针修改托管类型的对象可能导致未定义的行为。例如,因为字符串是不可变的,所以程序员有责任确保不修改指向固定字符串的指针所引用的字符。
当调用期望“C风格”字符串的外部API时,字符串的自动空终止特别方便。但请注意,允许字符串实例包含空字符。如果存在此类空字符,则在将其视为以空值终止时,该字符串将显示为截断char*
。
固定大小缓冲区
固定大小的缓冲区用于将“C样式”内联数组声明为结构的成员,主要用于与非托管API进行交互。
固定大小的缓冲区声明
固定大小缓冲器是表示对于给定类型的变量的固定长度缓冲器的存储的部件。固定大小的缓冲区声明引入给定元素类型的一个或多个固定大小的缓冲区。固定大小的缓冲区仅在结构声明中允许,并且只能在不安全的上下文中发生(不安全的上下文)。
1 struct_member_declaration_unsafe 2 : fixed_size_buffer_declaration 3 ; 4 5 fixed_size_buffer_declaration 6 : attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type fixed_size_buffer_declarator+ ';' 7 ; 8 9 fixed_size_buffer_modifier 10 : 'new' 11 | 'public' 12 | 'protected' 13 | 'internal' 14 | 'private' 15 | 'unsafe' 16 ; 17 18 buffer_element_type 19 : type 20 ; 21 22 fixed_size_buffer_declarator 23 : identifier '[' constant_expression ']' 24 ;
固定大小的缓冲区声明可以包括一组属性(属性),new
修饰符(修饰符),四个访问修饰符(类型参数和约束)的有效组合和unsafe
修饰符(不安全上下文)。属性和修饰符适用于固定大小缓冲区声明声明的所有成员。同一修饰符在固定大小的缓冲区声明中多次出现是错误的。
固定大小的缓冲区声明不允许包含static
修饰符。
固定大小缓冲区声明的缓冲区元素类型指定声明引入的缓冲区的元素类型。缓冲元件类型必须是预定义的类型中的一种sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,或bool
。
缓冲区元素类型后跟一个固定大小的缓冲区声明符列表,每个缓冲区声明符都引入一个新成员。固定大小的缓冲区声明符包含一个标识该成员的标识符,后跟一个括在其中的常量表达式[
和]
标记。常量表达式表示由该固定大小缓冲区声明符引入的成员中的元素数。常量表达式的类型必须可隐式转换为type int
,并且该值必须是非零正整数。
保证固定大小缓冲区的元素在存储器中顺序排列。
声明多个固定大小缓冲区的固定大小缓冲区声明等效于具有相同属性和元素类型的单个固定大小缓冲区声明的多个声明。例如
1 unsafe struct A 2 { 3 public fixed int x[5], y[10], z[100]; 4 }
相当于
1 unsafe struct A 2 { 3 public fixed int x[5]; 4 public fixed int y[10]; 5 public fixed int z[100]; 6 }
修复表达式中的大小缓冲区
固定大小缓冲区成员的成员查找(操作符)与字段的成员查找完全相同。
可以使用simple_name(类型推断)或member_access(动态重载决策的编译时检查)在表达式中引用固定大小的缓冲区。
当固定大小的缓冲区成员作为简单名称引用时,效果与表单的成员访问权限相同this.I
,其中I
是固定大小的缓冲区成员。
在表单的成员访问中E.I
,如果E
是结构类型并且该结构类型中的成员查找I
标识了固定大小的成员,则E.I
评估分类如下:
- 如果表达式
E.I
未在不安全的上下文中发生,则会发生编译时错误。 - 如果
E
被归类为值,则发生编译时错误。 - 否则,如果
E
是可移动变量(固定和可移动变量)且表达式E.I
不是fixed_pointer_initializer(固定语句),则发生编译时错误。 - 否则,
E
引用一个固定变量和表达式的结果是一个指针到固定大小缓冲部件的第一元件I
在E
。结果是类型S*
,其中S
元素类型为I
,并且被分类为值。
可以使用来自第一个元素的指针操作来访问固定大小缓冲区的后续元素。与访问数组不同,访问固定大小缓冲区的元素是一种不安全的操作,不进行范围检查。
以下示例声明并使用具有固定大小缓冲区成员的结构。
1 unsafe struct Font 2 { 3 public int size; 4 public fixed char name[32]; 5 } 6 7 class Test 8 { 9 unsafe static void PutString(string s, char* buffer, int bufSize) { 10 int len = s.Length; 11 if (len > bufSize) len = bufSize; 12 for (int i = 0; i < len; i++) buffer[i] = s[i]; 13 for (int i = len; i < bufSize; i++) buffer[i] = (char)0; 14 } 15 16 unsafe static void Main() 17 { 18 Font f; 19 f.size = 10; 20 PutString("Times New Roman", f.name, 32); 21 } 22 }
明确的赋值检查
固定大小的缓冲区不受明确的赋值检查(定义赋值),并且为了对结构类型变量进行明确的赋值检查,忽略固定大小的缓冲区成员。
当固定大小缓冲区成员的最外层包含struct变量是静态变量,类实例的实例变量或数组元素时,固定大小缓冲区的元素将自动初始化为其默认值(默认值)。在所有其他情况下,固定大小缓冲区的初始内容是未定义的。
堆栈分配
在不安全的上下文中,局部变量声明(局部变量声明)可能包括一个堆栈分配初始化器,它从调用堆栈中分配内存。
1 local_variable_initializer_unsafe 2 : stackalloc_initializer 3 ; 4 5 stackalloc_initializer 6 : 'stackalloc' unmanaged_type '[' expression ']' 7 ;
所述unmanaged_type指示将被存储在新分配的位置的物品的类型和表达表明这些项目的数目。总之,这些指定了所需的分配大小。由于堆栈分配的大小不能为负,因此将项目数指定为计算为负值的constant_expression是编译时错误。
表单的堆栈分配初始值设定项stackalloc T[E]
需要T
是非托管类型(指针类型)并且E
是类型的表达式int
。该构造E * sizeof(T)
从调用堆栈分配字节,并将类型的指针返回T*
给新分配的块。如果E
是负值,则行为未定义。如果E
为零,则不进行分配,并且返回的指针是实现定义的。如果没有足够的可用内存来分配给定大小的块,System.StackOverflowException
则抛出a。
新分配的内存的内容未定义。
堆栈分配初始值设定项不允许在块catch
或finally
块中(try语句)。
没有办法显式释放使用分配的内存stackalloc
。在该函数成员返回时,将自动丢弃在执行函数成员期间创建的所有堆栈分配的内存块。这对应于alloca
函数,这是C和C ++实现中常见的扩展。
在这个例子中
1 using System; 2 3 class Test 4 { 5 static string IntToString(int value) { 6 int n = value >= 0? value: -value; 7 unsafe { 8 char* buffer = stackalloc char[16]; 9 char* p = buffer + 16; 10 do { 11 *--p = (char)(n % 10 + '0'); 12 n /= 10; 13 } while (n != 0); 14 if (value < 0) *--p = '-'; 15 return new string(p, 0, (int)(buffer + 16 - p)); 16 } 17 } 18 19 static void Main() { 20 Console.WriteLine(IntToString(12345)); 21 Console.WriteLine(IntToString(-999)); 22 } 23 }
一个stackalloc
初始值设定在所使用的IntToString
方法来分配的16个字符缓冲器中的堆栈中。方法返回时自动丢弃缓冲区。
动态内存分配
除了stackalloc
运算符之外,C#没有提供用于管理非垃圾收集内存的预定义构造。此类服务通常由支持类库提供或直接从底层操作系统导入。例如,Memory
下面的类说明了如何从C#访问底层操作系统的堆函数:
1 using System; 2 using System.Runtime.InteropServices; 3 4 public unsafe class Memory 5 { 6 // Handle for the process heap. This handle is used in all calls to the 7 // HeapXXX APIs in the methods below. 8 static int ph = GetProcessHeap(); 9 10 // Private instance constructor to prevent instantiation. 11 private Memory() {} 12 13 // Allocates a memory block of the given size. The allocated memory is 14 // automatically initialized to zero. 15 public static void* Alloc(int size) { 16 void* result = HeapAlloc(ph, HEAP_ZERO_MEMORY, size); 17 if (result == null) throw new OutOfMemoryException(); 18 return result; 19 } 20 21 // Copies count bytes from src to dst. The source and destination 22 // blocks are permitted to overlap. 23 public static void Copy(void* src, void* dst, int count) { 24 byte* ps = (byte*)src; 25 byte* pd = (byte*)dst; 26 if (ps > pd) { 27 for (; count != 0; count--) *pd++ = *ps++; 28 } 29 else if (ps < pd) { 30 for (ps += count, pd += count; count != 0; count--) *--pd = *--ps; 31 } 32 } 33 34 // Frees a memory block. 35 public static void Free(void* block) { 36 if (!HeapFree(ph, 0, block)) throw new InvalidOperationException(); 37 } 38 39 // Re-allocates a memory block. If the reallocation request is for a 40 // larger size, the additional region of memory is automatically 41 // initialized to zero. 42 public static void* ReAlloc(void* block, int size) { 43 void* result = HeapReAlloc(ph, HEAP_ZERO_MEMORY, block, size); 44 if (result == null) throw new OutOfMemoryException(); 45 return result; 46 } 47 48 // Returns the size of a memory block. 49 public static int SizeOf(void* block) { 50 int result = HeapSize(ph, 0, block); 51 if (result == -1) throw new InvalidOperationException(); 52 return result; 53 } 54 55 // Heap API flags 56 const int HEAP_ZERO_MEMORY = 0x00000008; 57 58 // Heap API functions 59 [DllImport("kernel32")] 60 static extern int GetProcessHeap(); 61 62 [DllImport("kernel32")] 63 static extern void* HeapAlloc(int hHeap, int flags, int size); 64 65 [DllImport("kernel32")] 66 static extern bool HeapFree(int hHeap, int flags, void* block); 67 68 [DllImport("kernel32")] 69 static extern void* HeapReAlloc(int hHeap, int flags, void* block, int size); 70 71 [DllImport("kernel32")] 72 static extern int HeapSize(int hHeap, int flags, void* block); 73 }
Memory
下面给出了使用该类的示例:
1 class Test 2 { 3 static void Main() { 4 unsafe { 5 byte* buffer = (byte*)Memory.Alloc(256); 6 try { 7 for (int i = 0; i < 256; i++) buffer[i] = (byte)i; 8 byte[] array = new byte[256]; 9 fixed (byte* p = array) Memory.Copy(buffer, p, 256); 10 } 11 finally { 12 Memory.Free(buffer); 13 } 14 for (int i = 0; i < 256; i++) Console.WriteLine(array[i]); 15 } 16 } 17 }
该示例分配256字节的内存Memory.Alloc
并初始化内存块,其值从0增加到255.然后,它分配256个元素的字节数组,并用于Memory.Copy
将内存块的内容复制到字节数组中。最后,释放内存块,Memory.Free
并在控制台上输出字节数组的内容。