C#中byte[]和byte*的復制和轉換


C#中,byte數組在很多數據流中具有普遍的適用,尤其是和其他程序語言、其他架構設備、不同通訊協議等打交道時,字節流能夠保證數據的傳輸安全可靠,可以認為是最接近底層的數據類型了,因此對字節數據的操作就很常見和必要了。常見的場景是字節數組的復制,截斷等,常規、最簡單粗暴的循環系列代碼,這里就不啰嗦了,主要總結一些現有類所提供的方法。

一、byte[]的復制

byte[]具有數組的一般特性,復制數據可以使用如下方式。

0. 打印數組元素

為了顯示操作的結果,先寫一個打印字節數組元素的函數:

static void PrintArray(byte[] x)
{
  foreach(byte b in x)
  {
    Console.Write(b + " ");
  }
  Console.WriteLine();
}

1. Array.Copy方法

這是Array的靜態方法,示例代碼如下:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte[] x=new byte[5];
Array.Copy(barr, x, 4);
PrintArray(x);

顯示數組x的結果是1 2 3 4 0,紅色的來自源字節數組,因為初始化的數組,具有默認值0。這種方法可以從一個數組中復制從索引0開始的部分或全部元素。這種方式可以作為拷貝形式的字節數組截斷。

 2. ConstrainedCopy方法

這個也是Array類的靜態方法,函數原型為:

public static void ConstrainedCopy (Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length);

這個函數從指定的源索引開始,復制 Array 中的一系列元素,將它們粘貼到另一 Array 中(從指定的目標索引開始),保證在復制未成功完成的情況下撤消所有更改。

測試代碼如下:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte[] x = new byte[10];
Array.ConstrainedCopy(barr, 2, x, 6, 3);
PrintArray(x);

運行結果為:0 0 0 0 0 0 3 4 5 0,從源數組barr的第2個元素開始拷貝,放入目標數組x的第6個位置,且拷貝長度為3。

3. CopyTo方法

這是繼承了ICollection接口的類需要實現的方法,該方法將元字節數組中的所有元素,都拷貝到了目標數組中,其中第二個參數是指定了源字節數組第0個字節在目標數組中的位置,測試代碼如下:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte[] x=new byte[20]; barr.CopyTo(x, 10); PrintArray(x);

顯示的結果是0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9 0,其中紅色的是源目標數組中的值。

這種方法相對於第一種方法,更多的是應用於字節流的拼接。

4. Linq擴展方法

還有一種方式是Linq的擴展方法,這種方法比前面兩種提供了更加靈活的操作,相關的擴展方法有Skip/Take, SkipLast/TakeLast,測試代碼如下:

//需要using System.Linq;
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte[] x=barr.Skip(1).Take(3).ToArray();
PrintArray(x);

顯示的結果是2 3 4,Skip是去掉前面的元素,Take是截取多少個數據。也可以是從后面獲取,例如:

byte[] x = barr.TakeLast(4).ToArray();

則最后的結果是7 8 9 0,即獲取最后的4個元素。

當然,使用linq還可以有很多高級操作,例如我們只需要提取其中的奇數,可以使用:

byte[] x = barr.Where(b => b % 2 == 1).ToArray();

 關於Linq的使用,可以參考其他資料,此處不做展開論述了。

5. Clone方法

Array.Clone方法也可以實現拷貝,但是這種方式太生硬,只能完全復制,而且還需要做強制類型轉換,個人不推薦使用,也不展開敘述。

 6. MemoryStream類

這個類提供了內存中的流式讀寫,所以對數組的部分或全部拷貝,也是很方便的,雖然在實現上有種“曲線救國”的感覺,但是在涉及到流操作的時候,其實還是很常見且實用的,測試代碼如下:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
MemoryStream ms = new MemoryStream(barr);
ms.Position = 3;
byte[] x = new byte[5];
ms.Read(x, 0, 5);
PrintArray(x);
ms.Close();

執行的結果是顯示4 5 6 7 8,因為MemoryStream支持隨機訪問,通過設置Position,然后再按順序讀數據,甚至還可以多次使用目標數組,例如我們要把barr的前2個值和最后3個值放入x數字中,可以這么寫:

ms.Position = 0;
byte[] x = new byte[5];
ms.Read(x, 0, 2);
ms.Position = 7;
ms.Read(x, 2, 3);

當然這種方法相對來說也是比較占用內存的,通常用於數組不大,而又需要多次、隨機訪問的場合。

 

二、byte[]的截斷

前面通過復制的方式,可以獲取字節數組的部分元素,但是需要一個新的數組,那么有沒有在位截斷的方法呢?也是有的,使用Array的靜態方法Resize:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
Array.Resize(ref barr, 5);
PrintArray(barr);

測試結果是1 2 3 4 5。這種方式不需要額外的內存。

 

三、byte[]和byte*的互換

在C#中,偶爾還會碰到byte*的指針類型 ,這就會涉及到了byte*和byte[]之間的轉換,以及byte*的復制等問題。byte*在C#中的出鏡率不高,畢竟是unsafe的,不過在一些諸如Socket等的方法中還是有露臉的機會。

目前發現,從byte[]到byte*,或者反過來,沒有直接的轉換方法,不能像C語言那樣有直接取數組的首地址,畢竟C#是一個強類型語言。能做的只是分配地址,然后在其中拷貝數據,其中會牽扯到Iunsafe代碼,以及ntPtr指針類型,可以將byte*理解為是IntPtr的強制類型轉換。

1. 從byte[]到byte*

 測試代碼如下:

byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte* bp = (byte*)Marshal.AllocHGlobal(5);
PrintPtr(bp, 5);
Marshal.Copy(barr, 3, (IntPtr)bp, 5);
PrintPtr(bp, 5);
Marshal.FreeHGlobal((IntPtr)bp);

其中PrintPtr函數如下:

static void PrintPtr(byte* bp,int n)
{
    for(int i=0;i<n;++i)
        Console.Write(bp[i] + " ");
    Console.WriteLine();
}

程序輸出的結果是:

176 63 95 127 17
4 5 6 7 8

因為是堆上分配的,所以其中第一行的值(紅色)是亂碼,每次都不一樣,第二行的值是通過復制Marshal.Copy將其填充到了byte*所指向的地址。

2.從byte*轉byte[]

 同樣是使用Marshal的靜態方法。測試代碼如下:

byte[] barr = new byte[10];
byte* bp = (byte*)Marshal.AllocHGlobal(10);
for (int i = 0; i < 10; i++)
    bp[i] = (byte)i;
Marshal.Copy((IntPtr)bp, barr, 0, 10);
PrintArray(barr);
Marshal.FreeHGlobal((IntPtr)bp);

執行結果是輸出0 1 2 3 4 5 6 7 8 9。

四、byte*的復制

byte*的復制,可以理解為是void*類型的復制,即內存數據的復制,在C#中表現為IntPtr所指向的兩個內存,那么就有相關的函數可以使用,例如平台調用,最常見的是CopyMemory或者MoveMemory。

[DllImport("kernel32.dll", EntryPoint = "RtlCopyMemory", CharSet = CharSet.Ansi)]
public extern static long CopyMemory(IntPtr dest, IntPtr source, int size);

測試代碼:

IntPtr p1 = Marshal.AllocHGlobal(10);
IntPtr p2 = Marshal.AllocHGlobal(20);
for (int i=0;i<10;i++)
{
    ((byte*)p1)[i] = (byte)i;
}
CopyMemory(p2, p1, 10);
PrintPtr((byte*)p1, 10);
PrintPtr((byte*)p2, 20);

Marshal.FreeHGlobal(p1);
Marshal.FreeHGlobal(p2);

運行的結果是:

0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 232 85 67 0 92 0 32 186 184 215

第二行后面十個元素是亂碼(紅色數字)。

有CopyMemory函數,自然也可以使用MoveMemory函數,定義如下:

[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", CharSet = CharSet.Ansi)]
public extern static long MoveMemory(IntPtr dest, IntPtr source, int size);

兩者的使用方法差不多,但是MoveMemory有個好處是當內存中出現重疊時,結果是唯一的,而CopyMemory似乎不能保證(簡單的測試顯示效果相同,但是可能是由於測試環境和數據偏少)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM