当前位置: 首页 > news >正文

理解 C# 中的各类指针

前言

变量可以理解成是一块内存位置的别名,访问变量也就是访问对应内存中的数据。

指针是一种特殊的变量,它存储了一个内存地址,这个内存地址代表了另一块内存的位置。

指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存、一个函数等。

截止到发文为止,.NET 最新正式版本为 .NET 9,C# 最新正式版本为 C# 13。文中提及的 IL 代码可能会随编译器版本的不同而有所差异,仅供参考。

本文将介绍到发文为止 C# 中的各类指针,并对比差异:

  • 对象引用(Object Reference)

  • 指针(Pointer,一些资料中称为非托管指针)

  • IntPtr(表示指针或句柄的值,用于管理非托管资源或非托管代码交互)

  • 函数指针(Function Pointer)

  • 托管指针(Managed Pointer)

本文旨在为读者建立对各类指针的概念认知,不会每个细节都展开,读者可以参考 C# 的官方文档,了解更多用法。

涉及的知识点较多,如果存在纰漏和错误,还请谅解。

对象引用(Object Reference)

对象引用,也就是我们常说的引用类型变量,是一个类型安全的指针,指向引用类型实例的 MethodTable 指针,通过偏移和计算可以访问对象头和字段。

对象实例被分配在托管堆上,引用类型变量存储了一个指向该对象实例的引用。对象引用可以被赋值为 null,表示没有指向任何对象实例。通过 null 的对象引用访问不存在的对象会导致 NullReferenceException

对象引用可以存在栈或者堆上,作为局部变量时,存储在栈上;作为值类型字段时,跟随值类型的位置存储;作为引用类型字段时,存储在堆上。

指针(Pointer)

指针的声明和使用

指针允许用户直接操作内存地址,提供了更高的性能和灵活性,但也带来了更高的风险。因此,C# 只允许在用 unsafe 关键字标记的代码块中使用指针,并且需要在项目中启用 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

unsafe 关键字可以用于方法、代码块、字段、类、结构体等。

一些资料中将这边的指针(Pointer)称为非托管指针(Unmanaged Pointer),因为它们不受 GC 的管理。

我们需要使用 <type>* ptr 的语法来声明指针类型的变量。

通过 & 运算符获取变量的地址,通过 * 运算符访问指针指向的数据。

& 通常被称为寻址运算符,* 通常被称为解引用运算符或间接寻址运算符。

unsafe class Program
{static void Main(){int* p = null; // 声明一个指向 int 的指针int a = 10;p = &a; // 获取 a 的地址并赋值给指针 pConsole.WriteLine(*p); // 输出 10}
}

指针可以指向的位置

指针可以指向以下几种位置:

  • 值类型变量:也就是指向值类型的数据本体。

  • 引用类型变量:因为引用类型变量存储的是对象实例的引用,所以这边相当于一个二级指针。

  • 值类型或者引用类型的实例字段:readonly 也可以修改。

  • 值类型或者引用类型的静态字段:readonly 也可以修改。

  • 数组元素:数组在内存中是连续存储的,所以可以通过指针和指针算法来访问数组元素。

  • 非托管内存:使用 Marshal 分配非托管内存。

  • 另一个指针(Pointer):可以实现多级指针。

  • null:表示没有指向任何有效的内存地址,通过 null 指针访问不存在的数据会导致 NullReferenceException

注意:在声明指向实例字段,静态字段以及数组元素的指针时,需要使用 fixed 关键字。

可以声明指针的位置

指针可以在以下位置声明:

  • 局部变量:可以在方法中声明指针变量。

  • 方法参数:可以将指针作为方法参数传递。

  • 方法返回值:可以将指针作为方法的返回值。

  • 实例字段:可以在类或结构体中声明指针类型的字段。

  • 静态字段:可以在类或者结构体中声明指针类型的静态字段。

  • 只读属性:包含只读索引(indexer),但不支持自动属性(Automatically implemented properties)。

指向值类型变量的指针

指针可以指向值类型变量,直接访问值类型的数据本体,并且可以修改值类型变量的值。

unsafe class Program
{static void Main(){int a = 10;int* p = &a; // 获取 a 的地址并赋值给指针 pConsole.WriteLine(*p); // 输出 10*p = 20; // 修改指针 p 指向的值Console.WriteLine(a); // 输出 20}
}

指向对象引用的指针

指针可以指向对象引用,相当于一个二级指针。

在下面的示例代码中,关键的部分标注了编译后的 IL 代码。

class Program
{static void Main(){var foo = new Foo{Bar = 1};unsafe{// ldloca.s     foo   // 加载 foo 的地址// conv.u             // 将 foo 的地址转换为 unsigned native int// stloc.1            // 将转换后的 int 存储到 fooPtrFoo* fooPtr = &foo;// ldloc.1            // 加载 fooPtr// ldind.ref          // 将 fooPtr 指向的对象引用加载到栈上// callvirt     instance int32 Foo::get_Bar()// call         void [System.Console]System.Console::WriteLine(int32)Console.WriteLine(fooPtr->Bar); // 输出 1// ldloc.1            // 加载 fooPtr// newobj       instance void Foo::.ctor()// dup// ldc.i4.2// callvirt     instance void Foo::set_Bar(int32)// nop// stind.ref          // 新的 Foo 对象的地址保存通过 fooPtr 保存到 foo*fooPtr = new Foo{Bar = 2};// ldloc.0      // 和指针相比,少了一个 ldind.ref,对象引用可以直接使用// callvirt     instance int32 Foo::get_Bar()// call         void [System.Console]System.Console::WriteLine(int32)Console.WriteLine(foo.Bar); // 输出 2// ldloc.1      // 加载 fooPtr// ldind.ref    // 将 fooPtr 指向的对象引用加载到栈上// ldc.i4.3     // 将 3 压入栈上// callvirt     instance void Foo::set_Bar(int32)fooPtr->Bar = 3;Console.WriteLine(foo.Bar); // 输出 3}}
}class Foo
{public int Bar { get; set; }
}

关键的三个IL 指令:

  • conv.u:将对象引用(foo)的地址转换为 unsigned native int,并存储到指针(fooPtr)中。

  • ldind.ref:将指针(fooPtr)指向的对象引用(foo)加载到栈上。

  • stind.ref:将栈上的对象引用(新的foo实例的引用)存储到指针指向的地址(foo)上。

指向 GC Heap 的指针

如果指针指向 GC Heap 上的数据,例如指向数组元素或者引用类型实例字段,指针需要通过 fixed 关键字固定对象的地址,防止 GC 移动对象的位置。

class Program
{static void Main(){Foo foo = new Foo{Bar = 1};unsafe{fixed (int* p = &foo.Bar) // 固定 foo.Bar 的地址{Console.WriteLine(*p); // 输出 1*p = 2; // 修改指针 p 指向的值}}Console.WriteLine(foo.Bar); // 输出 2}
}class Foo
{public int Bar;
}

注意:不应在 fixed 语句块结束后,继续使用指针变量,因为 GC 可能会移动对象的位置,导致指针指向无效的内存地址。

class Program
{static void Main(){Foo foo = new Foo{Bar = 1};var weakReference = new WeakReference(foo);unsafe{int* p2;fixed (int* p1 = &foo.Bar) // 固定 foo.Bar 的地址{Console.WriteLine(*p1); // 输出 1p2 = p1; // 将指针 p1 存放的地址复制给 指针p2*p1 = 2; // 修改指针 p1 指向的值}Console.WriteLine(*p2); // 输出 2,此时 p1 已经被释放了,但 p2 仍然可以访问到 foo.Bar 的值// 往托管堆上分配一些数据,并触发 GCfor (int i = 0; i < 1_000_000; i++){var arr = new int[1000];}GC.Collect();Console.WriteLine(weakReference.IsAlive); // 输出 true,证明 foo 仍然存活Console.WriteLine(*p2); // 输出 0, 因为 foo 的位置已经被 GC 移动了}}
}class Foo
{public int Bar;
}

指向数组元素的指针

当指针指向数组元素时,可以通过指针算法遍历数组元素,指针的单次偏移量为元素类型的大小。

指针算法支持的操作有:

对指针进行加法和减法运算时,p + n 是将指针 p 向后移动 n 个元素的大小,p - n 是将指针 p 向前移动 n 个元素的大小。

本文会讨论三种数组类型:

  • 在栈上分配的数组

  • 在托管堆上分配的数组

  • 在非托管堆上分配的数组

本小节先讨论前两种,指向非托管堆上分配的数组的指针会在后面讨论。

栈上和非托管堆上分配的数组时,指针可以直接访问数组元素。在托管堆上分配的数组时,指针需要通过 fixed 关键字固定数组元素的地址,防止 GC 移动数组元素的位置。

在栈上分配的数组的示例代码:

unsafe class Program
{static void Main(){int* arr = stackalloc int[5] { 0, 1, 2, 3, 4 }; // 在栈上分配一个 int 数组并初始化// 下面是等效代码// int* arr = stackalloc int[5]; // 在栈上分配一个 int 数组// for (int i = 0; i < 5; i++)// {//     *(arr + i) = i; // 通过指针访问数组元素,赋值// }for (int i = 0; i < 5; i++){Console.WriteLine(*(arr + i)); // 输出 0 1 2 3 4}// 也可以直接通过下标访问for (int i = 0; i < 5; i++){Console.WriteLine(arr[i]); // 输出 0 1 2 3 4}}
}

在托管堆上分配的数组的示例代码:

unsafe class Program
{static void Main(){int[] arr = new int[5] { 0, 1, 2, 3, 4 }; // 在堆上分配一个 int 数组并初始化fixed (int* p = arr) // 固定数组元素的地址{for (int i = 0; i < 5; i++){Console.WriteLine(*(p + i)); // 输出 0 1 2 3 4}}fixed (int* p = &arr[0]) // 固定数组元素的地址{for (int i = 0; i < 5; i++){*(p + i) = i * 10; // 修改数组元素的值}}foreach (var item in arr){Console.WriteLine(item); // 输出 0 10 20 30 40}}
}

在 fixed 语句块结束后,数组元素的地址会被释放,指针变量将不再有效。

在 fixed 语句块中,指针变量可以直接访问数组元素的地址,并且可以修改数组元素的值。

int* p = arr 和 int* p = &arr[0] 是等效的,都是获取数组第一个元素的地址。

注意: int[]* p = &arr 是创建一个指向数组变量的指针,并不是指向数组元素的指针。

指向静态字段的指针

静态字段位于托管堆上,但非 GC 管理的内存区域,理论上内存地址应该是固定的,但不排除某些平台实现或某些情况下会被移动。

在.NET的规范以及C#语言规范中,编译器并不能完全确定某个字段是否可移动,必须通过 fixed 修饰保证安全。

统一使用 fixed 也可以避免特例导致的复杂性或bug。如果静态保存的是值类型还好。但如果静态字段保存的是一个对象引用,那就和方法的局部变量一样,指针必定需要通过 fixed 关键字固定对象的地址,防止 GC 移动对象的位置。静态字段如果存的是数组的引用,也是必须使用 fixed 关键字固定对象的地址才能访问数组元素。

unsafe class Program
{static void Main(){// 值类型的静态字段Foo.ValueTypeField = 1;// 获取指针fixed (int* valueTypeFieldPtr = &Foo.ValueTypeField){*valueTypeFieldPtr = 2; // 修改值类型字段的值}Console.WriteLine(Foo.ValueTypeField); // 输出 2// 引用类型的静态字段Foo.ReferenceTypeField = new Bar { Baz = 1 };// 获取指针fixed (Bar* referenceTypeFieldPtr = &Foo.ReferenceTypeField){*referenceTypeFieldPtr = new Bar { Baz = 2 }; // 修改引用类型字段的值}Console.WriteLine(Foo.ReferenceTypeField.Baz); // 输出 2// 数组的静态字段Foo.ArrayField = [1, 2, 3];// 获取指针fixed (int* arrayFieldPtr = Foo.ArrayField){arrayFieldPtr[0] = 4; // 修改数组的值}Console.WriteLine(Foo.ArrayField[0]); // 输出 4}
}class Foo
{public static int ValueTypeField;public static Bar ReferenceTypeField;public static int[] ArrayField;
}class Bar
{public int Baz;
}

指向非托管内存的指针

使用 Marshal.AllocHGlobal 分配非托管内存,返回一个指向非托管内存的指针,最后使用 Marshal.FreeHGlobal 释放非托管内存。

Marshal 提供的方法的参数和返回值都是 IntPtr 类型,但可以和指针互换转换。

public static class Marshal
{public static IntPtr AllocHGlobal(int cb);public static void FreeHGlobal(IntPtr hglobal);
}

using System.Runtime.InteropServices;unsafe class Program
{static void Main(){// 在非托管内存中分配一块内存用于存储整数数组int size = 10;var ptr = (int*)Marshal.AllocHGlobal(size * sizeof(int));// 将数据写入非托管内存for (int i = 0; i < size; i++){ptr[i] = i;}// 读取非托管内存的数据for (int i = 0; i < size; i++){Console.WriteLine(ptr[i]);}// 也可以使用指针算法访问非托管内存存储的数组// int* p = ptr;// for (int i = 0; i < size; i++)// {//     Console.WriteLine(*p);//     p++;// }// 释放非托管内存Marshal.FreeHGlobal((IntPtr)ptr);}
}

作为方法参数的指针

指针可以作为方法参数传递,允许在方法中修改指针指向的数据,但指针本身的传递是值传递,无法在传入的方法中修改指针的值,也就是无法修改指针指向的地址。

unsafe class Program
{static void Main(){int a = 10;int b = 20;int* p1 = &a; // 获取 a 的地址并赋值给指针 p1int* p2 = &b; // 获取 b 的地址并赋值给指针 p2Console.WriteLine(*p1); // 输出 10Console.WriteLine(*p2); // 输出 20ModifyPointer(p1, p2); // 传递指针 p1 和 p2Console.WriteLine(*p1); // 输出 11}static void ModifyPointer(int* p1, int* p2){*p1 = 11; // 修改指针 p1 指向的值p1 = p2; // 无效代码,不会影响外部的 p1}
}

作为方法返回值的指针

当指针作为方法的返回值时,需要注意不能返回局部变量的指针,因为局部变量在方法结束后会被销毁,指针将指向无效的内存地址。

unsafe class Program
{static void Main(){Foo* p = GetPointer(); // 获取指针Console.WriteLine(p->Bar); // 输出 10Console.WriteLine(p->Bar); // 输出 随机值}static Foo* GetPointer(){Foo a = new Foo{Bar = 10};return &a;}
}struct Foo
{public int Bar;
}

上述代码中,GetPointer 方法返回了一个指向局部变量 a 的指针,但 a 在方法结束后会被销毁,所以返回的指针将指向无效的内存地址。

之所以第一次输出 10,是因为 a 的内存数据没有被覆盖,第二次输出随机值是因为 a 的内存数据已经被覆盖。

在打印 p->Bar 之前,将一些别的数据载入到栈上,就会覆盖 a 的内存数据。下面的代码只打印了一次 p->Bar,但在打印之前,已经将 20 到过栈上(被 Console.WriteLine 消费了),所以 a 的内存数据被覆盖了。

unsafe class Program
{static void Main(){Foo* p = GetPointer(); // 获取指针Console.WriteLine(20); // 输出 20Console.WriteLine(p->Bar); // 输出 随机值}static Foo* GetPointer(){Foo a = new Foo{Bar = 10};return &a;}
}struct Foo
{public int Bar;
}

改为返回字段的指针也是一样的结果

unsafe class Program
{static void Main(){int* p = GetPointer(); // 获取指针Console.WriteLine(*p); // 输出 10Console.WriteLine(*p); // 输出 随机值}static int* GetPointer(){Foo a = new Foo{Bar = 10};return &a.Bar;}
}struct Foo
{public int Bar;
}

多级指针

下面是一个三级指针的例子

{int x = 1;int* p1 = &x;         // 一级指针int** p2 = &p1;       // 二级指针int*** p3 = &p2;      // 三级指针***p3 = 2;            // 三次寻址Console.WriteLine(x); // 输出 2
}

进一步理解 fixed 关键字

fixed 关键字用于固定对象的地址,防止 GC 移动对象的位置。

查看下面代码编译成的 IL 代码。

unsafe class Program
{static void Main(){// 引用类型的静态字段Foo.ReferenceTypeField = new Bar { Baz = 1 };// 获取指针fixed (Bar* referenceTypeFieldPtr = &Foo.ReferenceTypeField){*referenceTypeFieldPtr = new Bar { Baz = 2 }; // 修改引用类型字段的值}Console.WriteLine(Foo.ReferenceTypeField.Baz); // 输出 2// 数组的静态字段Foo.ArrayField = [1, 2, 3];// 获取指针fixed (int* arrayFieldPtr = Foo.ArrayField){arrayFieldPtr[0] = 4; // 修改数组的值}Console.WriteLine(Foo.ArrayField[0]); // 输出 4}
}class Foo
{public static Bar ReferenceTypeField;public static int[] ArrayField;
}class Bar
{public int Baz;
}

.class private auto ansi beforefieldinitProgramextends [System.Runtime]System.Object
{.method private hidebysig static voidMain() cil managed{.entrypoint.maxstack 4.locals init ([0] class Bar* referenceTypeFieldPtr,[1] class Bar& pinned V_1,[2] int32* arrayFieldPtr,[3] int32[] pinned V_3)// ... 省略方法体}
}

在 IL 代码中,Bar& pinned V_1 和 int32[] pinned V_3 表示固定的指向对象引用的托管指针和固定的数组的对象引用。

pinned 表示这个对象引用是固定的,GC 会识别到这个标记,并不会移动其指向的对象的位置。

在 fixed 语句块内,对 Bar* referenceTypeFieldPtr 的读写将转换为 Bar& pinned V_1 的读写。对 int32* arrayFieldPtr 的读写将转换为 int32[] pinned V_3 的读写。

IntPtr

基本概念

IntPtr 是一个结构体,表示指针或句柄的值,用于管理非托管资源或非托管代码交互。

在部分场景,可以和指针互换使用,但 IntPtr 不能直接进行指针运算。

IntPtr 是一个平台相关的类型,在 32 位平台上是 4 字节,在 64 位平台上是 8 字节。

在使用 IntPtr 时,不需要使用 unsafe 关键字,也不需要启用 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>(如果使用 P/Invoke 调用非托管函数时,仍然需要启用)。

指向非托管内存的 IntPtr

在使用 IntPtr 管理非托管内存时,不能直接读取和写入内存,需要使用 Marshal 提供的ReadXXX 和 WriteXXX 方法。

using System.Runtime.InteropServices;class Program
{static void Main(){// 在非托管内存中分配一块内存用于存储整数数组int size = 10;IntPtr ptr = Marshal.AllocHGlobal(size * sizeof(int));// 将数据写入非托管内存for (int i = 0; i < size; i++){Marshal.WriteInt32(ptr + i * sizeof(int), i);}// 读取非托管内存的数据for (int i = 0; i < size; i++){Console.WriteLine(Marshal.ReadInt32(ptr + i * sizeof(int)));}// 释放非托管内存Marshal.FreeHGlobal(ptr);}
}

保存句柄的 IntPtr

IntPtr 也可以用于存储句柄,例如文件句柄、窗口句柄等。

句柄可以理解为一个指向资源的引用,通常是一个整数值,用于唯一标识和访问由操作系统管理的资源。本质上它是一个资源标识符,而不是资源在内存中的实际地址。

下面是一个 windows 平台的例子

using System.Runtime.InteropServices;public static partial class Program
{// Define a delegate that corresponds to the unmanaged function.private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);// Import user32.dll (containing the function we need) and define// the method corresponding to the native function.[LibraryImport("user32.dll")]private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);// Define the implementation of the delegate; here, we simply output the window handle.private static bool OutputWindow(IntPtr hwnd, IntPtr lParam){Console.WriteLine(hwnd.ToInt64());return true;}public static void Main(string[] args){// Invoke the method; note the delegate as a first parameter.EnumWindows(OutputWindow, IntPtr.Zero);}
}

上面的代码使用了 LibraryImport 特性来导入 user32.dll 中的 EnumWindows 函数,并定义了一个委托 EnumWC 来对应这个函数的回调函数。EnumWindows 函数会枚举所有顶级窗口,并调用 OutputWindow 函数来输出每个窗口的句柄。

OutputWindow 函数的参数 hwnd 是一个 IntPtr 类型的句柄,表示窗口的句柄。可以使用 hwnd.ToInt64() 将其转换为长整型值进行输出。

函数指针(Function Pointer)

基本概念

函数指针是一个指向函数的指针,分为托管函数指针和非托管函数指针。

这是一个 C# 9 新增的特性,建议读者阅读官方文档地址加深理解:https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers

在 IL 层面,调用方法的指令分为三种:

call:直接调用静态方法或非虚方法。

  • 常用于静态方法、私有实例方法、构造函数、基类方法等。

    不会进行虚方法表查找,故不能用于虚方法调用。

callvirt:用于调用虚方法(virtual)、接口方法,或者有时也用来调用非虚实例方法。

  • 会进行虚方法表(vtable)查找,确保调用最终派生类的实现(多态)。

    调用前自动检测 this 是否为 null,如果是则抛出 NullReferenceException。所以 C# 编译器的常见做法是对非虚方法也使用 callvirt,以保证 null 检查。

calli:间接调用,通过函数指针进行调用。

  • 性能开销更低,但安全性、类型检查弱。

    通常只有在编写 IL 代码,或者使用 Emit 动态生成代码时才会使用。

    新增的函数指针语法允许在 C# 中使用 calli 指令,提供了更好的类型安全性。

早期 C# 为我们提供了委托(Delegate)来封装方法的引用,委托可以看作是一个类型安全的函数指针。所有的委托类型都继承自 System.Delegate 类。我们在调用委托时,实际上是调用了委托的 Invoke 这个虚方法,IL 指令是 callvirt

在后期新增的函数指针语法中,编译器使用 calli 指令来调用函数,而不是实例化委托对象并调用 Invoke 方法。

函数指针的声明和使用

和指针一样,函数指针也需要在 unsafe 代码块中使用,并且需要启用 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

声明函数指针的语法如下:

delegate*<[parameter type list], return type> variableName

delegate* 是一个关键字,表示函数指针类型。

<parameter type list> 是参数类型列表,可以是空的,也可以是一个或多个参数类型,用逗号分隔。return type 是返回值类型,可以是 void 或者其他类型。

下面是几个例子:

  • delegate*<void> ptr:表示一个不带参数和返回值的函数指针。

  • delegate*<int> ptr:表示一个不带参数,返回值为 int 的函数指针。

  • delegate*<int, int, int> ptr:表示一个带两个 int 参数,返回值为 int 的函数指针。

  • delegate*<int, int, void> ptr:表示一个带两个 int 参数,无返回值的函数指针。

函数指针的声明和使用示例:

unsafe class Program
{static void Main(){// 声明一个函数指针,指向一个返回 int 的函数,参数为两个 intdelegate*<int, int, int> addPtr = &Add;// 调用函数指针int result = addPtr(1, 2);Console.WriteLine(result); // 输出 3}static int Add(int a, int b){return a + b;}
}

使用 & 运算符获取函数的地址,并赋值给函数指针变量。

函数指针只能指向静态方法,不能指向实例方法或者委托。

可以指向静态的本地函数(local function),也就是说这个本地函数不是闭包。

下面对比函数指针和委托,用 BenchmarkDotNet 做个简单的性能测试

public class Program
{public static void Main(string[] args){BenchmarkRunner.Run<Benchmark>();}
}[MemoryDiagnoser]
public class Benchmark
{private delegate int AddDelegate(int a, int b);private static AddDelegate addDelegate = Add;private unsafe delegate*<int, int, int> addPtr = &Add;[Benchmark]public void Delegate(){for (int i = 0; i < 1000000; i++){var result = addDelegate(1, 2);}}[Benchmark]public unsafe void FunctionPointer(){for (int i = 0; i < 1000000; i++){var result = addPtr(1, 2);}}private static int Add(int a, int b){return a + b;}
}

运行结果如下:

| Method          | Mean     | Error     | StdDev    | Allocated |
|---------------- |---------:|----------:|----------:|----------:|
| Delegate        | 1.530 ms | 0.0054 ms | 0.0048 ms |       1 B |
| FunctionPointer | 1.409 ms | 0.0042 ms | 0.0039 ms |       1 B |

虽然此处例子差距不是很明显,但还是能看到函数指针的性能更好一些。

托管函数指针和非托管函数指针

在声明函数指针时,可以在 delegate* 后面加上 managed 或 unmanaged 关键字,表示托管函数指针或非托管函数指针。

不加关键字时,默认是托管函数指针。

下面是一个可以在 macOS 上运行的例子

unsafe class Program
{// 声明C函数指针类型(C的 getpid:int getpid(void);)private delegate* unmanaged[Cdecl]<int> GetPidDelegate;static void Main(){var prog = new Program();prog.Run();}public void Run(){// 加载libc(macOS下通常路径就是 /usr/lib/libc.dylib)IntPtr lib = NativeLibrary.Load("/usr/lib/libc.dylib");// 获取getpid符号IntPtr pidFuncPtr = NativeLibrary.GetExport(lib, "getpid");// 转为函数指针(需要unsafe上下文)GetPidDelegate = (delegate* unmanaged[Cdecl]<int>)pidFuncPtr;// 用C#的函数指针调用 (unsafe 上下文中)int pid = GetPidDelegate();Console.WriteLine($"Current PID from libc.getpid(): {pid}");// 释放库NativeLibrary.Free(lib);}
}

上面的代码中,delegate* unmanaged[Cdecl]<int> 声明了一个非托管函数指针类型,指向一个返回 int 的函数。

Cdecl 是调用约定,表示使用 C 语言的调用约定。

通过获取 getpid 函数的地址,并将其转换为函数指针类型,最后调用该函数获取当前进程的 PID。

NativeLibrary 是一个用于加载和调用非托管库的类,提供了 Load 和 GetExport 方法来加载库和获取函数地址。

使用完后,使用 NativeLibrary.Free 方法释放库。

托管指针(Managed Pointer)

托管指针的声明和使用

托管指针并非一个新的特性,在早期的 C# 版本中,我们在方法参数上使用的 ref 和 out 就是声明了托管指针。

在 IL 中,用 <type>* 来表示前面说的指针(pointer,有些资料中称为 非托管指针)。

而 ref 和 out 在 IL 中对应的是 <type>&,也就是托管指针(managed pointer)。

out 相当于 ref 的一种特殊情况,表示参数是一个输出参数,方法内部必须对其赋值。

另外还有一个 in 可以把方法参数声明为只读的托管指针,方法内部不能对其赋值。

使用托管指针时,我们不需要使用 unsafe 关键字,也不需要启用 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

注意:托管指针相关的语法会在几个位置用到 ref 关键字,但作用和意义是不同的。

  • 我们使用 ref <type> ptr 来声明一个托管指针。

  • 同时也用 ref 关键字来获取变量的地址,ref <type> ptr = ref a

  • 访问托管指针指向的数据时,语法上只需直接访问不带 ref 的指针变量名 ptr 即可。

  • 复制托管指针的值时,需要在指针变量前面加上 ref 关键字。ref <type> ptr2 = ref ptr

  • 修改托管指针指向的数据时,语法上只需直接访问不带 ref 的指针变量名 ptr 即可,ptr = ref b

class Program
{static void Main(){int a = 10;ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址Console.WriteLine(p1); // 输出 10,访问托管指针 p1 指向的值,即 a 的值p1 = 20; // 修改托管指针 p1 指向的值,即修改 a 的值Console.WriteLine(a); // 输出 20ref int p2 = ref p1; // 将托管指针 p1 的值复制给 p2,即 p2 也指向 a 的地址p2 = 30; // 修改托管指针 p2 指向的值,即修改 a 的值Console.WriteLine(a); // 输出 30int b = 40;p1 = ref b; // 将 p1 重新指向 bConsole.WriteLine(p1); // 输出 40,访问托管指针 p1 指向的值,即 b 的值p1 = 50; // 修改托管指针 p1 指向的值,即修改 b 的值Console.WriteLine(b); // 输出 50Console.WriteLine(p2); // 输出 30,p2 仍然指向 a 的地址}
}

托管指针可以指向的位置

  • 值类型变量:也就是指向值类型的数据本体。

  • 引用类型变量:和上文指向对象引用的指针(Pointer)一样,相当于一个二级指针,但不支持指向另一个托管指针。

  • 值类型或者引用类型的实例字段。

  • 值类型或者引用类型的静态字段

  • 数组元素:但不支持指针算法。

  • null:表示没有指向任何有效的内存地址,尝试访问 null 指针会导致 NullReferenceException。目前只有作为 ref struct 的 ref 字段时,可能出现这个情况,需使用 Unsafe.IsNullRef<T>(T) 方法确定 ref 字段是否为 null。

可以声明托管指针的位置

  • 局部变量:可以在方法中声明托管指针变量。

  • 方法参数:可以将托管指针作为方法参数传递。

  • 方法返回值:可以将托管指针作为方法的返回值。

  • ref struct 的实例字段:ref struct 的 ref 不代表这种 struct 是按引用传递的,是指其具有类似托管指针的限制。

  • 只读属性:包含只读索引(indexer),但不支持自动属性(Automatically implemented properties)。

托管指针的限制

出于安全的设计目的,相较于指针(Pointer),托管指针只允许存在于栈上,不允许在存在于堆上。主要的限制如下:

  • 不能作为类或者非 ref struct 的结构体的字段。

  • 不能作为静态字段,因为静态字段在保存在托管堆上(非 GC Heap)。

  • 不能作为 async方法 或 迭代器方法 的参数,因为参数会被状态机捕获,并保存在堆上。

  • 不能在 await 和 yield 语句中使用,因为相关的变量会被状态机捕获,并保存在堆上。

  • 不能被闭包捕获,因为编译器会将闭包转换为一个类,并将捕获的变量作为类的字段。

作为能保存托管指针的的 ref struct,也只允许在栈上分配内存。C# 对 ref struct 的限制主要如下:

  • 不能作为类或者非 ref struct 的结构体的字段。

  • 不能作为静态字段。

  • 不能装箱。无法将 ref struct 装箱为 object 或者接口类型。也无法将 ref struct 作为数组元素。

  • 不能作为 async方法 的参数,因为参数会被状态机捕获,并保存在堆上。但可以作为迭代器方法的参数。

  • 不能在 await 和 yield 语句中使用,因为相关的变量会被状态机捕获,并保存在堆上。

  • 不能被闭包捕获,因为编译器会将闭包转换为一个类,并将捕获的变量作为类的字段。

指向对象引用的托管指针

托管指针指向对象引用时,和指针(Pointer)一样,都类似于一个二级指针。

下面是一个简单的例子,演示了如何使用托管指针指向对象引用:

class Program
{static void Main(){Foo foo = new Foo{Bar = 1};// 声明一个托管指针,指向 foo 的地址// ldloca.s     foo   // 加载 foo 的地址// stloc.1            // 将转换后的 int 存储到 fooPtrref Foo fooPtr = ref foo;// 访问托管指针指向的对象引用// ldloc.1            // 加载 fooPtr// ldind.ref          // 将 fooPtr 指向的对象引用加载到栈上// callvirt     instance int32 Foo::get_Bar()// call         void [System.Console]System.Console::WriteLine(int32)Console.WriteLine(fooPtr.Bar); // 输出 1// 修改托管指针指向的对象引用// ldloc.1            // 加载 fooPtr// newobj       instance void Foo::.ctor()// dup// ldc.i4.2// callvirt     instance void Foo::set_Bar(int32)// nop// stind.ref          // 新的 Foo 对象的地址保存通过 fooPtr 保存到 foofooPtr = new Foo{Bar = 2};// 访问托管指针指向的对象引用Console.WriteLine(foo.Bar); // 输出 2// 通过托管指针修改原对象的属性// ldloc.1      // 加载 fooPtr// ldind.ref    // 将 fooPtr 指向的对象引用加载到栈上// ldc.i4.3     // 将 3 压入栈上// callvirt     instance void Foo::set_Bar(int32)// nopfooPtr.Bar = 3;Console.WriteLine(foo.Bar); // 输出 3}
}public struct Foo
{public int Bar { get; set; }
}

上面的代码中,ref Foo fooPtr = ref foo; 声明了一个托管指针 fooPtr,指向 foo 的地址。

fooPtr 是一个托管指针,指向 foo 的地址,虽然语法可以直接访问 fooPtr.Bar 的属性,但其过程是先将 fooPtr 指向的对象引用加载到栈上,然后调用 get_Bar() 方法获取属性值。

fooPtr = new Foo { Bar = 2 }; 修改了 fooPtr 指向的对象引用,也就是修改了 foo 的值。

和指针(Pointer)那一章节生成的 IL 代码进行对比,你会发现,唯一的区别是将变量地址保存到指针时,指针比托管指针多了一个 conv.u 指令。

class Program
{static unsafe void Main(){Foo foo = new Foo{Bar = 1};// ldloca.s     foo// conv.u       // 将 foo 的地址转换为unsigned native int// stloc.1      // fooPtr1Foo* fooPtr1 = &foo;// ldloca.s     foo// stloc.2      // fooPtr2ref Foo fooPtr2 = ref foo;}
}public struct Foo
{public int Bar { get; set; }
}

可以看出唯一的区别就是 指针(Pointer)和托管指针(Managed Pointer)在保存变量地址时,指针(Pointer)需要转换为 unsigned native int,而托管指针(Managed Pointer)不需要转换。

在获取对象引用时 ldind.ref 同时支持两种指针格式。

指向 GC Heap 的托管指针

托管指针受 GC 管理,不用关注指向的数据是否在 GC 过程中被移动。在 GC 过程中,托管指针会被自动更新为新的地址。

下面是一个简单的例子,演示了如何使用托管指针指向引用类型的实例字段:

class Program
{static void Main(){Foo foo = new Foo{Bar = 1};ref int p = ref foo.Bar; // 声明一个托管指针,指向 foo 的 Bar 字段Console.WriteLine(p); // 输出 1p = 2; // 修改托管指针 p 指向的值,即修改 foo 的 Bar 字段Console.WriteLine(foo.Bar); // 输出 2}
}public class Foo
{public int Bar;
}

指向数组元素的托管指针

托管指针可以指向数组元素,但不支持指针算法。

class Program
{static void Main(){int[] arr = new int[5] { 0, 1, 2, 3, 4 };// 声明一个托管指针,指向数组的第一个元素ref int p = ref arr[0];Console.WriteLine(p); // 输出 0p = 10; // 修改托管指针 p 指向的值,即修改数组的第一个元素Console.WriteLine(arr[0]); // 输出 10}
}

指向静态字段的托管指针

class Program
{static void Main(){// 声明一个托管指针,指向静态字段 Foo.StaticField 的地址ref int p = ref Foo.StaticField;Console.WriteLine(p); // 输出 0p = 20; // 修改托管指针 p 指向的值,即修改 Foo.StaticField 的值Console.WriteLine(Foo.StaticField); // 输出 20}
}public class Foo
{public static int StaticField;
}

作为方法参数的托管指针

目前,我们有下面几种方法可以声明托管指针作为方法参数:

注意:托管指针本身是值传递,无法在方法内修改外部的托管指针的指向

1、ref 关键字:表示参数是一个引用类型的托管指针,方法内部可以修改托管指针指向的外部变量。

class Program
{static void Main(){int a = 10;int b = 20;ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址ref int p2 = ref b; // 声明一个托管指针,指向变量 b 的地址Modify(ref p1, ref p2); // 传递托管指针作为参数Console.WriteLine(a); // 输出 11Console.WriteLine(b); // 输出 22}static void Modify(ref int p1, ref int p2){p1 = 11; // 修改托管指针 p1 指向的变量 a 的值p1 = ref p2; // 将托管指针 p1 指向变量 b 的地址,但托管指针本身是值传递的,不会影响原变量 a 的值,这边修改的只是作为参数的 p1 的值p1 = 22; // 修改托管指针 p1 指向的变量 b 的值}}

2、in 关键字:表示参数是一个只读的托管指针,方法内部不能修改托管指针指向的外部变量。

class Program
{static void Main(){int a = 10;int b = 20;ref int p1 = ref a; // 声明一个托管指针,指向变量 a 的地址ref int p2 = ref b; // 声明一个托管指针,指向变量 b 的地址Modify(ref p1, ref p2); // 传递托管指针作为参数Console.WriteLine(a); // 输出 10Console.WriteLine(b); // 输出 20}static void Modify(in int p1, in int p2){// p1 = 11; // 错误:不能修改 in 托管指针指向的变量 a 的值p1 = ref p2; // 无效:不能修改 in 托管指针 ref int p1 的指向}
}

3、out 关键字:表示参数是一个输出参数,方法内部必须通过托管指针对其指向的外部变量赋值。

class Program
{static void Main(){int a = 10;int b = 20;Modify(out a, out b); // 传递托管指针作为参数Console.WriteLine(a); Console.WriteLine(b);}static void Modify(out int p1, out int p2){p1 = 11; // 修改 p1 指向的变量 a 的值,不赋值会报错p2 = 22; // 修改 p2 指向的变量 b 的值,不赋值会报错p1 = ref p2; // 无效:不能修改 out 托管指针 ref int p1 的指向}
}

4、readonly ref 关键字:按目前的标准,作为参数时和 in 关键字的效果是一样的。

class Program
{static void Main(){int a = 10;ref int p = ref a; // 声明一个托管指针,指向变量 a 的地址ModifyRef(ref p);ModifyRefReadonly(ref p);ModifyInt(in p);}static void ModifyRef(ref int p){Console.WriteLine(p); // 可以读取托管指针指向的变量的值p = 11; // 修改托管指针指向的变量的值}static void ModifyInt(in int p){Console.WriteLine(p); // 可以读取 in 托管指针指向的变量的值p = 11; // 错误:不能修改 in 托管指针指向的变量的值}static void ModifyRefReadonly(ref readonly int p){Console.WriteLine(p); // 可以读取 ref readonly 托管指针指向的变量的值p = 11; // 错误:不能修改 in 托管指针指向的变量的值}
}

ref readonly 托管指针

在声明作为局部变量的托管指针时,可以使用 ref readonly 关键字,表示无法通过这个托管指针修改其指向的数据,但是可以修改托管指针的指向。

class Program
{static void Main(){int a = 10;// 声明一个 ref readonly 托管指针,指向变量 a 的地址ref readonly int p1 = ref a;// p1 = 20; // 错误:无法修改指向的变量的值int b = 20;p1 = ref b; // 可以指向其他变量Console.WriteLine(p1); // 输出 20Console.WriteLine(a); // 输出 10,a 的值没有改变}
}

作为 ref struct 的字段的托管指针

ref struct 表示一个引用类型的结构体,具有类似于托管指针的限制。

在 ref struct 可以声明托管指针作为字段。

注意:只能在 ref struct 的构造函数中对 ref 字段 进行初始化,不支持初始化器初始化或者实例化完成之后的初始化,否则将触发 NullReferenceException

using System.Runtime.CompilerServices;var foo = new Foo();// 不能用 == null 来判断,会触发 NullReferenceException
// Console.WriteLine(foo.Value == null);// 只能用 Unsafe.IsNullRef 来判断
Console.WriteLine(Unsafe.IsNullRef(foo.Value));// 不能在 ref struct 实例化完成之后对 ref 字段进行初始化,会触发 NullReferenceException
// foo.Value = 1;// 只能在 ref struct 的构造函数中对 ref 字段进行初始化
int value = 1;
var bar = new Bar(ref value);Console.WriteLine(bar.Value);ref struct Foo
{public ref int Value;
}ref struct Bar
{public Bar(ref int value){Value = ref value;}public ref int Value;
}

有几种方式可以声明 ref struct 的字段:

1、ref 关键字:表示字段是一个引用类型的托管指针,可以修改指针指向的数据以及修改指针的指向。

var a = 1;
var foo = new Foo(ref a);Console.WriteLine(foo.Value); // 输出 1// 修改指针指向的数据
foo.Value = 11;Console.WriteLine(a); // 输出 11// 修改指针的指向
var b = 2;// 将指针重新指向 b
foo.Value = ref b;Console.WriteLine(foo.Value); // 输出 2ref struct Foo
{// 声明一个托管指针,指向 int 类型的值public ref int Value;public Foo(ref int value){// 在构造函数中初始化托管指针Value = ref value;}
}

2、ref readonly 关键字:表示字段是一个指向只读数据的托管指针,不能修改指针指向的数据,但可以修改指针的指向。

var a = 1;
var foo = new Foo(ref a);Console.WriteLine(foo.Value); // 输出 1// foo.Value = 11; // 编译错误:不能修改只读数据// 修改指针的指向
var b = 2;
// 将指针重新指向 b
foo.Value = ref b;
Console.WriteLine(foo.Value); // 输出 2ref struct Foo
{// 声明一个指向只读数据的托管指针,指向 int 类型的值public ref readonly int Value;public Foo(ref int value){// 在构造函数中初始化托管指针Value = ref value;}
}

3、readonly ref 关键字:表示字段是一个只读的托管指针,不能修改指针的指向,但可以修改指针指向的数据。

var a = 1;
var foo = new Foo(ref a);Console.WriteLine(foo.Value); // 输出 1// 修改指针指向的数据
foo.Value = 11;
Console.WriteLine(a); // 输出 11// 修改指针的指向
var b = 2;
// 将指针重新指向 b
// foo.Value = ref b; // 编译错误:不能修改只读指针的指向ref struct Foo
{// 声明一个只读的托管指针,指向 int 类型的值public readonly ref int Value;public Foo(ref int value){// 在构造函数中初始化托管指针Value = ref value;}
}

4、readonly ref readonly 关键字:表示字段是一个指向只读数据的只读托管指针,不能修改指针的指向,也不能修改指针指向的数据。

var a = 1;
var foo = new Foo(ref a);Console.WriteLine(foo.Value); // 输出 1// foo.Value = 11; // 编译错误:不能修改只读数据int b = 2;
// 将指针重新指向 b
// foo.Value = ref b; // 编译错误:不能修改只读指针的指向ref struct Foo
{// 声明一个指向只读数据的只读托管指针,指向 int 类型的值public readonly ref readonly int Value;public Foo(ref int value){// 在构造函数中初始化托管指针Value = ref value;}
}

托管指针受 GC 管理

托管指针受 GC 管理,不用关注指向的数据是否在 GC 过程中被移动。在 GC 过程中,托管指针会被自动更新为新的地址。

下面的例子中演示了用 指针(Pointer)和 托管指针(Managed Pointer)分别指向数组元素的情况。

GetArrayElementPointer 方法中的数组对象在方法结束后失去了根引用,GC 会在下一次回收时将其回收。

GetArrayElementManagedPointer 方法中的数组对象在方法结束后仍然有托管指针作为根引用,GC 不会回收它。

unsafe class Program
{static void Main(){Console.WriteLine("before GC");// 获取指针int* p1 = GetArrayElementPointer(out var wr1);// 输出 true,表示数组对象仍然存在Console.WriteLine($"wr1.IsAlive: {wr1.IsAlive}");// 输出 1Console.WriteLine($"*p1: {*p1}");// 获取托管指针ref int p2 = ref GetArrayElementManagedPointer(out var wr2);// 输出 true,表示数组对象仍然存在Console.WriteLine($"wr2.IsAlive: {wr2.IsAlive}");// 输出 2Console.WriteLine($"p2: {p2}");GC.Collect();Console.WriteLine();Console.WriteLine("after GC");// 输出 false,表示数组对象已被回收Console.WriteLine($"wr1.IsAlive: {wr1.IsAlive}");// 输出 随机值,有可能是 0,也有可能是其他值Console.WriteLine($"*p1: {*p1}");// 输出 true,表示数组对象仍然存在Console.WriteLine($"wr2.IsAlive: {wr2.IsAlive}");// 输出 2Console.WriteLine($"p2: {p2}");}static int* GetArrayElementPointer(out WeakReference wr){int[] arr = [1];wr = new WeakReference(arr);fixed (int* p = &arr[0]){return p;}}static ref int GetArrayElementManagedPointer(out WeakReference wr){int[] arr = [2];wr = new WeakReference(arr);return ref arr[0];}
}

Unsafe.AsRef 方法

Unsafe.AsRef<T> 有两个重载:

1、AsRef<T>(Void*): 将非托管指针转换为指向 类型的 T值的托管指针。

using System.Runtime.CompilerServices;unsafe class Program
{static void Main(){int a = 10;int* p = &a;// 将非托管指针转换为指向 int 的托管指针ref int p1 = ref Unsafe.AsRef<int>(p);Console.WriteLine(p1); // 输出 10p1 = 20; // 修改托管指针 p1 指向的值,即修改 a 的值Console.WriteLine(a); // 输出 20}
}

2、AsRef<T>(T): 将给定的 ref readonly 托管指针重新解释为可以修改指向的值的托管指针。

可以修改 ref readonly 托管指针指向的值。

using System.Runtime.CompilerServices;class Program
{static void Main(){int a = 10;// 声明一个 ref readonly 托管指针,指向变量 a 的地址ref readonly int p1 = ref a;// 将 ref readonly 托管指针转换为普通的托管指针ref int p2 = ref Unsafe.AsRef<int>(p1);Console.WriteLine(p2); // 输出 10p2 = 20; // 修改托管指针 p2 指向的值,即修改 a 的值Console.WriteLine(a); // 输出 20Console.WriteLine(p1); // 输出 20,p1 仍然指向 a 的地址}
}

也可以修改 ref struct 的 ref readonly 或 readonly ref readonly 字段的值。

using System.Runtime.CompilerServices;var a = 1;
var foo = new Foo(ref a);Console.WriteLine(foo.Value); // 输出 1ref int p = ref  Unsafe.AsRef(foo.Value); // 获取指向 foo.Value 的指针p = 11; // 修改指针指向的值Console.WriteLine(a); // 输出 11ref struct Foo
{// 声明一个指向只读数据的只读托管指针,指向 int 类型的值public readonly ref readonly int Value;public Foo(ref int value){// 在构造函数中初始化托管指针Value = ref value;}
}

文章转载自:黑洞视界 

原文链接:理解 C# 中的各类指针 - 黑洞视界 - 博客园

体验地址:JNPF快速开发平台

相关文章:

理解 C# 中的各类指针

前言 变量可以理解成是一块内存位置的别名&#xff0c;访问变量也就是访问对应内存中的数据。 指针是一种特殊的变量&#xff0c;它存储了一个内存地址&#xff0c;这个内存地址代表了另一块内存的位置。 指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存…...

MySQL 事务(二)

文章目录 事务隔离性理论理解隔离性隔离级别 事务隔离级别的设置和查看事务隔离级别读未提交读提交&#xff08;不可重复读&#xff09; 事务隔离性理论 理解隔离性 MySQL服务可能会同时被多个客户端进程(线程)访问&#xff0c;访问的方式以事务方式进行一个事务可能由多条SQL…...

【HarmonyOS】ArkTS开发应用的横竖屏切换

文章目录 1、简介2、静态 — 横竖屏切换2.1、效果2.2、实现原理2.3、module.json5 源码 3、动态 — 横竖屏切换3.1、应用随系统旋转切换横竖屏3.2、setPreferredOrientation 原理配置3.3、锁定旋转的情况下&#xff0c;手动设置横屏状态 1、简介 在完成全屏网页嵌套应用开发后…...

Linux中find命令用法核心要点提炼

大家好&#xff0c;欢迎来到程序视点&#xff01;我是你们的老朋友.小二&#xff01; 以下是针对Linux中find命令用法的核心要点提炼&#xff1a; 基础语法结构 find [路径] [选项] [操作]路径&#xff1a;查找目录&#xff08;.表当前目录&#xff0c;/表根目录&#xff09;…...

专栏项目框架介绍

项目整体实现框图 如下图所示&#xff0c;是该项目的整体框图&#xff0c;项目的功能概括为&#xff1a;PC端下发数据文件&#xff0c;FPGA板卡接收数据文件&#xff0c;缓存至DDR中&#xff0c;待数据文件发送完毕&#xff0c;循环读取DDR有效写区域数据&#xff0c;将DDR数据…...

WSL 安装 Debian 12 后,Linux 如何安装 vim ?

在 WSL 的 Debian 12 中安装 Vim 非常简单&#xff0c;只需使用 apt 包管理器即可。以下是详细步骤&#xff1a; 1. 更新软件包列表 首先打开终端&#xff0c;确保系统包列表是最新的&#xff1a; sudo apt update2. 安装 Vim 直接通过 apt 安装 Vim&#xff1a; sudo apt …...

【SpringBoot】从零开始全面解析Spring MVC (一)

本篇博客给大家带来的是SpringBoot的知识点, 本篇是SpringBoot入门, 介绍Spring MVC相关知识. &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子…...

C++—特殊类设计设计模式

目录 C—特殊类设计&设计模式1.设计模式2.特殊类设计2.1设计一个无法被拷贝的类2.2设计一个只能在堆上创建对象的类2.3设计一个只能在栈上创建对象的类2.4设计一个类&#xff0c;无法被继承2.5设计一个类。这个类只能创建一个对象【单例模式】2.5.1懒汉模式实现2.5.2饿汉模…...

初入OpenCV

OpenCV简介 OpenCV是一个开源的跨平台计算机视觉库&#xff0c;它实现了图像处理和计算机视觉方面的很多通用算法。 应用场景&#xff1a; 目标识别&#xff1a;人脸、车辆、车牌、动物&#xff1b; 自动驾驶&#xff1b;医学影像分析&#xff1b; 视频内容理解分析&#xff…...

霍夫圆变换全面解析(OpenCV)

文章目录 一、霍夫圆变换基础1.1 霍夫圆变换概述1.2 圆的数学表达与参数化 二、霍夫圆变换算法实现2.1 标准霍夫圆变换算法流程2.2 参数空间的表示与优化 三、关键参数解析3.1 OpenCV中的HoughCircles参数3.2 参数调优策略 四、Python与OpenCV实现参考4.1 基本实现代码4.2 改进…...

互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-4

互联网大厂Java求职面试&#xff1a;优惠券服务架构设计与AI增强实践-4 场景设定 面试官&#xff1a;某互联网大厂技术总监&#xff0c;拥有超过10年大型互联网企业一线技术管理经验&#xff0c;擅长分布式架构、微服务治理、云原生等领域。 候选人&#xff1a;郑薪苦&#…...

项目中会出现的css样式

1.重复渐变边框 思路&#xff1a; 主要是用重复的背景渐变实现的 如图&#xff1a; <div class"card"><div class"container">全面收集中医癌毒临床医案&#xff0c;建立医案共享机制&#xff0c;构建癌毒病机知识图谱&#xff0c;便于医疗人…...

LeetCode[101]对称二叉树

思路&#xff1a; 对称二叉树是左右子树对称&#xff0c;而不是左右子树相等&#xff0c;所以假设一个树只有3个节点&#xff0c;那么判断这个数是否是对称二叉树&#xff0c;肯定是先判断左右两个树&#xff0c;然后再看根节点&#xff0c;这样递归顺序我们就确认了&#xff0…...

黑马k8s(四)

1.资源管理介绍 本章节主要介绍yaml语法和kubernetes的资源管理方式 2.YAML语言介绍 3.资源管理方式 命令式对象管理 dev下删除了pod&#xff0c;之后发现还有pod&#xff0c;把原来的pod删除了&#xff0c;重新启动了一个 命令式对象配置 声明式对象配置 命令式对象配置&…...

华为ensp实现跨vlan通信

要在网络拓扑中实现主机192.168.1.1、192.168.1.2和192.168.2.1之间的互相通信&#xff0c;需要正确配置交换机&#xff08;S5700&#xff09;和路由器&#xff08;AR3260&#xff09;&#xff0c;以确保不同网段之间的通信&#xff08;即VLAN间路由&#xff09;。 网络拓扑分析…...

TCPIP详解 卷1协议 十 用户数据报协议和IP分片

10.1——用户数据报协议和 IP 分片 UDP是一种保留消息边界的简单的面向数据报的传输层协议。它不提供差错纠正、队列管理、重复消除、流量控制和拥塞控制。它提供差错检测&#xff0c;包含我们在传输层中碰到的第一个真实的端到端&#xff08;end-to-end&#xff09;校验和。这…...

Java笔记4

第一章 static关键字 2.1 概述 以前我们定义过如下类&#xff1a; public class Student {// 成员变量public String name;public char sex; // 男 女public int age;// 无参数构造方法public Student() {}// 有参数构造方法public Student(String a) {} }我们已经知道面向…...

Matlab 垂向七自由度轨道车辆开关型半主动控制

1、内容简介 Matlab 229-垂向七自由度轨道车辆开关型半主动控制 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...

Matlab 短时交通流预测AR模型

1、内容简介 Matlab 230-短时交通流预测AR模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略城市道路短时交通流预测.pdf...

MYSQL之表的约束

表中真正约束字段的是数据类型, 但是只有数据类型约束就很单一, 也需要有一些额外的约束, 从而更好的保证数据的合法性, 从业务逻辑角度保证数据的正确性. 比如有一个字段是email, 要求是唯一的. 为什么要有表的约束? 表的约束: 表中一定要有各种约束, 通过约束, 让我们未来…...

使用ACE-Step在本地生成AI音乐

使用ACE-Step v1-3.5B开源模型从文本提示、标签和歌词创建完整的AI生成歌曲 — 无需云服务,无需API,仅需您的GPU。 这是由ACE Studio和StepFun开发的开源音乐生成模型。 在对数据隐私和云服务依赖性日益增长的担忧时代,ACE-Step将强大的文本转音乐生成完全离线,使其成为A…...

web 自动化之 Unittest 四大组件

文章目录 一、如何开展自动化测试1、项目需求分析&#xff0c;了解业务需求 web 功能纳入自动化测试2、选择何种方式实现自动化测试 二、Unittest 框架三、TestCase 测试用例四、TestFixture 测试夹具 执行测试用例前的前置操作及后置操作五、TestSuite 测试套件 & TestLoa…...

2025年渗透测试面试题总结-渗透测试红队面试七(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 渗透测试红队面试七 一百八十一、Shiro漏洞类型&#xff0c;721原理&#xff0c;721利用要注意什么&am…...

Mysql的索引,慢查询和数据库表的设计以及乐观锁和悲观锁

设计高性能数据表的原则 数据库设计经验和技巧 单张数据表的字段不宜过多&#xff08;20个&#xff09;&#xff0c;如果确实存在大量field,考虑拆成多张表或json text存储 数据表字段都是not null的&#xff0c;即使没有数据&#xff0c;最好也使用无意义的值填充&#xff0c…...

day012-软件包管理专题

文章目录 1. 生成随机密码2. 软件包管理2.1 类红帽系统2.1.1 安装软件包2.1.2 查找软件包2.1.3 查看软件包内容2.1.4 查看命令或文件属于哪个软件包2.1.5 重新安装软件包2.1.6 删除软件包2.1.7 升级2.1.8 rpm安装软件包2.1.9 rpm升级软件包2.1.10 rpm检查软件包文件是否改变 3.…...

学习黑客5 分钟深入浅出理解Windows Firewall

5 分钟深入浅出理解Windows Firewall &#x1f525; 大家好&#xff01;今天我们将探索Windows防火墙——这是Windows操作系统中的核心安全组件&#xff0c;负责控制进出计算机的网络流量。无论你是计算机初学者&#xff0c;还是在TryHackMe等平台上学习网络安全的爱好者&…...

node .js 启动基于express框架的后端服务报错解决

问题&#xff1a; node .js 用npm start 启动基于express框架的后端服务报错如下&#xff1a; /c/Program Files/nodejs/npm: line 65: 26880 Segmentation fault "$NODE_EXE" "$NPM_CLI_JS" "$" 原因分析&#xff1a; 遇到 /c/Program F…...

feign.RequestInterceptor 简介-笔记

1. feign.RequestInterceptor 简介 Feign 是一个声明式 Web 服务客户端&#xff0c;用于简化 HTTP 请求的编写与管理。feign.RequestInterceptor 是 Feign 提供的一个接口&#xff0c;用于在请求发出之前对其进行拦截和修改。这在微服务架构中非常有用&#xff0c;比如在请求中…...

软考错题(四)

在程序执行过程中&#xff0c;高速缓存cache与主存间的地址映射由硬件自动完成 以下关于两个浮点数相加运算的叙述中&#xff0c;正确的是首先进行对阶&#xff0c;阶码小的向阶码大的对齐 认证只能阻止主动攻击不能阻止被动攻击 BGP是外部网关协议 查看端口信息&#xff1…...

SSRF相关

SSRF(Server Side Request Forgery,服务器端请求伪造)&#xff0c;攻击者以服务器的身份发送一条构造好的请求给服务器所在地内网进行探测或攻击。 产生原理&#xff1a; 服务器端提供了能从其他服务器应用获取数据的功能&#xff0c;如从指定url获取网页内容、加载指定地址的图…...

供应链学习

供应链安全 供应链&#xff1a;整个业务系统中的节点&#xff08;一般是上游节点&#xff09; 乙方一般提供资源&#xff1a;人 软件 硬件 服务 如何寻找供应链 1.招投标信息&#xff1a;寻标包 例如&#xff1a;烟草 智能办公 2.网站本身指纹 例如&#xff1a; powered by xxx…...

力扣HOT100之二叉树:226. 翻转二叉树

这道题很简单&#xff0c;用递归来做&#xff0c;对于一个根节点来说&#xff0c;有两种情况我们不需要翻转&#xff1a;一是根节点为空&#xff0c;二是根节点为叶子节点。这很容易理解&#xff0c;当传入的节点不满足上面的两种情况时&#xff0c;我们就需要做一个翻转&#…...

如何让rabbitmq保存服务断开重连?保证高可用?

在 Spring Boot 集成 RabbitMQ 时&#xff0c;可以通过以下几种方式让 RabbitMQ 保存服务断开重连&#xff0c;以保证高可用&#xff1a; 配置自动重连 application.properties 配置 &#xff1a;在 Spring Boot 的配置文件 application.properties 中&#xff0c;可以设置 Ra…...

TCPIP详解 卷1协议 九 广播和本地组播(IGMP 和 MLD)

9.1——广播和本地组播&#xff08;IGMP 和 MLD&#xff09; IPv4可以使用4种IP地址&#xff1a;单播&#xff08;unicast&#xff09;、任播&#xff08;anycast&#xff09;、组播&#xff08;multicast&#xff09;和广播&#xff08;broadcast&#xff09;。 IPv6可以使用…...

全球变暖-bfs

1.不沉的就是4个方向没有海&#xff0c;一个大岛屿有一个不沉就行了&#xff0c;其余染色就好了 2.第一个bfs来统计总岛屿个数 3.第二个来统计不沉岛屿个数 4.一减就ac啦 #include<bits/stdc.h> using namespace std; #define N 100011 typedef long long ll; typede…...

DDD领域驱动开发

1. 现象: 软件设计质量最高的时候是第一次设计的那个版本&#xff08;通常是因为第一次设计时&#xff0c;业务技术沟通最充分&#xff0c;从业务技术整体视角出发设计系统&#xff09;。当第一个版本设计上线以后就开始各种需求变更&#xff0c;这常常又会打乱原有的设计。 2…...

【HarmonyOS 5】鸿蒙App Linking详解

【HarmonyOS 5】鸿蒙App Linking详解 一、前言 HarmonyOS 的 App Linking 功能为开发者提供了一个强大的工具&#xff0c;通过创建跨平台的深度聚合链接&#xff0c;实现用户在不同场景下的无缝跳转&#xff0c;极大地提升了用户转化率和应用的可用性。 其安全性、智能路由和…...

Android Studio 中 build、assemble、assembleDebug 和 assembleRelease 构建 aar 的区别

上一篇&#xff1a;Tasks中没有build选项的解决办法 概述&#xff1a; 在构建 aar 包时通常会在下面的选项中进行构建&#xff0c;但是对于如何构建&#xff0c;选择哪种方式构建我还是处于懵逼状态&#xff0c;所以我整理了一下几种构建方式的区别以及如何选择。 1. build…...

【爬虫】12306查票

城市代码&#xff1a; 没有加密&#xff0c;关键部分&#xff1a; 完整代码&#xff1a; import json import requests with open(rE:\学习文件夹&#xff08;关于爬虫&#xff09;\项目实战\12306\城市代码.json,r,encodingutf-8) as f:city_codef.read() city json.loads(c…...

火山RTC 7 获得远端裸数据

一、获得远端裸数据 1、获得h264数据 1&#xff09;、远端编码后视频数据监测器 /*** locale zh* type callback* region 视频管理* brief 远端编码后视频数据监测器<br>* 注意&#xff1a;回调函数是在 SDK 内部线程&#xff08;非 UI 线程&#xff09;同步抛出来的&a…...

请求参数:Header 参数,Body 参数,Path 参数,Query 参数分别是什么意思,什么样的,分别通过哪个注解获取其中的信息

在API开发中&#xff08;如Spring Boot&#xff09;&#xff0c;请求参数可以通过不同方式传递&#xff0c;对应不同的注解获取。以下是 Header参数、Body参数、Path参数、Query参数 的区别及对应的注解&#xff1a; Header 参数 ​ • 含义&#xff1a;通过HTTP请求头&#x…...

【Web/HarmonyOS】采用ArkTS+Web组件开发网页嵌套的全屏应用

文章目录 1、简介2、效果3、在ArkTs上全屏Web3.1、创建ArkTS应用3.2、修改模块化配置&#xff08;module.json5&#xff09;3.3、修改系统栏控制&#xff08;ArkTS代码&#xff09; 4、双网页嵌套Web实现5、ArkTSWeb技术架构的演进 1、简介 在鸿蒙应用开发领域&#xff0c;技术…...

Leetcode (力扣)做题记录 hot100(34,215,912,121)

力扣第34题&#xff1a;在排序数组中查找第一个数和最后一个数 34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣&#xff08;LeetCode&#xff09; class Solution {public int[] searchRange(int[] nums, int target) {int left 0;int right nums.length - 1;int[…...

Babylon.js学习之路《三、创建你的第一个 3D 场景:立方体、球体与平面》

文章目录 1. 引言&#xff1a;从零构建一个 3D 场景1.1 目标与成果预览1.2 前置条件 2. 初始化 Babylon.js 场景2.1 创建 HTML 骨架2.2 初始化引擎与场景 3. 创建基础几何体3.1 立方体&#xff08;Box&#xff09;3.2 球体&#xff08;Sphere&#xff09;3.3 平面&#xff08;P…...

Go 语言即时通讯系统开发日志-day1:从简单消息收发 Demo 起步

Go语言即时通讯系统开发日志day1&#xff0c;主要模拟实现的一个简单的发送消息和接受消息的小demo&#xff0c;因为也才刚学习go语言的语法&#xff0c;对go的json、net/http库了解不多&#xff0c;所以了解了一下go语言的encoding/json库和net/http库&#xff0c;以及websock…...

AAAI-2025 | 中科院无人机导航新突破!FELA:基于细粒度对齐的无人机视觉对话导航

作者&#xff1a;Yifei Su, Dong An, Kehan Chen, Weichen Yu, Baiyang Ning, Yonggen Ling, Yan Huang, Liang Wang 单位&#xff1a;中国科学院大学人工智能学院&#xff0c;中科院自动化研究所模式识别与智能系统实验室&#xff0c;穆罕默德本扎耶德人工智能大学&#xff0…...

中科院无人机导航物流配送的智能变革!LogisticsVLN:基于无人机视觉语言导航的低空终端配送系统

作者&#xff1a;Xinyuan Zhang, Yonglin Tian, Fei Lin, Yue Liu, Jing Ma, Kornlia Sra Szatmry, Fei-Yue Wang 单位&#xff1a;中国科学院大学人工智能学院&#xff0c;中科院自动化研究所多模态人工智能系统国家重点实验室&#xff0c;澳门科技大学创新工程学院工程科学系…...

IP协议、以太网包头及UNIX域套接字

IP协议、以太网包头及UNIX域套接字 IP包头结构 IP协议是互联网的核心协议之一&#xff0c;其包头包含了丰富的信息来控制数据包的传输。让我们详细解析IPv4包头结构&#xff1a; 4位版本号(version)&#xff1a;标识IP协议版本&#xff0c;IPv4值为4 4位首部长度(header len…...

普林斯顿数学三剑客读本分析。

这几天看了普斯林顿数学三剑客&#xff0c;主要看了微积分、概率论前半部分&#xff0c;数学分析看了目录&#xff0c;大体略读了一下。怎么说呢&#xff0c;整体上来看&#xff0c;是很不错的&#xff0c;适合平常性阅读&#xff0c;配套结合国内教材习题来深入还是很不错的。…...

Matlab 模糊pid的液压舵机伺服系统

1、内容简介 Matlab 235-模糊pid的液压舵机伺服系统 可以交流、咨询、答疑 2、内容说明 略 舵机是轮船&#xff0c;客机等机器控制系统的重要组成部分&#xff0c;是客机&#xff0c;战斗机等飞行器操作系统的关键部件&#xff0c;也是一种超高的精度的位置伺服系统&#xff…...