首頁 > 軟體

C# 指標記憶體控制Marshal記憶體資料儲存原理分析

2023-02-27 06:00:51

瞭解記憶體的原理

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、通過指標修改 參照型別 的變數資料

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型別的測試val
 
int* 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 - 讀取
    {
        int offset = length - 1;    //定義讀取最後一個byte的內容
 
        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個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);   //釋放非託管記憶體中分配出的記憶體 (釋放後可立即騰出空間給系統複用)
}

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com