18不安全代码
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 413
18. 不安全代码
如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C++ 有着显著的区
别。作为替代,C# 提供了各种引用类型,并能够创建可由垃圾回收器管理的对象。这种设计结合其他
功能,使 C# 成为比 C ...
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 413
18. 不安全代码
如前面几章所定义,核心 C# 语言没有将指针列入它所支持的数据类型,从而与 C 和 C++ 有着显著的区
别。作为替代,C# 提供了各种引用类型,并能够创建可由垃圾回收器管理的对象。这种设计结合其他
功能,使 C# 成为比 C 或 C++ 安全得多的语言。在核心 C# 语言中,不可能有未初始化的变量、“虚”
指针或者超过数组的界限对其进行索引的表达式。这样,以往总是不断地烦扰 C 和 C++ 程序的一系列
错误就不会再出现了。
尽管实际上对 C 或 C++ 中的每种指针类型构造,C# 都设置了与之对应的引用类型,但仍然会有一些场
合需要访问指针类型。例如,当需要与基础操作系统进行交互、访问内存映射设备,或实现一些以时间
为关键的算法时,若没有访问指针的手段,就不可能或者至少很难完成。为了满足这样的需求,C# 提
供了编写不安全代码 (unsafe code) 的能力。
在不安全代码中,可以声明和操作指针,可以在指针和整型之间执行转换,还可以获取变量的地址,等
等。在某种意义上,编写不安全代码很像在 C# 程序中编写 C 代码。
无论从开发人员还是从用户角度来看,不安全代码事实上都是一种“安全”功能。不安全代码必须用修
饰符 unsafe 明确地标记,这样开发人员就不会误用不安全功能,而执行引擎将确保不会在不受信任的
环境中执行不安全代码。
18.1 不安全上下文
C# 的不安全功能仅用于不安全上下文中。不安全上下文是通过在类型或成员的声明中包含一个 unsafe
修饰符或者通过使用 unsafe-statement 引入的:
类、结构、接口或委托的声明可以包含一个 unsafe 修饰符,在这种情况下,该类型声明的整个文
本范围(包括类、结构或接口的体)被认为是不安全上下文。
在字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数或静态构造函数的声明
中,也可以包含一个 unsafe 修饰符,在这种情况下,该成员声明的整个文本范围被认为是不安全
上下文。
unsafe-statement 使得可以在 block 内使用不安全上下文。该语句关联的 block 的整个文本范围被认为
是不安全上下文。
下面显示了关联的语法扩展。为简单起见,我们使用省略号(...)表示前面章节中出现的产生式。
class-modifier:
...
unsafe
struct-modifier:
...
unsafe
interface-modifier:
...
unsafe
C# 语言规范
414 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
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;
}
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 415
在结构声明中指定的 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 中的情况)。
18.2 指针类型
在不安全上下文中,type(第 4 章)可以是 pointer-type,也可以是 value-type 或 reference-type。但是,
pointer-type 也可以在不安全上下文外部的 typeof 表达式(第 7.6.10.6 节)中使用,因为此类使用不是
不安全的。
type:
...
pointer-type
pointer-type 可表示为 unmanaged-type 后接一个 * 标记,或者关键字 void 后接一个 * 标记:
C# 语言规范
416 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
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 表示 int 变量,该变量的地址就是 P 的值。
与对象引用类似,指针可以是 null。如果将间接寻址运算符应用于 null 指针,则其行为将由实现自己
定义。值为 null 的指针表示为将该指针的所有位都置零。
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 417
void* 类型表示指向未知类型的指针。因为目标类型是未知的,所以间接寻址运算符不能应用于 void*
类型的指针,也不能对这样的指针执行任何算术运算。但是,void* 类型的指针可以强制转换为任何其
他指针类型(反之亦然)。
指针类型是一个单独类别的类型。与引用类型和值类型不同,指针类型不从 object 继承,而且不存在
指针类型和 object 之间的转换。具体而言,指针不支持装箱和取消装箱(第 4.3 节)操作。但是,允
许在不同指针类型之间以及指针类型与整型之间进行转换。第 18.4 节中对此进行了介绍。
pointer-type 不能用作类型实参(第 4.4 节),且类型推断(第 7.5.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),下面的方法将在该整数序列中查找与该 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 节)。
C# 语言规范
418 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
-> 运算符可用于通过指针访问结构的成员(第 18.5.2 节)。
[] 运算符可用于索引指针(第 18.5.3 节)。
& 运算符可用于获取变量的地址(第 18.5.4 节)。
++ 和 -- 运算符可以用于递增和递减指针(第 18.5.5 节)。
+ 和 - 运算符可用于执行指针算术运算(第 18.5.6 节)。
==、!=、<、>、<= 和 => 运算符可以用于比较指针(第 18.5.7 节)。
stackalloc 运算符可用于从调用堆栈中分配内存(第 18.7 节)。
fixed 语句可用于临时固定一个变量,以便可以获取它的地址(第 18.6 节)。
18.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-element-access(第 18.5.1 节)、P->I 形式的 pointer-member-access(第 18.5.2
节)或 P[E] 形式的 pointer-element-access(第 18.5.3 节)表示的变量。
所有其他变量都属于可移动变量。
请注意静态字段属于可移动变量。还请注意即使赋予 ref 或 out 形参的实参是固定变量,它们仍属于
可移动变量。最后请注意,由取消指针引用而产生的变量总是属于固定变量。
18.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 节)的集合包括以下指针转换:
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 419
从任何 pointer-type 到 void* 类型。
两个指针类型之间的转换永远不会更改实际的指针值。换句话说,从一个指针类型到另一个指针类型的
转换不会影响由指针给出的基础地址。
当一个指针类型被转换为另一个指针类型时,如果没有将得到的指针正确地对指向的类型对齐,则当结
果被取消引用时,该行为将是未定义的。一般情况下,“正确对齐”的概念具有传递性:如果指向类型
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 类型的值与这些整型之间的对应方向上的转换具有完全相同
的行为。
18.4.1 指针数组
可以在不安全上下文中构造指针数组。只有一部分适用于其他数组类型的转换适用于指针数组:
从任意 array-type 到 System.Array 及其实现的接口的隐式引用转换(第 6.1.6 节)也适用于指针数
组。但是,由于指针类型不可转换为 object,因此只要尝试通过 System.Array 或其实现的接口访
问数组元素,就会导致在运行时出现异常。
从一维数组类型 S[] 到 System.Collections.Generic.IList
及其基接口的隐式和显式引用
转换(第 6.1.6、6.2.4 节)在任何情况下均不适用于指针数组,因为指针类型不能用作类型实参,且
不存在从指针类型到非指针类型的转换。
从 System.Array 及其实现的接口到任意 array-type 的显式引用转换(第 6.2.4 节)均适用于指针数组。
C# 语言规范
420 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
从 System.Collections.Generic.IList 及其基接口到一维数组类型 T[] 的显式引用转换
(第 6.2.4 节)在任何情况下均不适用于指针数组,因为指针类型无法用作类型实参,且不存在从指
针类型到非指针类型的转换。
这些限制意味着通过第 8.8.4 节中中给出的数组对 foreach 语句进行的扩展不能用于指针数组。而下列
形式的 foreach 语句
foreach (V v in x) embedded-statement
(其中 x 的类型为具有 T[,,…,] 形式的数组类型,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。
18.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
以下几节对这些构造进行了描述。相关的语法暗示了不安全运算符的优先级和结合性。
18.5.1 指针间接寻址
pointer-indirection-expression 包含一个星号 (*),后接一个 unary-expression。
pointer-indirection-expression:
* unary-expression
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 421
一元 * 运算符表示指针间接寻址并且用于获取指针所指向的变量。计算 *P 得到的结果(其中 P 为指针
类型 T* 的表达式)是类型为 T 的一个变量。将一元 * 运算符应用于 void* 类型的表达式或者应用于不
是指针类型的表达式属于编译时错误。
将一元 * 运算符应用于 null 指针的效果是由实现定义的。具体而言,不能保证此操作会引发
System.NullReferenceException。
如果已经将无效值赋给指针,则一元 * 运算符的行为是未定义的。通过一元 * 运算符取消指针引用有时
会产生无效值,这些无效值包括:没能按所指向的类型正确对齐的地址(请参见第 18.4 节中的示例)
和超过生存期的变量的地址。
出于明确赋值分析的目的,通过计算 *P 形式的表达式产生的变量被认为是初始化赋过值的(第 5.3.1
节)。
18.5.2 指针成员访问
pointer-member-access 包含一个 primary-expression,后接一个“->”标记,最后是一个 identifier。
pointer-member-access:
primary-expression -> identifier
在 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 方法可以等效地编写为:
C# 语言规范
422 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
class Test
{
static void Main() {
Point point;
unsafe {
Point* p = &point;
(*p).x = 10;
(*p).y = 20;
Console.WriteLine((*p).ToString());
}
}
}
18.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++ 相同。
18.5.4 address-of 运算符
addressof-expression 包含一个“and”符 (&),后接一个 unary-expression。
addressof-expression:
& unary-expression
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 423
如果给定类型为 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 进行的调用通常会传递局部结构变量的地址,而如果
没有上述规则,则将需要对此结构变量进行冗余初始化。
18.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)。
如果指针递增或递减运算的结果超过指针类型的域,则结果是由实现定义的,但不会产生异常。
18.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);
C# 语言规范
424 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
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
如果在执行上述指针算法时,计算结果超越该指针类型的域,则将以实现所定义的方式截断结果,但是
不会产生异常。
18.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);
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 425
由于存在从任何指针类型到 void* 类型的隐式转换,因此可以使用这些运算符来比较任何指针类型的
操作数。比较运算符像比较无符号整数一样比较两个操作数给出的地址。
18.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 应用于具有结构类型的操作数时,结果是该类型变量所占的字节总数(包括所有填充位在内)。
18.6 fixed 语句
在不安全上下文中,embedded-statement (第 8 章)产生式允许使用一个附加结构即 fixed 语句,该语
句用于“固定”可移动变量,从而使该变量的地址在语句的持续时间内保持不变。
embedded-statement:
...
fixed-statement
fixed-statement:
fixed ( pointer-type fixed-pointer-declarators ) embedded-statement
C# 语言规范
426 版权所有 Microsoft Corporation 1999-2010。保留所有权利。
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;
第 错误!使用“开始”选项卡将 Heading 1 应用于要在此处显示的文字。 章 错误!使用“开始”选项卡将 Heading
1 应用于要在此处显示的文字。
版权所有 Microsoft Corporation 1999-2010。保留所有权利。 427
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
本文档为【18不安全代码】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。