Programlamanın temellerinden olan C++ üzerine başlattığımız makalelere 14. bölüm olan "İşaretçiler 2" ile devam ediyoruz. İlk bölüme buradan ulaşabilirsiniz. Tüm bölümlerimize ise buradan ulaşabilirsiniz.
İŞARETÇİ ARİTMETİĞİ
Normal değişkenlerle kullanılan operatörlerden bazıları, işaretçilerle de kullanılabilirler. Bu işaretçilerle bazı aritmetik işlemlerin yapılabileceği anlamına gelir. Bir işaretçinin gösterdiği adres (içeriği), bir tamsayı kadar arttırılabilir. Aşağıdaki örnek koda bakılırsa
long int dizi; long* l; l = &dizi[0];
dizinin ilk elemanının adresi l işaretçisine atanır. l işaretçisine atanan değer 2000 olsun. Eğer l işaretçisinin dizinin üçüncü elemanını göstermesi gerekiyorsa yapılması gereken işlem oldukça basittir.
l = l + 2;
Yukarıdaki kod satırı, l işaretçisinin gösterdiği adresin 2002 olmasını sağlamaz. Bu işlem herhangi basit bir toplamadan farklıdır ve birkaç adımda gerçekleşir.
- Derleyici + operatörünü görür, operatörün sağ tarafındaki elemanı ve sol tarafındaki elemanı kontrol eder ve bir toplama işlemi yapılacağını fark eder.
- Derleyici + operatörünün sol tarafını kontrol ederken l değişkeninin bir işaretçi değişken olduğunu fark etmiştir. l işaretçisinin tipini araştırır ve işaretçinin tipinin long int olduğunu fark eder. long int veri tipi standart bir veri tipidir ve 4 byte uzunluğunda olduğu derleyici tarafından bilinmektedir.
- Derleyici + operatörünün sağ tarafını kontrol ederken 2 ile karşılaşmıştır. Artık elindeki bilgileri birleştirerek ne tip bir işlem yapması gerektiğini bulabilir. İşlem şudur : 4 byte uzunluğundaki bir veri tipini gösteren bir işaretçiyi bu veriden iki veri sonrasını gösteren bir işaretçi haline getirmek. Bunun için l işaretçisi 8 ile ( 4 * 2 ) toplanmalıdır.
- En son olarak toplama işlemi yapılır ve bulunan değer l işaretçisine atanır. Böylece l işaretçisinin değeri 2008 olur.
Toplama işlemi sadece standart veri tipleri ile değil, kullanıcı tarafından tanımlanmış standart olmayan veri tipleri arasında da yapılabilir.
Tipleri ne olursa olsun iki tane işaretçi birbirleri ile toplanamaz, ancak herhangi bir işaretçiyle bir tamsayı toplanabilir.
Toplama işlemi eğer sürekli ise ve tek tek adımlarla olacaksa ++ operatörünü de kullanmak mümkündür. Bir işaretçinin değeri bir tamsayı kadar azaltılabilir. Çıkarmada da toplamadaki kurallar geçerlidir. İşlem yapılırken işaretçinin ait olduğu veri tipinin uzunluğuna dikkat edilir. Çıkarma işleminde toplama işleminden farklı bir durum söz konusudur. Aynı tipten olmak şartıyla iki işaretçi arasında çıkarma yapılabilir. Bir işaretçinin bir tamsayı ile yada başka bir işaretçi ile çarpılmasının ya da işaretçinin başka bir işaretçiye veya tamsayıya bölünmesinin bir anlamı yoktur. C ve C++ dillerinde herhangi bir işaretçi ile çarpma ve bölme işlemleri yapılamaz.
Aynı tipten olan işaretçiler birbirleri ile karşılaştırılabilirler. Aşağıdaki kod bloğu iki işaretçinin karşılaştırılmasını göstermektedir.
if ( p1 == p2 ) { cout << “İşaretçiler aynı veriyi gösteriyorlar” << endl; } else { cout << “İşaretçiler farklı verileri gösteriyorlar << endl; } İki işaretçi arasında == operatörü kullanılırsa, derleyici işaretçilerin içeriklerine bakar ve içerikleri karşılaştırır. Aynı şekilde !=, <, > karşılaştırma operatörleri ile de işlemler yapılabilir.
Aşağıdaki tablo işaretçiler ile yapılmasına izin verilen ve izin verilmeyen işlemleri göstermektedir.
İŞLEM | İZİN | Toplama | Evet | Çıkarma | Evet | Çarpma | Hayır | Bölme | Hayır | Arttırma (++) | Evet | Azaltma (--) | Evet | Karşılaştırma | Evet | Mantıksal işlemler | Hayır | Tip dönüşümleri | Evet | Atamalar | Evet |
İŞARETÇİLER ve DİZİLER ir dizinin herhangi bir elemanına dizinin ismi kullanılmadan işaretçilerle de erişim sağlanabilir. İşaretçi aritmetiği sayesinde, programcı dizinin içinde istenildiği gibi dolaşabilir. Aşağıdaki değişkenlere göre int x[10]; int *ix; int n; x 10 elemanı olan int tipinde bir dizi, ix int tipinde bir değişkeni gösteren bir işaretçidir. n ise indis olarak kullanılabilecek bir değişkendir. Bilindiği gibi &x[0]
ile x dizisinin ilk elemanının adresi (x dizisinin başlangıç adresi) elde edilir. Bu adres aşağıda gösterildiği gibi uygun bir işaretçiye atanır. ix = &x[0]; Bundan sonra n değişkeni indis olarak kullanarak yada ix işaretçisi üzerinden x dizisinin elemanlarına erişmek mümkündür. Aşağıdaki döngü for( n = 0; n < 10; n++ ) { *ix++ = n; } x dizisinin içini sırasıyla 0,1,2,4,5,6,7,8,9 sayıları ile doldurur. for( n = 0; n < 10; n++ ) { x[n] = n; } dizisi de aynı işi yapar. C/C++ dillerinde bir dizinin başlangıç adresini dizinin sıfırıncı elemanının adresi önüne adres operatörü konularak elde edilebilmek mümkün olduğu gibi başlangıç adresini sadece dizinin ismini kullanarak elde etmekte mümkündür. Yukarıdaki örnekler için &x[0] ifadesi ile x ifadesi aynı anlama gelmektedir. Dolayısıyla ix = &x[0]; ifadesi yerine ix = x;
ifadesini kullanmak mümkündür. #include <iostream.h> void KareAl(int *sayi) { *sayi *= *sayi; } void KareAlDizi(int *is1) { for (int i=0; i<10; i++) is1[i] *= is1[i];}int main() { int x = 50; int intis1[10]; KareAl(&x); cout<<"x = "<<x<<endl; KareAlDizi(intis1); cout<<intis1; return 0; }
İŞARETÇİ DEĞİŞKENLER ve İŞRETÇİ SABİTLER Önceki örnekte gösterildiği gibi hem ix hem de x işaretçidir. Fakat bu işaretçiler arasında önemli bir fark vardır. ix bir işaretçi değişkenidir, dolayısıyla bir çok int değişkenini gösterebilir ve ix işaretçisinin değeri değişebilir. Fakat dizi isimleri işaretçi sabitlerdir ve değiştirilemezler. ix işaretçisi sol değer olabilirken i işaretçisi asla sol değer olamaz.
ix = x; ix = ix + 2; ix++;
ifadeleri doğru iken, dizi isimleri sol değer olamadıkları için
x = ix; x = x + 2; x++;
ifadeleri kesinlikle yanlıştırlar.
Dizi isimleri ile işaretçi aritmetiği işlemlerini yapmak mümkündür.
DİZİ İSİMLERİ ile İŞARETÇİ ARİTMETİĞİ İŞLEMLERİ Dizi ismi x, x dizisinin ilk elemanı olan x[0] değişkenin adresi olduğu için
- x // Birinci dizi elemanının adresi
- x + 1 // İkinci dizi elemanının adresi
- x + 2 // Üçüncü dizi elemanının adresi
- .....
ifadeleri sırasıyla
&x[0] &x[1] &x[2] .....
ifadelerine denktirler.
x + n
ifadesi dizinin indisi n olan elemanına (n + 1) inci elemanına eşittir ve
&x[n]
İfadesi ile aynı anlama gelir. x + n ve &x[n] adresleri aynı oldukları için bu adreslerin gösterdikleri verinin de aynı olduğu anlaşılabilir. Bundan dolayı
x[n]
ifadesi
*(x + n)
ifadesi ile denktir.
Bu daha basit bir yolla da anlaşılabilir. Eğer x + n ve &x[n] ifadeleri birbirlerine denk ise, *(x + n ) ve *(&x[n]) ifadeleri de birbirlerine denk olmak zorundadırlar. *(&x[n]) ifadesi de x[n] ifadesi ile aynı anlama geldiği için x[n] ifadesi, *(x + n) ifadesine denktir. Zaten derleyiciler, programı derlerken x[n] ifadesini *(x + n) ifadesine çevirirler. İNDİSLİ İŞARETÇİLER Daha önce de birçok kez tekrarlanıldığı gibi, eğer ix = x; gibi bir ifade kullanılırsa ix işaretçisi, x dizisini ilk elemanının adresini alır. Dolayısıyla *(x + n) dizinin n-inci elemanı olur.
C ve C++ derleyicileri ix[n] şeklinde bir yazılış şeklini de kabul ederler. Buradan işaretçilerinde aynen diziler gibi indis kabul ettikleri anlaşılır (Zaten dizi isimleri de birer işaretçi sabitidir.). C ve C++ dillerinde, bir işaretçiyi indisler yardımı ile bir dizi gibi kullanmak mümkündür. Aşağıdaki örnek Program indisli işaretçileri kullanmaktadır.
#include <iostream.h> void main() { int* ix; int i = 10; int n = 5; ix = &i; *(ix + 5) = 3; cout << ix[0] << endl; cout << ix[n] << endl; }
Programın çıktısı
10 3
Örnek program yanıltıcı görüntüsünün aksine tamamen doğru çalışmaktadır. Ortada kesinlikle bir dizi olmamasına rağmen ix işaretçisi aynen bir dizi gibi kullanılmaktadır.
Fakat burada dikkat edilmesi gereken bir durum vardır. ix bir dizi değildir ve derleyici büyüklüğü belli olan bir dizi için bellekte yer ayırmamıştır. ix[n] veya *(ix + n), ix işaretçisinin gösterdiği adresten n kadar uzaktaki bir adresin içeriğini belirler ve bu adreste ne olduğu hiç kimse tarafından bilinemez.
Aslında yukarıdaki örneğin hatasız çalışması da tamamen tesadüftür. Bu tip işlemler yapan bir programcının ne yaptığından kesinlikle emin olması gerekir, hatalı bir adım sistem çökmesine kadar varabilen sonuçlar doğurabilir.
NEGATİF İNDİSLİ İŞARETİLER Dizilerin tersine işaretçiler negatif indis alabilirler. Aşağıdaki örnek, böyle bir yeteneğin ne işe yarayabileceğini göstermektedir.
#include <iostream.h> void main() { int x[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int* ix = &x[4]; cout << ix[-1] << endl; cout << ix[-2] << endl; cout << ix[-3] << endl; cout << ix[-4] << endl; }
Programın çıktısı
3 2 1 0
Yukarıdaki örnekte önce ix işaretçisine dizinin indisi 4 olan elemanını (beşinci elemanının) adresi atanmıştır. Daha sonra dizinin indisi 4’ten daha küçük olan elemanlarına erişmek gerekince işaretçiye negatif bir indis verilmiştir.
ix[0] = x[4]
olduğuna göre,
ix[-1] = x[3] ix[-2] = x[2] ix[-3] = x[1] ix[-4] = x[0]
olur. Negatif indislerle yapılabilecekler programcının üretkenliğine kalmıştır.
STRUCT TİPİNDEKİ VERİLER ve İŞARETÇİLER Struct tipindeki veriler aslında tek bir işaretçi tarafından gösterilirler fakat bu işaretçinin (aynen struct veri tipinde olduğu gibi) kendi elemanları vardır. Bu elemanlara erişim -> operatörü ile sağlanır. Aşağıda konu ile ilgili örnek verilmiştir.
struct kompleks { float reel; float imajiner; } kompleks sanal; kompleks *sx; Yukarıdaki örnekte kompleks adında bir yapı, sanal adında kompleks yapısında bir değişken ve sx adında kompleks yapısındaki değişkenleri gösteren bir işaretçi tanımlanmıştır.
sx = &sanal
İfadesi ile aynen standart bir veri tipinde olduğu gibi, sx işaretçisi, sanal değişkeninin adresini alır. (Burada struct tipindeki veri yapıları ile diziler arasında bir fark söz konusudur. Dizi isimleri zaten birer sabit işaretçi oldukları için adres operatörüne ihtiyaç duymazlar. Fakat struct veri yapıları isimlerinin böyle bir özelliği yoktur.) Bu şekilde struct veri yapılarına erişim sağlanabilir.
Struct veri yapılarının elemanlarına ise iki şekilde erişim sağlanabilir.
ğer sx işaretçisi sanal değişkeninin adresini içeriyorsa *sx ifadesi sanal değişkeninin içeriği ile denktir. Dolayısıyla
*sx.imajiner
ifadesi
sanal.imajiner
ifadesi ile denk olmak zorundadır.
Fakat bir problem ortaya çıkar. Nokta ( . ) operatörünün * operatörüne göre önceliği vardır. (Aynen matematikte çarpmanın toplamaya göre önceliği olduğu gibi.) Bu yüzden *sx.imajiner ifadesi derleyici tarafından *(sx.imajiner) ifadesine çevrilir. *(sx.imajiner) gibi bir ifade ise hatalıdır, çünkü eğer operatörünün sol tarafı bir işaretçi ise mutlaka * operatörü ile birlikte kullanılmalıdır. *(sx.imajiner) ifadesinde ise parantez sx ile * operatörünün bağlantısını kesmiştir. Bu hata yüzünden başka bir yazılış şekli kullanılır.
(*sx).imajiner şeklinde bir ifade kullanılırsa bu hata ortadan kalkar. Bu ifadeden başka bir yol daha vardır. -> operatörü de kullanılabilir.
(*sx).imajiner
ifadesi
sx->imajiner
ifadesi ile eşittir.
Eğer şimdiye kadar anlatılanların çok karışık olduğunu düşünen okuyucular sadece en son ifadeyi kullanabilirler. Aşağıdaki örnek işaretçilerin struct veri yapıları ile kullanılmasını göstermektedir.
#include <iostream.h> struct kompleks { float reel; float imajiner; }; void main() { kompleks sanal; kompleks* sx; sanal.reel = 10; sanal.imajiner = 5; sx = &sanal; cout << "Koplekes sayi : " << sanal.imajiner << " + " << sanal.reel << "i" << endl; cout << endl; cout << "Sayinin reel kismi" << endl; cout << sx->reel << endl; cout << "Sayinin imajiner kismi" << endl; cout << sx->imajiner << endl; }
Programın çıktısı
Kompleks sayı : 10 + 5i Sayının reel kısmı
10
Sayının imajiner kısmı
5
Yukarıdaki örnek program incelendiğinde sanal.reel ifadesinin, sx->reel ifadesi ile aynı olduğu görülebilir. Faruk Alkaya Selçuk Üniversitesi - Teknik Eğitim Fakültesi
|
C++ Dersleri: İşaretçiler 2 Yazar Okuyucu açık 2006-11-14 16:03:32 Güzel. |
Powered by AkoComment 2.0! |