C Sharp Programlama Dili/Göstericiler
Göstericiler (pointer) yapı nesnelerinin bellekteki adreslerini tutan bir veri tipidir. Temel veri türlerinden byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal, char ve bool birer yapıdır. (string ve object ise birer sınıftır.) Bu yapıların yanında .Net Framework kütüphanesindeki diğer yapılar ve kendi oluşturduğumuz yapı nesneleriyle de göstericileri kullanabiliriz. Ancak C# normal yollarla gösterici kullanılmasına izin vermez. Programımızın herhangi bir yerinde gösterici kullanabilmek için o yeri unsafe
anahtar sözcüğüyle belirtmemiz gerekir. unsafe anahtar sözcüğünü şu şekillerde kullanabiliriz:
- Bir sınıfı unsafe olarak belirtirsek o sınıf içinde gösterici kullanabiliriz.
unsafe class Sinif
{
...
}
- Herhangi bir metodun içinde bir unsafe bloğu oluşturarak o bloğun içinde gösterici kullanabiliriz.
int Metot()
{
unsafe
{
...
}
}
- Normal bir metodu unsafe olarak belirterek o metot içinde gösterici kullanabiliriz.
unsafe int Metot()
{
...
}
- Özellikleri unsafe olarak belirterek yeni bir gösterici oluşturabiliriz. Bu durumda sınıfı unsafe olarak belirtmeye gerek kalmaz. Ancak bir metot içinde bir gösterici oluşturabilmek için ya o metodun unsafe olarak bildirilmesi ya da bir unsafe bloğunun açılması gerekir. Ayrıca içinde gösterici kullandığımız bir kaynak kod dosyası normal yollarla derlenemez. İçinde gösterici kullandığımız bir kaynak kod dosyamızı aşağıdaki iki yoldan biriyle derleriz.
csc /unsafe KaynakKod.cs
veya
csc -unsafe KaynakKod.cs
Eğer programlarımızı yazmak için Visual Studio programlama ortamını kullanıyorsak derleyicinin unsafe
olarak işaretlenmiş kod bloklarına izin vermesi için "Solution explorer" penceresindeki projemiz sağ tıklanır. "Properties" seçilir. Açılan penceredeki "Build" sekmesi tıklanır. Açılan sekmedeki "Allow unsafe code" onay kutusu seçilir. Son olarak ayarlarda yaptığımız değişiklikler kaydedilir.
Gösterici bildirimi
değiştirGösterici bildirimi ilgili yapı adının sonuna * işareti koyularak yapılır. Örnekler:
char* gosterici1;
int* gosterici2;
Bu göstericilerden birincisi bir char türünden nesnenin (değişkenin), ikincisi de bir int türünden nesnenin adresini tutabilir. Birden fazla aynı türde gösterici oluşturmak için normal veri türlerinde olduğu gibi virgül kullanılabilir. Örnek:
int* gosterici1, gosterici2;
Göstericileri sabit ifadeler ile de bildirebiliriz. Örnek:
double* gosterici=(double*)123456;
Burada bellekteki 123456 adresi gosterici göstericisine atanıyor. Ancak biz 123456 adresinde ne olduğunu bilmediğimiz için bu tür bir kullanım son derece tehlikelidir.
& operatörü
değiştir& operatörü yapı nesnelerinin bellekteki adreslerini üretir. Operandı hangi türdeyse o türün gösterici tipinde bir değer döndürür. Örnek:
int a=5;
int* gosterici;
gosterici=&a;
* operatörü
değiştir* operatörü adreslerdeki veriyi görmek veya değiştirmek için kullanılır. * operatörü hangi tür göstericiyle kullanıldıysa o türde nesne geri döndürülür. Örnek:
double a=5;
double* gosterici;
gosterici=&a;
*gosterici=10;
Yani kısaca & ve * operatörleri gösterici ve nesne arasında dönüşüm yapmak için kullanılır.
NOTLAR:
1-) Henüz adresi olmayan bir göstericiye değer atanamaz. Örneğin şu kod hatalıdır:
int* gosterici;
*gosterici=10;
2-) Ancak henüz bir değeri olmayan bir nesnenin adresi alınabilir. Yani şu kod hata vermez:
int a;
int* ptr=&a;
Göstericiler arasında tür dönüşümü
değiştirGöstericiler arasında bilinçli tür dönüşümü yapılabilir. Ancak dikkatli olunmalıdır. Çünkü büyük türün küçük türe dönüşümünde veri kaybı yaşanabilir. Küçük türün büyük türe dönüşümünde de küçük tür bellekte kendine ait olmayan alanlardan da alır. Dolayısıyla her iki durumda da istemediğimiz durumlar oluşabilir. Göstericiler arasında bilinçsiz tür dönüşümü ise imkansızdır. Örnek bir program:
using System;
class Gostericiler
{
unsafe static void Main()
{
char c='A';
int i=80;
char* cg=&c;
int* ig=&i;
cg=(char*)ig;
Console.WriteLine(*cg);
}
}
Bu program sonucunda ekrana P yazılacaktır. (80'in Unicode karşılığı P'dir.) Bu programda herhangi bir veri kaybı gerçekleşmedi. Çünkü 80 char türünün kapasitesindeydi.
Dersin en başında söylemiştik: Göstericiler yapı nesnelerinin bellekteki adreslerini tutarlar. İşte bu adresi elde etmek için ilgili gösterici bir tam sayı türüne dönüştürülmelidir. Örnek:
using System;
class Gostericiler
{
unsafe static void Main()
{
int i=80;
int* ig=&i;
uint adres=(uint)ig;
Console.WriteLine("{0:X}",adres);
}
}
Bu program sonucunda benim bilgisayarım 13F478 çıktısını verdi. Ancak bu sizde değişebilir. Çünkü işlemcinin verileri nereye koyacağını hiç birimiz bilemeyiz. Ayrıca adresi ekrana yazdırırken formatlama yapıp 16'lık düzende yazdırdığımın da farkında olmalısınız. Çünkü bellek adresleri genellikle 16'lık düzendeki sayılarla ifade edilir.
Göstericilerin adreslerini elde ederken uint türüne dönüşüm yapmak zorunlu değildir. İstediğiniz tam sayı türüne dönüşüm yapabilirsiniz. Ancak uint, bir bellek adresini tam olarak tutabilecek en küçük veri tipi olduğu için ilgili göstericiyi uint türüne dönüştürmeniz tavsiye edilir.
Göstericilerdeki adres bilgisini uint veya daha büyük bir tamsayı türüne dönüşüm yapmadan direkt göremeyiz. Örneğin aşağıdaki kullanım hata verir:
int i=80;
int* ig=&i;
Console.WriteLine(ig);
void göstericiler
değiştirvoid tipinden göstericilere her türden adres atanabilir. void türü nesnelerdeki object türüne benzer. Örnek kod
int* gosterici1;
void* gosterici2;
gosterici2=gosterici1;
sizeof operatörü
değiştirsizeof operatörü yapıların bellekte ne kadar yer kapladıklarını bayt türünden verir. Geri dönüş tipi inttir. Örnek kod:
Console.WriteLine(sizeof(int));
Bu kod ekrana 4 çıktısını verir. İsterseniz bir de şöyle bir örnek yapalım:
using System;
class Gostericiler
{
struct yapi
{
public int Ozellik1;
public int Ozellik2;
}
unsafe static void Main()
{
Console.WriteLine(sizeof(yapi));
}
}
Bu program 8 çıktısını verir.
Gösterici aritmetiği
değiştirGöstericilerin bir adres kısmı, bir de veri kısmı olmak üzere iki kısmı bulunur. Adres kısmı tüm gösterici tiplerinde 4 bayt yer kaplar ancak veri kısmının kapladığı alan türe göre değişir. Örneğin tür int* ise adres kısmı 4 bayt, veri kısmı 4 bayt olmak üzere gösterici toplam olarak 8 bayt yer kaplar. Herhangi bir göstericinin adres kısmıyla ilgili matematiksel işlem yapmaya gösterici aritmetiği denir. Göstericilerle matematiksel işlem yapmak göstericinin türüne bağlıdır. int* türünden bir göstericiyi 1 ile toplarsak o göstericinin adres kısmı 4 ile toplanır. Yani herhangi bir göstericiyi herhangi bir tam sayı ile toplarsak ya da herhangi bir tam sayıyı herhangi bir göstericiden çıkarırsak ilgili tam sayı değil, ilgili tam sayının göstericinin türünün bayt cinsinden boyutu ile çarpımı işleme sokulur. Örnek:
using System;
class Gosterici
{
unsafe static void Main()
{
int* g1=(int*)500;
char* g2=(char*)500;
double* g3=(double*)500;
byte* g4=(byte*)500;
g1+=2;
g2+=5;
g3+=1;
g4+=6;
Console.WriteLine("{0} {1} {2} {3}",(uint)g1,(uint)g2,(uint)g3,(uint)g4);
}
}
Bu programın ekran çıktısı 508 510 508 506
olmalıdır. Göstericiler yalnızca tam sayılarla matematiksel işleme girerler. +, -, --, ++, -= ve += operatörleri göstericilerle kullanılabilir. Bir göstericiyi bu operatörler ve tamsayılarla işleme sokarsak yine bu gösterici tipinden nesne oluşur. Örnek:
using System;
class Gosterici
{
unsafe static void Main()
{
int i=5;
int* ig=&i;
int* ig2=ig+2; //ig+2 yine int* türündendir.
}
}
void göstericiler matematiksel işleme girmezler.
İki gösterici birbirinden çıkarılabilir. Ancak bu durumda bir gösterici değil, long türünden bir nesne oluşur. Örnek:
using System;
class Gosterici
{
unsafe static void Main()
{
int* g1=(byte*)500;
int* g2=(byte*)508;
long fark=g2-g1;
Console.WriteLine(fark);
}
}
Bu program sonucunda ekrana 2 yazılır. Çünkü göstericilerde çıkarma yapılırken şu formül kullanılır:
(Birinci göstericinin adresi - İkinci göstericinin adresi)/Ortak türün byte cinsinden boyutu
Bu formülden de anlıyoruz ki yalnızca aynı türdeki göstericiler arasında çıkarma yapılabilir. Eğer bu formül sonucunda bir tam sayı oluşmuyorsa ondalıklı kısım atılır. Göstericilerle kullanılabilecek diğer operatörler ==, <, > gibi karşılaştırma operatörleridir. Bu operatörler iki göstericinin adres bilgilerini karşılaştırıp true veya false'tan uygun olanını üretirler.
fixed anahtar sözcüğü
değiştirGarbage Collection mekanizması sınıf nesnelerinin adreslerini program boyunca her an değiştirebilir. Dolayısıyla bu sınıf nesnesi üzerinden erişilen ilgili sınıfa ait yapı türünden olan özelliklerin de adresleri değişecektir. Ancak istersek bir sınıf nesnesi üzerinden erişilen bir yapı nesnesinin bir blok boyunca değişmemesini isteyebiliriz. Bunu fixed anahtar sözcüğü ile yaparız. Şimdi klasik örneği yapalım (fixed'sız):
using System;
class ManagedType
{
public int x;
public ManagedType(int x)
{
this.x=x;
}
}
class Gosterici
{
unsafe static void Main()
{
ManagedType mt=new ManagedType(5);
int* g=&(mt.x)
}
}
Bu programın derlenmesine C# izin vermeyecektir. Çünkü x bir sınıf nesnesidir ve sınıf nesnelerinin adresleri program boyunca değişebilir. Biz burada bu sınıf nesnesine ait olan bir özelliğin (yapı türünden) adresini aldık. Ancak muhtemelen program esnasında nesnemizin adresi değişecek ve bu adrese başka veriler gelecek. İşte karışıklığı engellemek için C# bu programın derlenmesini engelledi. Şimdi örneğimizi şöyle değiştirelim:
using System;
class ManagedType
{
public int x;
public ManagedType(int x)
{
this.x=x;
}
}
class Gosterici
{
unsafe static void Main()
{
ManagedType mt=new ManagedType(5);
fixed(int* g=&(mt.x))
{
//Bu blok boyunca x özelliğinin adresi değiştirilmeyecektir.
}
}
}
Birden fazla özelliği fixed yapmak için:
ManagedType mt1=new ManagedType(5);
ManagedType mt2=new ManagedType(10);
fixed(int* g1=&(mt1.x))
fixed(int* g2=&(mt2.x))
{
//Bu blok boyunca x özelliğinin adresi değiştirilmeyecektir.
}
Aynı şeyi şöyle de yapabilirdik:
ManagedType mt1=new ManagedType(5);
ManagedType mt2=new ManagedType(10);
fixed(int* g1=&(mt1.x), g2=&(mt2.x))
{
//Bu blok boyunca x özelliğinin adresi değiştirilmeyecektir.
}
NOT: C#'ta tipler yönetilen tipler (managed type) ve yönetilmeyen tipler (unmanaged type) olmak üzere ikiye ayrılır. Yönetilen tip nesnelerinin bellekteki adresi nesnenin hayatı boyunca değişebilir, yönetilmeyen tip nesnelerinin ise bellekteki adresi nesnenin hayatı boyunca sabit kalır. Referans tipleri yönetilen tiplerdir (örneğin sınıflar), değer tipleri ise yönetilmeyen tiplerdir (örneğin yapılar).
Göstericiler ile dizi işlemleri
değiştirDiziler bir System.Array sınıfı türünden nesnedir. Dolayısıyla diziler bir managed type'dır. Yani dizi elemanlarının adreslerini fixed anahtar sözcüğünü kullanmadan elde edemeyiz. Çok boyutlu diziler de dâhil olmak üzere tüm dizilerin elemanları bellekte ardışıl sıralanır. Bu hem performansta az da olsa bir artış hem de az sonra dizi elemanlarının bellekteki adreslerini alırken kolaylık sağlar. Örnek program:
using System;
class Gosterici
{
unsafe static void Main()
{
int[] a={1,2,3,4};
fixed(int* g=&a[0])
{
for(int i=0;i<a.Length;i++)
Console.WriteLine(*(g+i));
}
}
}
Bu program dizinin elemanlarını alt alta yazacaktır. Programda öncelikle dizinin ilk elemanının bellekteki adresini alıp g göstericisine atadık. Sonra a dizisinin eleman sayısı kadar dönen bir döngü başlattık. Döngünün her iterasyonunda ilgili göstericiyi birer birer artırdık. Yani adres her seferinde birer birer ötelendi. Gösterici aritmetiğinde de gördüğümüz gibi aslında adresler 4'er bayt ötelendi. Eğer gösterici aritmetiğinde öteleme türe bağımlı olmayıp teker teker olsaydı döngüde her dizinin türüne göre farklı atlayış miktarı belirlememiz gerekecekti ve bu da oldukça zahmetli olacaktı.
Göstericilerin indeksleyici ile kullanılması
değiştirGöstericiler ile indeksleyici kullanılabilir. Bir göstericinin bir tamsayı ile indekslenmesi durumunda ilgili göstericinin gösterdiği adrese (gösterici tipinin boyutu*indeksteki tamsayı) kadar ekleme yapılıp sonuçta bir gösterici değil ilgili adresteki verinin ilgili yapı tipindeki karşılığı üretilir. Örnek:
int i=5;
int* ig=&i;
int j=ig[1];
Bir diziyle aynı dizinin ilk elemanının adresi birbiriyle aynıdır. Bunu ispatlamak için şu programı yazabiliriz:
using System;
class Gosterici
{
unsafe static void Main()
{
int[] a={1,2,3,4};
fixed(int* g1=a, g2=&a[0])
{
Console.WriteLine((uint)g1);
Console.WriteLine((uint)g2);
}
}
}
Gördüğünüz gibi bir diziyi bir göstericiye aktarmak için & operatörünü kullanmamıza gerek kalmadı. Çünkü aslında bütün diziler aynı zamanda bir göstericidir. Bu program sonucunda ekrana iki tane birbiriyle aynı adres yazılacaktır.
Yönetilmeyen diziler
değiştirŞimdiye kadar gördüğümüz diziler System.Array sınıfı türünden birer nesne olduklarına göre bunlar yönetilen dizilerdir. Çünkü sınıflar yönetilen bir türdür. Yapılar, enum sabitleri, vs. ise yönetilmeyen türlerdir. Yönetilmeyen türlerle ilgili işlem yapmak daha hızlıdır. Çünkü yönetilmeyen türlerde C# bellek sorumluluğunu bize verir. Hatırlarsanız C# bir dizinin eleman sayısı aşıldığında çalışma zamanında IndexOutOfRangeException istisnai durumunu veriyordu. Yani dizimizin kendine ait olmayan bir bellek bölgesine müdahale etmesini engelliyordu. Bu çoğu durumda son derece güzel bir özelliktir. Ancak istersek C#'ın böyle bir durumda çalışma hatası vermemesini sağlayabiliriz. Bu yönetilmeyen dizilerle mümkün olur. Yönetilmeyen diziler yönetilen dizilere oranla daha hızlı çalışır. Yönetilmeyen diziler stackalloc anahtar sözcüğüyle belirtilir. stackalloc bize bellekte istediğimiz kadar alan tahsis eder ve bu alanın başlangıç adresini bir gösterici olarak döndürür. Örnek:
int* dizi=stackalloc int[10];
Gördüğünüz gibi new anahtar sözcüğü yerine stackalloc anahtar sözcüğü gelmiş. Bu komut ile bellekte kendimize 10*sizeof(int)=40 baytlık alan ayırdık. Örnek program:
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]);
}
}
Şimdi başka bir örnek:
using System;
class Gosterici
{
unsafe static void Main()
{
int* dizi=stackalloc int[10];
for(int i=0;i<50;i++)
Console.WriteLine("*(dizi+{0})={1}",i,dizi[i]);
}
}
Bu programda dizinin sınırları aşılmış olmasına rağmen program çalışmaya devam eder. Ancak başka programların bellekteki verilerine de müdahale etmiş oluruz. Bir programcı için pek tavsiye edilmeyen bir durumdur. Ayrıca normal dizilerde olduğu gibi stackalloc bir dizinin eleman sayısının derlenme zamanı bilinmesi zorunlu değildir. İstersek dizinin eleman sayısını kullanıcı belirleyebilir.
Yapı türünden göstericiler
değiştirŞimdiye kadar int, double gibi temel veri türleri tipinden göstericiler oluşturduk. Bölümün en başında da söylediğimiz gibi bütün yapılardan gösterici oluşturulabilir. Ancak bir şart vardır: İlgili yapının içinde geri dönüş tipi bir sınıf olan özellik olmamalıdır. Geri dönüş tipi bir sınıf olan metot ya da sahte özellik olabilir. Buradan anlıyoruz ki int, double vb. yapıların içinde geri dönüş tipi bir sınıf olan özellik yokmuş.
-> operatörü
değiştir-> operatörü bir gösterici üzerinden ilgili yapının üye elemanlarına erişmek için kullanılır. Şimdi yukarıdaki bilgilerle bu bilgiyi bir örnek üzerinde görelim:
using System;
struct Yapi
{
//string k; ---> Bu kod eklenseydi program derlenemezdi.
public int x;
int s;
public Yapi(int x, int s)
{
this.x=x;
this.s=s;
}
public string Metot()
{
return "Deneme";
}
public string Ozellik
{
set{}
get{return "Deneme";}
}
}
class Prg
{
unsafe static void Main()
{
Yapi a=new Yapi(2,5);
Yapi* b=&a;
Console.WriteLine(b->Metot()); //Gördüğünüz gibi göstericiler üzerinden ilgili yapının üye elemanlarına erişebiliyoruz.
Console.WriteLine(b->Ozellik);
Console.WriteLine(b->x);
}
}
Bildiğiniz üzere nesneler üzerinden yapıların üye elemanlarına erişmek için . operatörünü kullanıyorduk. Yani -> ile . operatörleri bir bakıma benzeşiyorlar. Göstericiler ile yapının üye elemanlarına erişebileceğimiz gibi göstericiyi * operatörü ile nesneye çevirip . operatörüyle de erişebiliriz. Örnek:
Yapi a=new Yapi(2,5);
Yapi* b=&a;
Console.WriteLine((*b).Metot());
Console.WriteLine((*b).Ozellik);
Console.WriteLine((*b).x);
Göstericiler ve string türü
değiştirStringlerin göstericiler için özel bir anlamı vardır. Char türünden herhangi bir göstericiye bir string atanabilir. Bu durumda göstericiye stringin ilk karakterinin bellekteki adresi atanır. Stringlerin karakterleri tıpkı diziler gibi bellekte ardışıl sıralanır. Dolayısıyla ilk karakterinin adresini bildiğimiz bir stringin tüm karakterlerini elde edebiliriz. Örnek:
using System;
class Stringler
{
unsafe static void Main()
{
fixed(char* g="Vikikitap")
{
for(int i=0;g[i]!='\0';i++)
Console.WriteLine(g[i]);
}
}
}
Gördüğünüz gibi string bir sınıf olduğu için fixed anahtar sözcüğü kullanıldı. Ayrıca programdaki for(int i=0;g[i]!='\0';i++)
satırı dikkatinizi çekmiş olmalı. Buradaki '\0' karakteri stringin sonuna gizlice eklenenen bir karakterdir. Biz bu karakterden stringin bittiğini anlıyoruz.