开发者

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

开发者 https://www.devze.com 2023-02-27 10:33 出处:网络 作者: s666666__
目录了解内存的原理了解指针的原理1、通过指针修改 值类型 的变量数据2、通过指针修改 引用类型 的变量数据3、通过指针修改 数组对象 的成员数据4、通过指针修改 类对象 的字段数据5、通过IntPtr自定义内存地址修改
目录
  • 了解内存的原理
  • 了解指针的原理
  • 1、通过指针修改 值类型 的变量数据
  • 2、通过指针修改 引用类型 的变量数据
  • 3、通过指针修改 数组对象 的成员数据
  • 4、通过指针修改 类对象 的字段数据
  • 5、通过IntPtr自定义内存地址修改 值类型 数据
  • 6、void* 一个任意类型的指针
  • 7、stackalloc 申请内存空间
  • 8、Marshal 操作内存数据
  • 总结

了解内存的原理

1、内存是由 Key 和 Value 组成,Key 是内存地址、Value 是存储的数据;

2、Key:是一个32位长度的二进制数;(64位的程序则是64位长度的二进制)

  • > 32位最大值为二进制 ‭0111 1111 1111 1111 1111 1111 1111 1111‬
  • 或十六进制 0x7FFF FFFF,或十进制 2 147 483 647 (2GB) (int.MaxValue);
  • > 在C#程序中由 IntPtr 类型进行存储,常以十六进制数进行交互;

3、Value:则是一个8位长度的二进制数;(所以说计算机只能存储 0 和 1 就是这原因)

  • > 最大值为二进制 1111 1111‬,或十六进制 0xFF,或十进制 255;
  • > 也就是 1byte 的数据,所以说计算机最小存储单位为 byte 也正是如此;

4、内存组成结构如下:

  • >  二进制:Key (0111 1111 1111 1111 1111 1111 1111 1111‬) = Value (1111 1111)
  • >  十六进制:Key (0x7FFF FFFF) = Value (0xFF)
  • >  十进制:Key (2 147 483 647) = Value (255)
  • >  程序:Key (IntPtr) = Value (byte)

了解指针的原理

1、指针是用于指向一个值类型数据,非想象中的面向过程逻辑、认为第一个读取后会自动指向下一个,哈哈;

2、如 int 类型的指针,就是将指定内存地址中的数据转换成 int 数据;

3、由于 int 类型长度为32位(4byte),所以指针读取数据时会自动取连续4byte的数据来转换成 int;

  • > 如一个 int 类型值为 123456,假设他的内存地址为 IntPtr(0x014245E0),那么他所占用的内存块则为以下:
  • 第1byte:IntPtr(0x014245E0) = byte(0x40)
  • 第2byte:IntPtr(0x014245E1) = byte(0xE2)
  • 第3byte:IntPtr(0x014245E2) = byte(0x01)
  • 第4byte:IntPtr(0x014245E3) = byte(0x00)
  • 组成结构为:IntPtr(0x014245E0) = byte[] { 0x40, 0xE2, 0x01, 0x00 }
  • > 那么下一个对象则就从 IntPtr(0x014245E4) 开始,如:IntPtr(0x014245E4) = byte[] { 0x00, 0x00, 0x00, 0x00 };

OK,说完原理得开始说代码了,来个华丽的分割线;

再声明一下: 

  • 1、由于 C# 程序中默认是不允许使用不安全代码,如内存控制、指针等操作;
  • 2、所以关于非安全操作的代码需要写在 unsafe 语句块中;
  • 3、另外还需要设置允许使用不安全代码,如:解决方案 > 选择项目 > 右键 > 属性 > 生成 > [√] 允许不安全代码;

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

int val = 10;

unsafe
{
  int* p = &val; //&val用于获取val变量的内存地址,*p为int类型指针、用于间接访问val变量

  *p *= *p;    //通过指针修改变量值(执行此操作后 val 变量值将会变成 100)
}

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

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.aphpge)  //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类型的测试val

int* pInt = &valInt;  //定义一个int*类型的指针
char* pChar = &valChar; //定义一个char*类型的指针

void* p1 = pInt;    //void*可以用于存储任意类型的指针
void* p2 = pChar;    //void*可android以用于存储任意类型的指针

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 * 102开发者_Python教程4;  //定义需要申请的内存块大小(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 - 读取
  {
    int offset = length - 1;  //定义读取最后一个byte的内容

    byte buffer0 = Marshal.ReadByte(address);       //读取第一个byte中的数据
    byte buffer1 = Marshal.ReadByte(address, 0);     //读取第一个byte中的数据
    byte buffer2 = Marsphphal.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开始
      lengtjavascripth: 3,       //选择source的3个item
      destination: 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数组中
  }
  #endregion

  unsafe
  {
    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);  //释放非托管内存中分配出的内存 (释放后可立即腾出空间给系统复用)
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号