溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

C#中指針的示例分析

發(fā)布時(shí)間:2021-07-10 12:33:05 來(lái)源:億速云 閱讀:240 作者:小新 欄目:開(kāi)發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)C#中指針的示例分析,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

    一、簡(jiǎn)潔優(yōu)美的代碼

    本來(lái)初稿這節(jié)寫(xiě)了好幾百字,將C#指針開(kāi)發(fā)與C/C++開(kāi)發(fā),Java開(kāi)發(fā)、D語(yǔ)言開(kāi)發(fā)等進(jìn)行對(duì)比,闡述理念。不過(guò)現(xiàn)在覺(jué)得,闡述一個(gè)新事物,沒(méi)有比用例子更直接的了。

    例子:打開(kāi)一張圖像,先將它轉(zhuǎn)化為灰度圖像,再進(jìn)行二值化(變成黑白圖像),然后進(jìn)行染色,將白色的像素變成紅色。以上每一個(gè)過(guò)程都彈出窗體顯示出來(lái)。

    代碼截圖更有視覺(jué)沖擊力:

    C#中指針的示例分析

    二、C# 指針基礎(chǔ)

    在C#中使用指針,需要在項(xiàng)目屬性中選中“Allow unsafe code”:

    C#中指針的示例分析

    接著,還需要在使用指針的代碼的上下文中使用unsafe關(guān)鍵字,表明這是一段unsafe代碼??梢杂胾nsafe { } 將代碼圍住,如:

     unsafe
                         {
                             new ImageArgb32(path).ShowDialog("原始圖像")
                                 .ToGrayscaleImage().ShowDialog("灰度圖像")
                                 .ApplyOtsuThreshold().ShowDialog("二值化圖像")
                                 .ToImageArgb32()
                                 .ForEach((Argb32* p) => { if (p->Red == 255) *p = Argb32.RED; })
                                 .ShowDialog("染色");
                         }

    也可在方法或?qū)傩陨霞尤雞nsafe關(guān)鍵字,如:

       private unsafe void btnSubmit_Click(object sender, EventArgs e)

    也可在class或struct 上加上unsafe 關(guān)鍵字,如:

    public partial unsafe class FrmDemo1 : Form

    指針配合fixed關(guān)鍵字可以操作托管堆上的值類(lèi)型,如:

      public unsafe class Person
        {
            public int Age;
            public void SetAge(int age)
            {
                fixed (int* p = &Age)
                {
                    *p = age;
                }
            }
        }

    指針可以操作棧上的值類(lèi)型,如:

       int age = 0;
                 int* p = &age;
                 *p = 20;
                 MessageBox.Show(p->ToString());

    指針也可以操作非托管堆上的內(nèi)存,如:

     IntPtr handle = System.Runtime.InteropServices.Marshal.AllocHGlobal(4);
                 Int32* p = (Int32*)handle;
                 *p = 20;
                 MessageBox.Show(p->ToString());
                 System.Runtime.InteropServices.Marshal.FreeHGlobal(handle);

    System.Runtime.InteropServices.Marshal.AllocHGlobal 用來(lái)從非托管堆上分配內(nèi)存。System.Runtime.InteropServices.Marshal.FreeHGlobal(handle)用來(lái)釋放從非托管對(duì)上分配的內(nèi)存。這樣我們就可以避開(kāi)GC,自己管理內(nèi)存了。

    三、幾種常用用法

    1、使用Dispose模式管理非托管內(nèi)存

    如果使用非托管內(nèi)存,建議用Dispose模式來(lái)管理內(nèi)存,這樣做有以下好處: 可以手動(dòng)dispose來(lái)釋放內(nèi)存;可以使用using 關(guān)鍵字開(kāi)管理內(nèi)存;即使不釋放,當(dāng)Dispose對(duì)象被GC回收時(shí),也會(huì)收回內(nèi)存。

    下面是Dispose模式的簡(jiǎn)單例子:

    public unsafe class UnmanagedMemory : IDisposable
              {
                  public int Count { get; private set; }
                  private byte* Handle;
                 private bool _disposed = false;
                  public UnmanagedMemory(int bytes)
                  {
                     Handle = (byte*) System.Runtime.InteropServices.Marshal.AllocHGlobal(bytes);
                     Count = bytes;
                }
                 public void Dispose()
                 {
                     Dispose(true);
                     GC.SuppressFinalize(true);
                 }
                protected virtual void Dispose( bool isDisposing )
                 {
                     if (_disposed) return;
                     if (isDisposing)
                     {
                         if (Handle != null)
                         {                         System.Runtime.InteropServices.Marshal.FreeHGlobal((IntPtr)Handle);
                         }
                     }
                     _disposed = true;
                 }
                 ~UnmanagedMemory()
                {
                   Dispose( false );
                }
             }

    使用:

      using (UnmanagedMemory memory = new UnmanagedMemory(10))
                {
                    int* p = (int*)memory.Handle;
                    *p = 20;
                    MessageBox.Show(p->ToString());
                }

    2、使用 stackalloc 在棧中分配內(nèi)存

    C# 提供了stackalloc 關(guān)鍵字可以直接在棧中分配內(nèi)存,一般情況下,使用棧內(nèi)存會(huì)比使用堆內(nèi)存速度快,且棧內(nèi)存不用擔(dān)心內(nèi)存泄漏。下面是例子:

       int* p = stackalloc int[10];
                 for (int i = 0; i < 10; i++)
                 {
                     p[i] = 2 * i + 2;
                 }
                 MessageBox.Show(p[9].ToString());

    3、模擬C中的union(聯(lián)合體)類(lèi)型

    使用 StructLayout 可以模擬C中的union:

      [StructLayout(LayoutKind.Explicit)]
            public struct Argb32
            {
                [FieldOffset(0)]
                public Byte Blue;
                [FieldOffset(1)]
                public Byte Green;
                [FieldOffset(2)]
                public Byte Red;
                [FieldOffset(3)]
                public Byte Alpha;
                [FieldOffset(0)]
                public Int32 IntVal;
            }

    這個(gè)和指針無(wú)關(guān),非unsafe環(huán)境下也可使用,有很多用途,比如,序列化和反序列化,求hash值 ……

    四、C# 指針操作的幾個(gè)缺點(diǎn)

    C# 指針操作的缺點(diǎn)也不少。下面一一道來(lái)。

    缺點(diǎn)1:只能用來(lái)操作值類(lèi)型

    .Net中,引用類(lèi)型的內(nèi)存管理全部是由GC代勞,無(wú)法取得其地址,因此,無(wú)法用指針來(lái)操作引用類(lèi)型。所以,C#中指針操作受到值類(lèi)型的限制,其中,最主要的一點(diǎn)就是:值類(lèi)型無(wú)法繼承。

    這一點(diǎn)看起來(lái)是致命的,其實(shí)不然。首先,需要用到指針來(lái)提高性能的地方,其類(lèi)型是很少變動(dòng)的。其次,在OO編程中有個(gè)名言:組合優(yōu)于繼承。使用組合,我們可以解決很多需要繼承的地方。第三,最后,我們還可以使用引用類(lèi)型來(lái)對(duì)值類(lèi)型打包,進(jìn)行繼承,權(quán)衡兩者的比重來(lái)完成任務(wù)。

    缺點(diǎn)2:泛型不支持指針類(lèi)型

    C# 中泛型不支持指針類(lèi)型。這是個(gè)很大的限制,在后面的篇幅中,我會(huì)引入模板機(jī)制來(lái)克服這個(gè)問(wèn)題。同理,迭代器也不支持指針,因此,我們需要自己實(shí)現(xiàn)迭代機(jī)制。

    缺點(diǎn)3:沒(méi)有函數(shù)指針

    幸運(yùn)的是,C# 中有delegate,delegate 支持支持指針類(lèi)型,lambda 表達(dá)式也支持指針。后面會(huì)詳細(xì)講解。

    五、引入模板機(jī)制

    沒(méi)有泛型,但是我們可以模擬出一套類(lèi)似C++的模板機(jī)制出來(lái),進(jìn)行代碼復(fù)用。這里大量的用到了C#的語(yǔ)法糖和IDE的支持。

    先介紹原理:

    partial 關(guān)鍵字讓我們可以將一個(gè)類(lèi)的代碼分在多個(gè)文件,那么可以這樣分:第一個(gè)文件是我們自己寫(xiě)的代碼,第二個(gè)文件用來(lái)描述模板,第三個(gè)文件,用來(lái)根據(jù)模板自動(dòng)生成代碼。

    三個(gè)文件這樣取名字的:

    C#中指針的示例分析

    XXXClassHelper 是模板定義文件,XXXClassHelper_Csmacro.cs 是自動(dòng)生成的模板實(shí)現(xiàn)代碼。

    ClassHelper文件的例子:

    namespace Geb.Image
    {
        using TPixel = Argb32;
        using TCache = System.Int32;
        using TKernel = System.Int32;
        using TImage = Geb.Image.ImageArgb32;
        using TChannel = System.Byte;
        public static partial class ImageArgb32ClassHelper
        {
            #region include "ImageClassHelper_Template.cs"
            #endregion
        }
        public partial class ImageArgb32
        {
            #region include "Image_Template.cs"
            #endregion
            #region include "Image_Paramid_Argb_Templete.cs"
            #endregion
        }
        public partial struct Argb32
        {
            #region include "TPixel_Template.cs"
            #endregion
        }
    }

    這里用到了using 語(yǔ)法糖。using 關(guān)鍵字,可以為一個(gè)類(lèi)型取別名。使用 VS 的 #region 來(lái)定義所使用的模板文件的位置。上面這個(gè)文件中,引用了4個(gè)模板文件:ImageClassHelper_Template.cs,Image_Template.cs,Image_Paramid_Argb_Templete.csTPixel_Template.cs

    只看其中的一個(gè)模板文件 Image_Template.cs

     using TPixel = System.Byte;
     using TCache = System.Int32;
     using TKernel = System.Int32;
     using System;
     using System.Collections.Generic;
     using System.Text;
     namespace Geb.Image.Hidden
     {
         public abstract class Image_Template : UnmanagedImage<TPixel>
         {
             private Image_Template()
                 : base(1,1)
             {
                 throw new NotImplementedException();
             }
             #region mixin
             public unsafe TPixel* Start { get { return (TPixel*)this.StartIntPtr; } }
             public unsafe TPixel this[int index]
             {
                 get
                 {
                     return Start[index];
                 }
                 set
                 {
                     Start[index] = value;
                 }
             }
       
       ……
     
             #endregion
         }
     }

    這個(gè)模板文件是編譯通過(guò)的。也使用了 using 關(guān)鍵字來(lái)對(duì)使用的類(lèi)型取別名,同時(shí),在代碼中,有一段用 #region mixin #endregion 環(huán)繞的代碼。只需要寫(xiě)一個(gè)工具,將模板文件中 #region mixin#endregion 環(huán)繞的代碼提取出來(lái),替換到模板定義中 #region include "Image_Template.cs" 和 #endregion 之間,生成第三個(gè)文件 ClassHelper_Csmacro.cs 即可實(shí)現(xiàn)模板機(jī)制。由于都使用了 using 關(guān)鍵字對(duì)類(lèi)型取別名,因此,ClassHelper_Csmacro.cs 文件也是可以編譯通過(guò)的。在不同的模板定義中,令同樣的符號(hào)來(lái)代表不同的類(lèi)型,實(shí)現(xiàn)了模板代碼的公用。

    上面機(jī)制可以全部自動(dòng)化。Csmacro 是我寫(xiě)的一個(gè)工具,可以完成上面的過(guò)程。將它放在系統(tǒng)路徑下,然后在項(xiàng)目的build event中添加pre-build 指令即可。Csmacro程序在代碼包的lib的目錄下。

    C#中指針的示例分析

    如此實(shí)裝,我們就有模板用了!一切自動(dòng)化,就好像內(nèi)置的一樣。強(qiáng)類(lèi)型、有編譯器進(jìn)行類(lèi)型約束,減少出錯(cuò)的可能。調(diào)試也很容易,就和調(diào)試普通的C#代碼一樣,不存在C++中的模板的難調(diào)試問(wèn)題。缺點(diǎn)嘛,就是沒(méi)有C++中模板的語(yǔ)法優(yōu)美,但是,也看的過(guò)去,至少比C中的宏好看多了是吧。

    參照上面對(duì)模板的實(shí)現(xiàn),完全可以定義出一套C#的宏出來(lái)。沒(méi)這樣做,是因?yàn)闆](méi)這個(gè)需求。

    下面是一個(gè)完整的例子,為 Person 類(lèi)和 Cat 類(lèi)添加模板擴(kuò)展方法(非擴(kuò)展方法也可類(lèi)似添加),由于這個(gè)方法有指針,無(wú)法用泛型實(shí)現(xiàn):

    void SetAge(this T item,  int* age)

    首先,建一個(gè)可編譯通過(guò)的模板類(lèi) Template.cs

     namespace Introduce.Hide
     {
         using T = Person;
         public static class Template
         {
             #region mixin
             public static unsafe void SetAge(this T item,  int* age)
             {
                 item.Age = *age;
             }
             #endregion
         }
     }

    我在命名空間中加入了 Hide,只要不引用這個(gè)命名空間,這個(gè)擴(kuò)展方法不會(huì)出現(xiàn)對(duì)程序產(chǎn)生干擾。

    接著,建立 PersonClassHelper.cs 文件:

    namespace Introduce
     {
         using T = Person;
         public static partial class PersonClassHelper
         {
             #region include "Template.cs"
             #endregion 
         }
     }

    建立 CatClassHelper.cs 文件:

     namespace Introduce
     {
         using T = Cat;
         public static partial class CatClassHelper
         {
             #region include "Template.cs"
             #endregion
         }
     }

    為了節(jié)省篇幅,我省略了命名空間的引用,實(shí)際代碼中是有命名空間的引用的。下載包里包含了全部的代碼。接下來(lái),編譯一下,哈哈,編譯通過(guò)。

    且慢,怎么看不到編譯生成的兩個(gè) Csmacro.cs 文件呢?

    這兩個(gè)文件已經(jīng)生成了,需要手動(dòng)將它們添加到項(xiàng)目中,只用添加一次即可。添加進(jìn)來(lái),再編譯一下,哈哈,通過(guò)。

    這個(gè)例子雖小,可不要小看模板啊,在Geb.Image庫(kù)里,大量使用了模板:

    C#中指針的示例分析

    有了模板,只用維護(hù)公共代碼。

    六、迭代器

    下面來(lái)實(shí)現(xiàn)迭代器。這里,要放棄使用foreach,返回古老的迭代器模式,來(lái)訪(fǎng)問(wèn)圖像的每一個(gè)像素:

       public unsafe struct ItArgb32Old
        {
            public unsafe Argb32* Current;
            public unsafe Argb32* End;
            public unsafe Argb32* Next()
            {
                if (Current < End) return Current ++;
                else return null;
            }
        }
        public static class ImageArgb32Helper
        {
            public unsafe static ItArgb32Old CreateItorOld(this ImageArgb32 img)
            {
                ItArgb32Old itor = new ItArgb32Old();
                itor.Current = img.Start;
                itor.End = img.Start + img.Length;
                return itor;
            }
        }

    不幸的是,測(cè)試性能,這個(gè)迭代器比單純的while循環(huán)慢很多。對(duì)一個(gè)100萬(wàn)像素的圖像,將其每一個(gè)像素值的Red分量設(shè)為200,循環(huán)100遍,使用迭代器在我的電腦上耗時(shí)242 ms,直接使用循環(huán)耗時(shí) 72 ms。我測(cè)試了很多種方案,均未得到和直接循環(huán)性能近似的迭代器實(shí)現(xiàn)方案。

    沒(méi)有辦法,只好對(duì)迭代器來(lái)打折了,只進(jìn)行部分抽象(這已經(jīng)不能算迭代器了,但這里仍沿用這個(gè)名稱(chēng)):

     public unsafe struct ItArgb32
         {
             public unsafe Argb32* Start;
             public unsafe Argb32* End;
             public int Step(Argb32* ptr)
             {
                 return 1;
             }
         }

    產(chǎn)生迭代器的代碼:

       public unsafe static ItArgb32 CreateItor(this ImageArgb32 img)
         {
             ItArgb32 itor = new ItArgb32();
             itor.Start = img.Start;
             itor.End = img.Start + img.Length;
             return itor;
         }

    使用:

       ItArgb32 itor = img.CreateItor();
         for (Argb32* p = itor.Start; p < itor.End; p+= itor.Step(p))
         {
             p->Red = 200;
         }

    測(cè)試性能和直接循環(huán)性能幾乎一樣。有人可能要問(wèn),你這樣有什么優(yōu)勢(shì)?和for循環(huán)有什么區(qū)別?

    這個(gè)例子中當(dāng)然看不出優(yōu)勢(shì),換個(gè)例子就可以看出來(lái)了。

    在圖像編程中,有 ROI(Region of Interest,感興趣區(qū)域)的概念。比如,在下面這張女王出場(chǎng)的畫(huà)面中,假設(shè)我們只對(duì)她的頭部感興趣(ROI區(qū)域),只對(duì)該區(qū)域進(jìn)行處理(標(biāo)注為紅色區(qū)域)。

    對(duì)ROI區(qū)域創(chuàng)建一個(gè)迭代器,用來(lái)迭代ROI中的每一行:

      public unsafe struct ItRoiArgb32
        {
            public unsafe Argb32* Start;
            public unsafe Argb32* End;
            public int Width;
            public int RoiWidth;
            public int Step(Argb32* ptr)
            {
                return Width;
            }
            public ItArgb32 Itor(Argb32* p)
            {
                ItArgb32 it = new ItArgb32();
                it.Start = p;
                it.End = p + RoiWidth;
                return it;
            }
        }

    這個(gè)ROI迭代器又可以產(chǎn)生一個(gè)ItArgb32迭代器,來(lái)迭代該行中的像素。

    產(chǎn)生ROI迭代器的代碼如下,為了簡(jiǎn)化代碼,我這里沒(méi)有進(jìn)行ROI的驗(yàn)證:

     public unsafe static ItRoiArgb32 CreateRoiItor(this ImageArgb32 img,
                int x, int y, int roiWidth, int roiHeight)
            {
                ItRoiArgb32 itor = new ItRoiArgb32();
                itor.Width = img.Width;
                itor.RoiWidth = roiWidth;
                itor.Start = img.Start + img.Width * y + x;
                itor.End = itor.Start + img.Width * roiHeight;
                return itor;
            }

    性能測(cè)試表明,使用ROI迭代器進(jìn)行迭代和直接進(jìn)行循環(huán),性能一致。為一副圖像添加ROI字段,設(shè)置ROI值來(lái)控制不同的處理區(qū)域,然后用ROI迭代器進(jìn)行迭代,比直接使用循環(huán)要方便得多。

    七、風(fēng)情萬(wàn)種的Lambda表達(dá)式

    接下來(lái),來(lái)看看C#指針最有風(fēng)情的一面——Lambda表達(dá)式。 C# 里 delegate 支持指針,下面這種寫(xiě)法是沒(méi)有問(wèn)題的:

     void ActionOnPixel(TPixel* p);

    對(duì)于圖像處理,我定義了許多擴(kuò)展方法,F(xiàn)orEach是其中的一種,下面是它的模板定義:

     public unsafe static UnmanagedImage<TPixel> ForEach(this UnmanagedImage<TPixel> src, ActionOnPixel handler)
            {
                TPixel* start = (TPixel*)src.StartIntPtr;
                TPixel* end = start + src.Length;
                while (start != end)
                {
                    handler(start);
                    ++start;
                }
                return src;
            }

    讓我們用lambda表達(dá)式對(duì)圖像迭代,將每像素的Red分量設(shè)為200吧,一行代碼搞定:

    img.ForEach((Argb32* p) => { p->Red = 200; });

    用ForEach測(cè)試,對(duì)100萬(wàn)像素的圖像設(shè)置Red通道值為200,循環(huán)100次,我的測(cè)試結(jié)果是 400 ms,約是直接循環(huán)的 4-5 倍??梢?jiàn)這是個(gè)性能不高的操作(其實(shí)也夠高了,100萬(wàn)象素,循環(huán)100遍,耗時(shí)400ms),可以在對(duì)性能要求不是特別高時(shí)使用。

    八、與C/C++的比較

    我測(cè)試了很多場(chǎng)景,C# 下指針性能約是 C/C++ 的 70-80%,性能差距,可以忽略。

    相對(duì)于C/C++來(lái)說(shuō),C#無(wú)法直接操作硬件是其遺憾,這種情況,可以使用C/C++寫(xiě)段小程序來(lái)彌補(bǔ),不過(guò),我還沒(méi)遇到這種場(chǎng)景。很多情況都可以P/Invoke解決。

    做圖像的話(huà),很多時(shí)候需要使用顯卡加速,如使用CUDA或OpenCL,幸運(yùn)的是,C#也可以直接寫(xiě)CUDA或OpenCL代碼,但是功能可能會(huì)受到所用的庫(kù)的限制。也可以用傳統(tǒng)方式寫(xiě)CUDA或OpenCL代碼,再P/Invoke調(diào)用。如果用傳統(tǒng)的C/C++開(kāi)發(fā)的話(huà),也需要做同樣的工作。

    和C比較:

    這套方案比C的抽象程度高,我們有模板,有l(wèi)ambda表達(dá)式,還有一大票的語(yǔ)法糖。在類(lèi)庫(kù)上,比C的類(lèi)庫(kù)完善的多。我們還有反射,有命名空間等等一大票的東西。

    和C++比較:

    這套方案的抽象程度比C++要低一些。畢竟,值類(lèi)型無(wú)法繼承,模板機(jī)制比C++ 差一點(diǎn)。但是在生產(chǎn)力上比C++要高很多。拋開(kāi)C++那一大票陷阱不說(shuō),以秒計(jì)算的編譯速度就夠讓C++程序員流口水的。當(dāng)我們?cè)诳Х瑞^里約會(huì)喝咖啡時(shí),C++程序員還正端著一杯咖啡坐在電腦前等待程序編譯結(jié)束。

    關(guān)于“C#中指針的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI