C# 试题 47:unsafe 与原生互操作
0921 以下代码能否编译?
难度: 实战
byte[] bytes = new byte[8];
unsafe
{
fixed (int* p = bytes)
{
// ...
}
}
- A. 编译通过,
byte[]可以隐式转换为int* - B. 编译通过,
p指向数组起始字节的地址 - C. 编译失败 CS0266:
byte[]只能固定为byte*,指针类型必须与元素类型一致 - D. 运行抛
ArgumentException
查看答案与解析
正确答案C
解析: fixed 的数组到指针转换是“元素类型数组 → 元素类型指针”,byte[] 只隐式转换为 byte*,不能直接当 int*。想按 int* 读取必须显式转换:fixed (int* p = (int*)...) 或先取 byte* 再转型。CS0266 提示存在显式转换,说明隐式路径不存在。
关键点: fixed 的指针类型必须与数组元素类型对应。
0922 以下代码输出什么?
难度: 进阶
int[] arr = null;
unsafe
{
fixed (int* p = arr)
{
Console.WriteLine(p == null);
}
}
- A. 输出
True:fixed对 null 数组产生空指针,不抛异常 - B. 运行抛
NullReferenceException - C. 输出
False - D. 编译失败,不能对 null 数组使用
fixed
查看答案与解析
正确答案A
解析: fixed 初始化表达式对 null 数组不“钉住”任何对象,指针变量直接置为 null,整个语句合法且不抛异常。写互操作代码时要注意:p == null 是合法的空指针判断,不要额外做防御性抛错。
关键点: fixed null 数组得到空指针,是定义好的行为。
0923 以下代码输出什么?
难度: 进阶
string s = "abc";
Span<int> empty = Span<int>.Empty;
unsafe
{
fixed (char* p = s)
{
Console.WriteLine(*p);
}
fixed (int* q = empty)
{
Console.WriteLine(q == null);
}
}
- A. 输出
a与False - B. 输出
a与True:字符串固定为char*指向首字符;空Span固定得到空指针 - C. 编译失败,
Span<T>不能用于fixed - D. 运行抛
ArgumentException
查看答案与解析
正确答案B
解析: 字符串可 fixed 为 char*,指向首字符 'a';Span<T> 通过 GetPinnableReference() 支持固定,空 Span 的固定引用为 null,因此 q == null 为 True。这两条都是固定语法的标准形态,不是异常场景。
关键点: fixed 支持字符串与 ref struct,空 Span 得到空指针。
0924 关于 fixed 语句的作用对象,哪项正确?
难度: 进阶
- A.
fixed只能用于数组 - B.
fixed的作用是临时禁用 GC - C.
fixed块结束后指针仍然有效 - D.
fixed钉住托管对象(数组、字符串、引用类型字段)防止 GC 搬移;栈上局部变量取址无需fixed;ref struct通过GetPinnableReference支持固定
查看答案与解析
正确答案D
解析: fixed 的目的是把堆上对象“钉住”,使 GC 压缩时不搬移,从而让非托管代码安全使用指针。取栈上局部变量的地址不需要 fixed(不在 GC 堆上);取托管对象字段地址则必须 fixed。固定只持续到 fixed 块结束,块外指针失效。
关键点: 只有托管堆对象才需要 fixed。
0925 以下代码运行结果是什么?
难度: 进阶
int[] arr = new int[3];
unsafe
{
fixed (int* p = arr)
{
p[5] = 1;
}
}
- A. 通常不会抛托管异常:指针索引
p[5]不做边界检查,属于未定义行为,可能写坏相邻内存 - B. 抛
IndexOutOfRangeException - C. 编译失败,指针索引越界
- D. 数组自动扩容为 6 个元素
查看答案与解析
正确答案A
解析: p[5] 等价于 *(p + 5),是裸指针运算,没有任何边界检查,越界写入属于未定义行为:可能落在对象分配区内“成功”写入并损坏相邻数据,也可能触发访问违规。它与托管数组下标 arr[5](必有检查)行为完全不同。
关键点: 指针索引无边界检查,越界是未定义行为。
0926 以下代码运行结果是什么?
难度: 进阶
int[] arr = new int[3];
unsafe
{
fixed (int* p = arr)
{
arr[3] = 1; // ①
p[3] = 1; // ②
}
}
- A. 两处都抛
IndexOutOfRangeException - B. 两处都正常执行
- C. ① 抛
IndexOutOfRangeException:数组下标访问仍有边界检查;② 不会执行,指针索引无检查 - D. ① 编译失败
查看答案与解析
正确答案C
解析: arr[3] 是托管数组访问,运行时执行边界检查,越界抛 IndexOutOfRangeException;p[3] 是 *(p+3) 裸指针访问,无检查,但它在 ① 抛出异常之后,根本不会执行。同一数组、两种访问路径,检查语义截然不同。
关键点: 数组下标有检查,指针索引无检查,二者不可混同。
0927 以下代码运行结果是什么?
难度: 进阶
int* p = null;
unsafe
{
*p = 1;
}
- A. 编译失败,不能对 null 指针解引用
- B. 静默忽略赋值
- C. 抛
InvalidOperationException - D. 运行时抛出
NullReferenceException(未处理时进程以访问违规退出):指针解引用没有托管边界检查
查看答案与解析
正确答案D
解析: 裸指针解引用不做托管检查,写入地址 0 触发访问违规,.NET Core 运行时会把它转化为 NullReferenceException;若未捕获,进程以访问违规码退出。这既不是托管数组的优雅异常,也不可静默忽略——unsafe 代码必须自己保证指针有效。
关键点: null 指针解引用是硬错误,靠调用方保证指针有效性。
0928 以下代码输出什么?
难度: 进阶
unsafe
{
int* p = stackalloc int[4];
int* q = p + 2;
Console.WriteLine(q - p);
Console.WriteLine((long)q - (long)p);
}
- A. 输出 2 与 2
- B. 输出 2 与 8:指针减法结果是元素个数,强转
long后才是字节偏移 - C. 输出 8 与 8
- D. 编译失败,指针不能相减
查看答案与解析
正确答案B
解析: 同类型指针相减按“元素个数”计,q - p 为 2;把地址强转为整数再相减才是字节距离,int 占 4 字节,故为 8。用指针差算字节偏移必须乘以 sizeof(int),这是互操作与二进制解析里最常见的换算错误。
关键点: 指针差是元素数,字节差需要乘元素大小。
0929 以下代码能否编译?
难度: 实战
[LibraryImport("user32.dll")]
static extern int Foo(int x);
- A. 编译失败 SYSLIB1050:
LibraryImport方法必须声明为static partial且非泛型 - B. 编译通过,与
DllImport完全等价 - C. 编译通过,但运行抛
EntryPointNotFoundException - D. 编译通过,运行时才生成封送桩
查看答案与解析
正确答案A
解析: [LibraryImport] 面向源生成器:方法必须是 static partial 且非泛型(还要放在 partial 类型中),否则生成器报 SYSLIB1050 并忽略该方法。与 DllImport 不同,这里不需要也不能用 extern 声明,实现由编译期生成。
关键点: LibraryImport 的正确形态是 static partial,不是 extern。
0930 以下代码能否编译?
难度: 实战
public partial class Native
{
[LibraryImport("user32.dll")]
private static partial int Foo(int x);
}
- A. 编译失败,方法必须带
extern修饰符 - B. 编译失败,包含类型必须是
sealed - C. 编译通过:
LibraryImport要求static partial方法,封送实现由源生成器在编译期生成 - D. 运行抛
TypeLoadException
查看答案与解析
正确答案C
解析: 这是 LibraryImport 的标准写法:partial 类型 + static partial 方法 + 可访问性修饰符,实现由源生成器在编译期补齐,不存在运行时的 DllImport stub。相比 DllImport,它没有运行时解析与封送开销,且编译期即可校验签名。
关键点: 编译期源生成是 LibraryImport 与 DllImport 的本质区别。
0931 以下代码能否编译?
难度: 进阶
public partial class Native
{
[LibraryImport("user32.dll")]
private static partial int Foo<T>(T x);
}
- A. 编译通过,泛型 P/Invoke 受支持
- B. 编译通过,但运行抛异常
- C. 编译失败,因为缺少
extern - D. 编译失败 SYSLIB1050:
LibraryImport方法必须是非泛型
查看答案与解析
正确答案D
解析: LibraryImport 明确要求方法非泛型:泛型参数无法确定封送形态,源生成器无法生成实现,报 SYSLIB1050。这也连带触发了“分部方法必须有实现部分”的 CS8795,但根因就是泛型不受支持。
关键点: LibraryImport 方法禁止泛型。
0932 关于以下 LibraryImport 的字符串封送,哪项正确?
难度: 进阶
public partial class Native
{
[LibraryImport("libc.so")]
private static partial int puts(string s);
}
- A. 默认按 ANSI 代码页封送,与
DllImport默认行为相同 - B.
LibraryImport默认按 UTF-8 封送string,与DllImport的默认行为不同;需要时可显式指定StringMarshalling - C. 字符串参数会先复制为
StringBuilder再传入 - D.
LibraryImport不支持字符串参数
查看答案与解析
正确答案B
解析: LibraryImport 的字符串默认封送是 UTF-8,而 DllImport 默认按 CharSet(通常是 ANSI/Unicode)处理——同一份签名在两个特性下行为可能不同。可以通过 StringMarshalling = StringMarshalling.Utf16 等属性显式覆盖,移植旧代码时这是最常见的隐性差异。
关键点: LibraryImport 默认 UTF-8,与 DllImport 默认不一致。
0933 以下代码能否编译?
难度: 实战
[UnmanagedCallersOnly]
static int Add(int a, int b) => a + b;
int r = Add(1, 2);
- A. 编译失败 CS8901:标记
UnmanagedCallersOnly的方法不能从托管代码直接调用,只能取其函数指针 - B. 编译通过,托管代码可以直接调用
- C. 运行抛
InvalidOperationException - D. 编译通过,但返回 0
查看答案与解析
正确答案A
解析: [UnmanagedCallersOnly] 的方法是给非托管回调用的,编译器禁止从托管代码直接调用(CS8901),提示“请获取指向此方法的函数指针”。这与 DllImport 的外部函数完全不同——它是托管方法反向暴露给本机代码。
关键点: UnmanagedCallersOnly 方法只能经函数指针调用。
0934 以下代码输出什么?
难度: 实战
[UnmanagedCallersOnly]
static int Add(int a, int b) => a + b;
unsafe
{
delegate* unmanaged<int, int, int> fp = &Add;
Console.WriteLine(fp(1, 2));
}
- A. 编译失败,函数指针不能指向
UnmanagedCallersOnly方法 - B. 编译通过,但运行抛
InvalidOperationException - C. 输出 3:函数指针直接调用,无委托分配
- D. 输出 0
查看答案与解析
正确答案C
解析: UnmanagedCallersOnly 方法通过 & 取函数指针(delegate* unmanaged<...>)后即可调用,fp(1, 2) 输出 3。相比 Marshal.GetFunctionPointerForDelegate,函数指针不产生委托实例与 GC 根,零分配,适合高频回调场景。
关键点: 函数指针是 UnmanagedCallersOnly 的调用通道。
0935 以下代码能否编译?
难度: 进阶
[UnmanagedCallersOnly]
static int Sum(ref int a, int b) => a + b;
- A. 编译通过,
ref参数常用于互操作回调 - B. 编译通过,但运行抛异常
- C. 编译失败,因为方法不是
partial - D. 编译失败 CS8977:
UnmanagedCallersOnly方法的签名中不能使用ref/in/out
查看答案与解析
正确答案D
解析: UnmanagedCallersOnly 要求参数全部为可 blittable 的非托管类型且按值传递,ref/in/out 一律禁止(CS8977)。需要传引用时改用指针参数(int*),这也是非托管 ABI 下更明确的表达。
关键点: 回调签名里用指针代替 ref 参数。
0936 关于 UnmanagedCallersOnly 的适用约束,哪项正确?
难度: 进阶
- A. 可以标记实例方法与泛型方法,只要参数是 blittable
- B. 要求
static、非泛型、参数全为可 blittable 的非托管类型、无ref/out;不能从托管代码直接调用,只能通过函数指针,从而避免委托分配与封送开销 - C. 只能用于返回
void的方法 - D. 必须与
DllImport组合使用
查看答案与解析
正确答案B
解析: UnmanagedCallersOnly 的硬性约束:静态方法、非泛型、无托管类型参数、无 ref/in/out、不能直接调用。它把托管静态方法暴露给非托管 ABI,跳过委托与封送层,是自包含的互操作原语,不需要也不应该配 DllImport。
关键点: 约束都是围绕“无托管语义的裸 ABI”设计的。
0937 以下代码输出什么?
难度: 进阶
class H : SafeHandle
{
public H(IntPtr p, bool owns) : base(p, owns) { }
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
Console.WriteLine("released");
return true;
}
}
var h1 = new H(new IntPtr(1), ownsHandle: true);
h1.Dispose();
var h2 = new H(new IntPtr(2), ownsHandle: false);
h2.Dispose();
- A. 只打印一次
released:ownsHandle: false的实例不负责关闭底层句柄 - B. 打印两次
released - C. 编译失败,
ownsHandle不是合法构造参数 - D. 运行抛
ObjectDisposedException
查看答案与解析
正确答案A
解析: SafeHandle 构造函数的第二个参数是所有权标志:ownsHandle: true 时 Dispose 会调用 ReleaseHandle 关闭句柄;false 时只释放包装本身,不触碰底层句柄。所以 h2.Dispose() 不打印 released。所有权语义决定释放行为,是 SafeHandle 最容易误解的点。
关键点: ownsHandle=false 表示“只借不还”,不关闭原生句柄。
0938 以下代码输出什么?
难度: 实战
IntPtr p = new IntPtr(42);
using var a = new H(p, ownsHandle: true);
using var b = new H(p, ownsHandle: true);
(H 的 ReleaseHandle 打印 released 并返回 true)
- A. 只打印一次
released - B. 编译失败,同一句柄不能包装两次
- C. 打印两次
released:同一个原生句柄被两个ownsHandle: true的 SafeHandle 包装,作用域结束时各释放一次,形成双重关闭 - D. 运行抛
InvalidOperationException
查看答案与解析
正确答案C
解析: 两个 ownsHandle: true 的 SafeHandle 包装同一原生句柄时,各自认为自己拥有它,作用域结束按逆序 Dispose,ReleaseHandle 被执行两次——真实场景中就是双重 close,可能导致句柄被复用后意外关闭。SafeHandle 所有权必须唯一,需要共享时只允许一方持有所有权。
关键点: 句柄所有权必须唯一,重复包装 = 双重释放。
0939 以下代码输出什么?
难度: 进阶
var h = new H(new IntPtr(9), ownsHandle: true);
h.SetHandleAsInvalid();
h.Dispose();
- A. 打印
released - B. 不打印
released:SetHandleAsInvalid标记句柄无效后,释放路径被跳过 - C. 运行抛
ObjectDisposedException - D. 编译失败
查看答案与解析
正确答案B
解析: SetHandleAsInvalid() 把 SafeHandle 标记为“句柄已无效”,后续 Dispose/Close 不会再调用 ReleaseHandle(不会重复关闭)。它适用于“句柄已被其他途径关闭/失效”的场景,防止二次释放。这是 SafeHandle 所有权管理中的常用逃生口。
关键点: SetHandleAsInvalid 之后的释放路径被跳过。
0940 关于 SafeHandle 的所有权与释放契约,哪项正确?
难度: 进阶
- A. SafeHandle 无论
ownsHandle为何值都会关闭底层句柄 - B.
Dispose永远不会调用ReleaseHandle - C. 必须手动调用底层
CloseHandle才能完成释放 - D.
ownsHandle决定 SafeHandle 是否负责关闭原生句柄;Dispose/Close触发ReleaseHandle,终结器兜底,DangerousAddRef/DangerousRelease管理引用计数
查看答案与解析
正确答案D
解析: SafeHandle 是句柄生命周期的权威管理者:Dispose/Close 走 ReleaseHandle,未显式释放时终结器兜底;ownsHandle 决定是否真正关闭;DangerousGetHandle 绕过保护,必须配合 DangerousAddRef/DangerousRelease 的引用计数使用。这些机制共同防止句柄泄漏与双重关闭。
关键点: 所有权 + 引用计数 + 终结器构成 SafeHandle 的完整契约。