|
C#'ta Gösterici(Pointer) Kullanmak - III |
|
Gönderiliyor lütfen bekleyin... |
|
|
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
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ı
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
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
ifadesini
ş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
yerine
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.
Yapı(Struct)
Türünden Göstericiler
int,char,double
gibi veri türleri aslında birer yapıdır. Bu konun başında bütün değer tipleri
ile gösterici tanımlayabileceğimizi söylemiştik. C# temel veri türlerinin yanısıra
kendi bildirdiğimiz yapılar da değer türündendir. O halde bir yapı göstericisi
tanımlayabilmemiz doğal bir durumdur. Yapı göstericilerinin tanımlanması temel
veri türlerinden gösterici tanımlama ile aynıdır. Ordaki kuralların tamamı yapılar
içinde geçerlidir. Fakat yapı göstericisi tanımlamanın bir şartı vardır, oda
yapının üye elemanlarının tamamının değer tipi olma zorunluluğudur. Örneğin
aşağıdaki yapı göstericisi tanımlaması geçersizdir.
using
System;
struct
Yapi
{
int x;
char c;
string s;
public
Yapi(int x,char
c, string str)
{
this.x
= x;
this.c
= c;
this.s
= str;
}
}
class
StackAlloc
{
unsafe static void Main()
{
Yapi
yapi = new Yapi(2,'a',"Deneme");
Yapi*
pYapi = &yapi;
}
}
|
Yukarıda 'Yapi'
türünden göstericinin tanımlanamamasının sebebi yapının yönetilen türden(managed
type) bir üye elemanının bulunmasıdır. Bu üye elemanı da doğal olaral string
türüdür. Yapı bildiriminden string türünü çıkarıp aşağıdaki gibi ilgili değişiklikleri
yaptığımızda 'Yapi' türünden göstericileri tanımlayabiliriz.
using
System;
struct
Yapi
{
int x;
char c;
public
Yapi(int x,char
c)
{
this.x
= x;
this.c
= c;
}
}
class
StackAlloc
{
unsafe static void Main()
{
Yapi
yapi = new Yapi(2,'a');
Yapi*
pYapi = &yapi;
}
}
|
Yapı göstericileri
üzerinden yapı göstericisinin adresine ilişkin nesnelerin elemanlarına özel
bir operatör olan -> operatörü ile erişebiliriz. Örneğin yukarıdaki
programın Main() metodunu asağıdaki gibi değiştirdiğinizde ekrana yapı nesnesinin
x ve c elemanları yazdırılacaktır. Tabi Yapi'nın üye alamanlarını public
olarak değiştirmeniz gerekecektir. Çünkü ok operatörü ilede olsa ancak public
olan elemanlara ulaşabiliriz.
unsafe
static void Main()
{
Yapi yapi = new Yapi(2,'a');
Yapi*
pYapi = &yapi;
Console.WriteLine("yapi.x=
" + pYapi->x);
Console.WriteLine("yapi.c= " + pYapi->c);
}
|
Not:
' -> ' operatörüne ok operatörü de denilmektedir.
Yapının public
olan elemanlarına ok operatörü yerine yapı nesnesinin içeriğini * operatörü
ile elde edip nokta operatörü ile de ulaşabiliriz. Buna göre yukarıdaki Main()
metodu ile asagidaki Main() metodu eşdeğerdir.
unsafe
static void Main()
{
Yapi
yapi = new Yapi(2,'a');
Yapi*
pYapi = &yapi;
Console.WriteLine("yapi.x=
" + (*pYapi).x);
Console.WriteLine("yapi.c= " + (*pYapi).c);
}
|
Her iki Main()
metodunun ürettiği çıktı aynıdır.
Göstericilerin
en çok kullanıldığı diğer bir uygulama alanı da karakter işlemleridir. Bir yazıyı
karekter dizisi olarak temsil edip yazılar ile ilgili işlemler yapılabilir.
C#' taki string türünün altında geçekleşen olaylarda zaten bundan ibarettir.
Karakter dizileri ile ilgili en önemli nokta bir yazıyı char türden bir göstericiye
atayabilmemizdir. Karekter dizileri olan stringlerdeki her bir karakter bellekte
ardışıl bulunmaktadır. Dolayısıyla yazıdaki ilk karakterin adresini bildiğimizde
yazıdaki bütün karakterlere erişebiliriz. C#'taki string türü yönetilen
tip(managed type) oldugu için char türden bir göstericiye bir yazının ilk karekterinin
adresini atamak için fixed anahtar sözcüğünü kullanmalıyız. Aşağıdaki programda
bir yazının char türden göstericiye nasıl atandığını ve bu gösterici ile yazıdaki
her karaktere ne şekilde erişildiğini görüyorsunuz.
using
System;
class
KarakterDizisi
{
unsafe
static void Main()
{
fixed(char*
ptr = "Sefer Algan")
{
for(int
i=0; ptr[i] != '\0'; ++i)
Console.Write(ptr[i]);
}
}
}
|
Buradaki en önemli
nokta ptr[i]' nin '\0' karakteri ile karşılaştırıldığı yerdir. Göstericiler
ile bellekte char türünün büyüklügü kadar ilerlerken yazının nerede sonlandıgını
bilemeyiz. Bunun için
char* ptr = "Sefer
Algan"
deyimi ile belleğe
yerlestirilen 'n' karakterinden sonra null değerini ifade eden '\0' karekter
yerleştirilir. Bu karektere rastlanıldığı zaman yazının sonuna gelmiş bulunuyoruz.
Not: Göstericilerle
ilgili yayınlanan bu makale dizisi yazmış olduğum ve yakında Pusula yayıncılıktan
çıkacak olan C# kitabından alınmıştır.
Makale:
C#'ta Gösterici(Pointer) Kullanmak - III C#, Visual C# ve .NET Sefer Algan
|
|
|
-
-
Eklenen Son 10
-
Bu Konuda Geçmiş 10
Bu Konuda Yazılmış Yazılmış 10 Makale Yükleniyor
Son Eklenen 10 Makale Yükleniyor
Bu Konuda Yazılmış Geçmiş Makaleler Yükleniyor
|
|