返回题库高级 .NET 刷题C# 选择题 · 第 47 / 50 篇

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. 输出 Truefixed 对 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. 输出 aFalse
  • B. 输出 aTrue:字符串固定为 char* 指向首字符;空 Span 固定得到空指针
  • C. 编译失败,Span<T> 不能用于 fixed
  • D. 运行抛 ArgumentException
查看答案与解析

正确答案B

解析: 字符串可 fixedchar*,指向首字符 'a'Span<T> 通过 GetPinnableReference() 支持固定,空 Span 的固定引用为 null,因此 q == nullTrue。这两条都是固定语法的标准形态,不是异常场景。

关键点: fixed 支持字符串与 ref struct,空 Span 得到空指针。

0924 关于 fixed 语句的作用对象,哪项正确?

难度: 进阶

  • A. fixed 只能用于数组
  • B. fixed 的作用是临时禁用 GC
  • C. fixed 块结束后指针仍然有效
  • D. fixed 钉住托管对象(数组、字符串、引用类型字段)防止 GC 搬移;栈上局部变量取址无需 fixedref 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] 是托管数组访问,运行时执行边界检查,越界抛 IndexOutOfRangeExceptionp[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. 只打印一次 releasedownsHandle: false 的实例不负责关闭底层句柄
  • B. 打印两次 released
  • C. 编译失败,ownsHandle 不是合法构造参数
  • D. 运行抛 ObjectDisposedException
查看答案与解析

正确答案A

解析: SafeHandle 构造函数的第二个参数是所有权标志:ownsHandle: trueDispose 会调用 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);

HReleaseHandle 打印 released 并返回 true

  • A. 只打印一次 released
  • B. 编译失败,同一句柄不能包装两次
  • C. 打印两次 released:同一个原生句柄被两个 ownsHandle: true 的 SafeHandle 包装,作用域结束时各释放一次,形成双重关闭
  • D. 运行抛 InvalidOperationException
查看答案与解析

正确答案C

解析: 两个 ownsHandle: true 的 SafeHandle 包装同一原生句柄时,各自认为自己拥有它,作用域结束按逆序 DisposeReleaseHandle 被执行两次——真实场景中就是双重 close,可能导致句柄被复用后意外关闭。SafeHandle 所有权必须唯一,需要共享时只允许一方持有所有权。

关键点: 句柄所有权必须唯一,重复包装 = 双重释放。

0939 以下代码输出什么?

难度: 进阶

var h = new H(new IntPtr(9), ownsHandle: true);
h.SetHandleAsInvalid();
h.Dispose();
  • A. 打印 released
  • B. 不打印 releasedSetHandleAsInvalid 标记句柄无效后,释放路径被跳过
  • 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/CloseReleaseHandle,未显式释放时终结器兜底;ownsHandle 决定是否真正关闭;DangerousGetHandle 绕过保护,必须配合 DangerousAddRef/DangerousRelease 的引用计数使用。这些机制共同防止句柄泄漏与双重关闭。

关键点: 所有权 + 引用计数 + 终结器构成 SafeHandle 的完整契约。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 45C# 试题 45:C# 13 新特性20 题
  2. 46C# 试题 46:C# 14 新特性20 题
  3. 47C# 试题 47:unsafe 与原生互操作20 题
  4. 48C# 试题 48:装箱、闭包与性能20 题
  5. 49C# 试题 49:测试、调试与分析器20 题
ESC

输入关键词开始搜索