当前位置: 代码迷 >> 综合 >> C# 指针 内存控制 Marshal 内存数据存储原理
  详细解决方案

C# 指针 内存控制 Marshal 内存数据存储原理

热度:23   发布时间:2023-12-10 17:49:15.0
  • 了解内存的原理

# [内存] 是由 [Key] 和 [Value] 组成的:

   [Key] 是 [内存地址];(在 C# 程序中用 [IntPtr] 类型表示)

   [Value] 是 [内存数据];(在 C# 程序中用 [byte] 类型表示)


   [Key] 是一个固定32位长度的二进制数;(64位的程序则是64位长度的二进制数)

   [Value] 是一个固定8位长度的二进制数;(这就是计算机只能存储 0 和 1,并且最小存储单位为 byte 的原因)

# 内存组成结构:(示例)

   [二进制]    格式:[Key (0111 1111 1111 1111 1111 1111 1111 1111?) => Value (1111 1111)]

   [十进制 ]   格式:[Key (2147483647) => Value (255)]

   [十六进制] 格式:[Key (0x7FFF FFFF) => Value (0xFF)]

   [程序]         格式:[Key (IntPtr) => Value (byte)]

# 数据类型组成:(示例)

   如:int 变量名 = 123456;  //假设这个变量的 [内存地址] 为 IntPtr (0x014245E0)

   内存组成:

          因为 int 类型在系统内已经定义过了 是固定长度4个byte(32位);

          所以他所占用的内存块就是由固定的4个byte组成的;

          第1个byte:[IntPtr (0x014245E0) => byte (0x40)]

          第2个byte:[IntPtr (0x014245E1) => byte (0xE2)]

          第3个byte:[IntPtr (0x014245E2) => byte (0x01)]

          第4个byte:[IntPtr (0x014245E3) => byte (0x00)]

   拼接后:IntPtr (0x014245E0) = byte[] { 0x40, 0xE2, 0x01, 0x00 };

   相当于:int 变量名 = BitConverter.ToInt32(new byte[] { 0x40, 0xE2, 0x01, 0x00 }, 0);


   那么下一个变量的 [内存地址] 将会从 IntPtr (0x014245E4) 开始;

   也不排除会间隔几个空 byte 开始的情况,因为可能前一个变量被回收了、所以空了一块;

# 另外解释一下:“为什么32位的程序最大只能申请2GB的空间?”

   答:因为 [32位] 的 [内存地址] 最大支持索引为 [0111 1111 1111 1111 1111 1111 1111 1111?]

          转换为十进制等于 [2147483647]

          即:可读取 [2147483647] 个 [Value]、或者可读取 [2147483647] 个 [byte]

          再转换 [2147483647] 个 [byte] 除 1024 / 1024 / 1024 即约等于 [2GB]

  • 了解指针的原理

# [指针] 并非想象中的 “执行完某一句代码后、再指向到另一句”

   而是直接用于 “指向一个内存地址、并读取或修改其内存数据”

# [指针] 定义时需要选择一个变量用于获取其 [内存地址]、并定义接收内存数据的 [数据类型];

# 如:定义一个变量的指针、接收类型为 int;

   即:读取此变量所在 [内存地址] 往后4个byte的数据、再转换成 int;

          支持修改指定 byte 的数据,实现修改 [值类型] 变量的数据;

          不像正常情况下的 int [值类型] 变量数据是无法直接修改的,只能重新定义;

讲完了原理再讲下代码,来个华丽的分割线;


再声明一下: 

由于 C# 程序中默认是不允许使用不安全代码,如内存控制、指针等操作;

所以关于非安全操作的代码需要写在 unsafe 语句块中;

另外还需要设置项目,开启允许不安全代码;

# 如:VS > 解决方案 > 选择项目 > 右键 > 属性 > 生成 > [√] 允许不安全代码;


1、通过指针修改 值类型 的变量数据

int val = 10;unsafe
{int* p = &val;  //定义一个指针(读取val值类型变量的内存数据,并将内存数据转换成int类型*p *= *p;       //通过指针修改变量值(类似于“val = val * val”,区别在于指针的方法是直接修改内存数据、不会改变原变量的内存地址)
}

 2、通过指针修改 引用类型 的变量数据

string val = "ABC";unsafe
{fixed (char* p = val)   //fixed用于禁止垃圾回收器重定向可移动的变量,可理解为锁定引用类型对象{*p = 'D';           //通过指针修改变量值(执行此操作后 val 变量值将会变成 "DBC")p[2] = 'E';         //通过指针修改变量值(执行此操作后 val 变量值将会变成 "DBE")int* p2 = (int*)p;  //将char类型的指针转换成int类型的指针}
}

3、通过指针修改 数组对象 的成员数据

double[] array = { 0.1, 1.5, 2.3 };unsafe
{fixed (double* p = &array[2]){*p = 0.2;           //通过指针修改变量值(执行此操作后 array 变量值将会变成{ 0.1, 1.5, 0.2 })}
}

4、通过指针修改 类对象 的字段数据

User val = new User() { age = 25 };unsafe
{fixed (int* p = &val.age)   //fixed用于禁止垃圾回收器重定向可移动的变量,可理解为锁定引用类型对象{*p = *p + 1;            //通过指针修改变量值(执行此操作后 val.age 变量值将会变成 26)}
}/*
public class User
{public string name;public int age;
}
*/

5、通过IntPtr自定义内存地址修改 值类型 数据

char val = 'A';unsafe
{int valAdd = (int)&val;             //获取val变量的内存地址,并将地址转换成十进制数//IntPtr address = (IntPtr)123;     //选择一个内存地址(可以是任何一个变量的内存地址)IntPtr address = (IntPtr)valAdd;    //选择一个内存地址(暂使用val变量的内存地址做测试)byte* p = (byte*)address;           //将指定的内存地址转换成byte类型的指针(如果指定的内存地址不可操的话、那操作时则会报异常“尝试读取或写入受保护的内存。这通常指示其他内存已损坏。”)byte* p2 = (byte*)2147483647;       //还可通过十进制的方式选择内存地址byte* p3 = (byte*)0x7fffffff;       //还可通过十六进制的方式选择内存地址*p = (byte)'B';                     //通过指针修改变量值(执行此操作后 val 变量值将会变成 'B')
}

6、void* 一个任意类型的指针

int valInt = 10;        //定义一个int类型的测试val
char valChar = 'A';     //定义一个char类型的测试valint* pInt = &valInt;    //定义一个int*类型的指针
char* pChar = &valChar; //定义一个char*类型的指针void* p1 = pInt;        //void*可以用于存储任意类型的指针
void* p2 = pChar;       //void*可以用于存储任意类型的指针pInt = (int*)p2;        //将void*指针转换成int*类型的指针 (#需要注意一点:因为都是byte数据、所以不会报转换失败异常)
pChar = (char*)p1;      //将void*指针转换成char*类型的指针(#需要注意一点:因为都是byte数据、所以不会报转换失败异常)

7、stackalloc 申请内存空间

unsafe
{int* intBlock = stackalloc int[100];char* charBlock = stackalloc char[100];
}

8、Marshal 操作内存数据

using System.Runtime.InteropServices;//int length = 1024;                //定义需要申请的内存块大小(1KB)
int length = 1024 * 1024 * 1024;    //定义需要申请的内存块大小(1GB)
IntPtr address = Marshal.AllocHGlobal(length);                //从非托管内存中申请内存空间,并返会该内存块的地址 (单位:字节)//相当于byte[length]//注意:申请内存空间不会立即在任务管理器中显示内存占用情况
try
{#region Marshal - 写入{Marshal.WriteByte(address, 111);                      //修改第一个byte中的数据Marshal.WriteByte(address, 0, 111);                   //修改第一个byte中的数据Marshal.WriteByte(address, 1, 222);                   //修改第二个byte中的数据Marshal.WriteByte(address, length - 1, 255);          //修改最后一个byte中的数据 (#此处需要注意,如果定义的偏移量超出则会误修改其他变量的数据)}#endregion#region Marshal - 读取{byte buffer0 = Marshal.ReadByte(address);             //读取第一个byte中的数据byte buffer1 = Marshal.ReadByte(address, 0);          //读取第一个byte中的数据byte buffer2 = Marshal.ReadByte(address, 1);          //读取第二个byte中的数据byte buffer3 = Marshal.ReadByte(address, length - 1); //读取最后一个byte中的数据}#endregion#region Marshal - 数组数据写入到目标内存块中{//source可以是byte[]、也可以是int[]、char[]...byte[] source = new byte[] { 1, 2, 3 };//将source变量的数组数据拷贝到address内存块中Marshal.Copy(source: source,startIndex: 0,          //从source的第一个item开始length: 3,              //选择source的3个itemdestination: address);  //选择存储的目标 (会写到address内存块的开头处)}#endregion#region Marshal - 内存块数据读取到目标数组中{//dest可以是byte[]、也可以是int[]、char[]...byte[] dest = new byte[5];Marshal.Copy(source: address,destination: dest,      //#注意:目标数组不能为空、且需要有足够的空间可接收数据startIndex: 1,          //从dest数组的第二个item开始length: 3);             //将address内存块的前3个item写入到dest数组中}#endregionunsafe{int[] array = new int[5] { 1, 2, 3, 4, 5 };int* p = (int*)Marshal.UnsafeAddrOfPinnedArrayElement(array, 1);    //获取数组第二个item的内存地址、并转换成int类型的指针char* p2 = (char*)Marshal.UnsafeAddrOfPinnedArrayElement(array, 1); //获取数组第二个item的内存地址、并转换成char类型的指针}
}
finally
{Marshal.FreeHGlobal(address);   //释放非托管内存中分配出的内存 (释放后可立即腾出空间给系统复用)
}

至此结束,希望大家能看的懂吧!再接再厉!