C#'ta Gösterici(Pointer) Kullanmak – III

C#'ta göstericilerin kullanımı ile ilgili yazı dizisinin son bölümü olan bu yazıda gösericiler ile dizi islemlerinin nasıl yapıldıgi, stackalloc ıle dinamik bellek tahsısatının yapılmasını ve son olarak yapı(struct) göstericilerinin kullanılışını inceleyeceğiz.

Göstericiler ile Dizi İşlemleri

C#'ta tanımladıgımız diziler System.Array sınıfi türündendir. Yani bütün diziler managed type kapsamına girerler. Bu yüzden tanımladıgımız bir dizinin herhangi bir elemanının adresini fixed bloğu kullanmadan bir göstericiye atayamayız. Bu yüzden göstericiler ile dizi islemleri yaparken ya fixed bloklari kullanıp gereksiz nesne toplayıcısını uyarmalıyız yada stackalloc anahtar sözcüğünü kullanarak kendimiz unmanaged type(yönetilemeyen tip) dizileri olusturmalıyız. Her iki durumuda birazdan inceleyeceğiz.

Bildiğiniz gibi dizi elemanları bellekte ardışıl bulunur. O halde bir dizinin elemanlarını elde etmek için dizinin ilk elemanının adresini ve dizinin boyutunu bilmemiz yeterlidir. System.Array sınıfı dizilerinin elemanlarina göstericiler yardimiyla rahatlıkla ulasabiliriz. Bunun için ilk olarak fixed ile isaretlenmis bir blok içerisinde dizinin ilk elemanının adresini bir göstericiye atamalıyız. Ardından göstericinin degerini bir döngü içerisinde birer birer artırdıgımızda her döngüde dizinin bir sonraki elemanina ulaşmış oluruz. Dizilere bu sekilde erisebilmemizi sağlayan ise gösterici aritmetiğidir. Tabi dizilerin elemanlarının bellekte ardışıl bulunması da bu işlemi bu şekilde yapmamızı sağlayan ilk etkendir.

Şimdi yönetilen türden(managed) bir dizinin elemanlarına nasıl eriştiğimizi bir örnek üzerinde inceleyelim.

using System;

class Gosterici
{
   
unsafe static void Main()
   {

      int[] a = {1,2,3,4};

      fixed(int* ptr = &a[0])
      {
        
 for(int i=0; i<a.Length; ++i)
            Console.WriteLine(*(ptr+i));
      }
   }
}

Programı derlediğinizde dizinin elemanlarının

1
2
3
4

seklinde ekrana yazdırıldığını görürsünüz. (programı derlerken /unsafe argümanını kullanmayı unutmayın). Programın kritik noktası

fixed(int* ptr = &a[0])

satırı ile dizinin ilk elemanının adresinin elde edilmesidir. Zira dizinin diğer elemanlarıda sırayla ilk elemandan itibaren her eleman için adres değeri 4 byte artacak şeklindedir. Diger önemli nokta ise for döngüsü içindeki

*(ptr+i)

ifadesidir. Bu ifade her döngüde ptr göstericisinin adres bileşeni, döngü degişkeni kadar artırılıyor, sözgelimi döngü degişkeni 1 ise ptr'nin adres bileşeni 4 artırılıyor. Bu da dizinin ikinci elemanının bellekte bulunduğu adrestir. İçerik operatörü ile bu adrese erişildiğinde ise dizinin elemanı elde edilmis olur.

Gösterici dizileri ile ilgili diğer önemli nokta göstericilerin indeksleyici gibi kullanılabilmesidir. Örneğin yukarıdaki örnekte bulunan

*(ptr+i)

ifadesini

ptr[i]

şeklinde degiştirebiliriz. Burdan aşağıdaki eşitlikleri çıkarabiliriz.

*(ptr+0) == ptr[0]
*(ptr+1) == ptr[1]
*(ptr+2) == ptr[2]
*(ptr+3) == ptr[3]

Dikkat: ptr[i] bir nesne belirtirken (ptr+i) bir adres belirtir.

Dizilerin isimleri aslinda dizilerin ilk elemanının adresini temsil etmektedir. Örneğin aşagıdaki programda bir dizinin ilk elemanının adresi ile dizinin ismi int türden bir göstericiye atanıyor. Bu iki göstericinin adres bileşenleri yazdırıldıgında sonucun aynı olduğu görülmektedir.

using System;

class Gosterici
{
  
 unsafe static void Main()
   {
      
int[] a = {1,2,3,4};

      fixed(int* ptr1 = a, ptr2 = &a[0])
      {
         Console.WriteLine((uint)ptr1);
         Console.WriteLine((uint)ptr2);
      }
   }
}

Programı derleyip çalıştırdığınızda ekrana alt alta iki tane aynı sayının yazıldığını görürsünüz.

Yönetilen(managed) tiplerle çalışmak her ne kadar kolay olsa da bazı performans eksiklikleri vardır. Örneğin bir System.Array dizisinin bir elemanına erişmek ile stack bölgesinde olusturacagımız bir dizinin elemanına ulaşmamız arasında zaman açısından büyük bir fark vardır. Bu yüzden yüksek performanslı dizilerle çalışmak için System.Array sınıfının dışında stack tabanlı diziler oluşturmamız gerekir. Stack tabanlı diziler yönetilemeyen dizilerdir. Bu yüzden bu tür dizileri kullanırken dikkatli olmalıyız. Çünkü her an bize tahsis edilmeyen bir bellek alanı üzerinde islem yapiyor olabiliriz. Ancak yönetilen dizilerde dizinin sınırlarını aşmak mümkün degildir. Hatırlarsanız bir dizinin sınırları aşılınca çalışma zamanında IndexOutOfRangeException istisnai durumu meydana geliyordu. Oysa stack tabanlı dizilerde dizinin sınırları belirli degildir ve tabiki dizinin sınırlarını aşmak kısıtlanmamıştır. Eğer dizinin sınırları aşılmışsa muhtemelen bu işlem bir hata sonucu yapılmıştır. Hiçbir programcı kendisine ait olmayan bir bellek alanında islem yapmamalıdır. Aksi halde sonuçlarına katlanması gerekir.

Stack tabanlı diziler stackalloc anahtar sözcüğü ile yapılır. stackalloc bize istediğimiz miktarda stack bellek bölgesinden alan tahsis eder. Ve tahsis edilen bu alanın başlangıç adresini geri döndürür. Dolayısıyla elimizde olan bu baslangıç adresi ile stackalloc ile bize ayrılmış olan bütün bellek bölgelerine erişebiliriz. stackalloc anahtar sözcüğünün kullanımı aşağıdaki gibidir.

int * dizi = stackalloc int[10];

Bu deyim ile stack bellek bölgesinde 10*sizeof(int) = 40 byte'lık bir alan programcının kullanmasi için tahsis edilir. Bu alan, dizinin faaliyet alanı bitinceye kadar bizim emrimizdedir. Tahsis edilen bu 40 byte büyüklüğündeki bellek alanının ilk byte'ının adresi ise int türden gösterici olan dizi elemanına aktarılır. Dolayısyla dizi göstericisi ile içerik operatörünü kullandığımızda bize ayrılan 10 int'lik alanın ilk elemanına erişmiş oluruz.

! stackalloc ile tahsis edilen bellek alanlarının ardışıl olması garanti altına alınmıştır.

stackalloc ile alan tahsisatı yapılır. Ancak alan tahsisatı yapılan bellek bölgesi ile ilgili hiçbir islem yapılmaz. Yani yukaridaki deyim ile, içinde tamamen rastgele değerlerin bulundugu 40 byte'lık bir alanımız olur. Bu alandaki değerlerin rastgele degerler olduğunu görmek için asagidaki programı yazın.

using System;

class Gosterici
{
  
 unsafe static void Main()
  
 {
  
    int * dizi = stackalloc int[10];

      for(int i=0; i<10;++i)
  
       Console.WriteLine("*(dizi+{0}) = {1}",i,dizi[i]); }
  
 }
}

! Yukarıdaki programda bir gösterici ile bellekteki ardışıl bölgelere indeksleyici operatörü ile nasıl eriştiğimize dikkat edin.

Programı /unsafe argümani ile beraber derleyip çalıştırdıktan sonra aşağıdaki ekran görüntüsünü elde etmeniz gerekir. Tabi bu değerler rastgele oldugu için sizdeki görüntü tamamen farklı olacaktır. Rastgele değerden kasıt çalışma zamanında Random gibi bir sınıfin kullanılıp rastgele bir sayı üretilmesi değildir. Burdaki sayılar daha önce çalışmiş olan programlardan kalan çöp değerlerdir.

C:\Programlar\StackAlloc
*(dizi + 0) = 0
*(dizi + 1) = 1244236
*(dizi + 2) = 1243328
*(dizi + 3) = 1350496
*(dizi + 4) = 124334
*(dizi + 5) = 1287174
*(dizi + 6) = 1243328
*(dizi + 7) = 0
*(dizi + 8) = 0
*(dizi + 9) = 1243404

C:\Programlar\StackAlloc

 

Console.WriteLine("*(dizi+{0}) = {1}",i,dizi[i]);

satırındaki

dizi[i]

yerine

*(dizi + i)

yazmamız herhangi birşeyi değiştirmezdi. Daha önce bu iki kullanımın eşdeğer olduğunu belirtmiştik.

Stack tabanli dizilerle ilgili bilinmesi gereken en önemli nokta dizinin sınırlarının aşılması ile ilgilidir. Daha önceden de denildiği gibi stack tabanli dizilerin sınırları aşıldığında herhangi bir uyarı verilmez. Elbetteki program başarıyla derlenir ancak çalımma zamanında bize ait olmayan bir adresin içeriğini degiştirmiş oluruz ki bu da bir programcının başina gelebilecek en tehlikeli durumdur. Örneğin aşağidaki programda stackalloc ile 10 int türünden nesnelik alan tahsis edilmektedir. Buna rağmen istediğimiz kadar alanı kullanabiliyoruz.

using System;

class Gosterici
{
  
 unsafe static void Main()
  
 {
  
     int * dizi = stackalloc int[10];

      for(int i=0; i<50;++i)
  
        *(dizi+i) = i;
  
 }
}

stackalloc ile oluşturacağımız dizilerin boyutu derleme zamanında bilinmek zorunda değildir. Örneğin çalışma zamanında kullanıcının belirlediği sayıda elemana sahip olan bir ardışıl bellek bölgesi aşagıdaki programda oldugu gibi tahsis edilebilir.

using System;

class Gosterici
{
  
 unsafe static void Main()
  
 {
  
    Console.Write("Dizi boyutu gir: ");
  
    uint boyut=0;

      try
  
    {
  
       boyut = Convert.ToUInt32(Console.ReadLine());
  
    }
  
    catch(FormatException e)
  
    {
  
       Console.WriteLine(e.Message);
  
    }

  
    int * dizi = stackalloc int[(int)10];

      for(int i=0; i<(int)boyut; ++i)
  
    {
  
       *(dizi+i) = i;
  
       Console.WriteLine(dizi[i]);
  
    }
  
 }
}

Bu program ile çalışma zamanında 7 elemanlı stack tabanlı bir dizinin oluşturulduğunu aşağıdaki ekran görüntüsünden görebilirsiniz.