C/C++ Ders Kitabı

Transkript

C/C++ Ders Kitabı
ÖNSÖZ
Bu kitabın yazılma amacı; C/C++ ve NYP ile, işletim sistemlerine bağlı
kalmadan her konuda ve her boyutta program geliştirebilmektir. Bugün
300 e yakın programlama dili vardır. Bunların hiçbiri C/C++ kadar
yaygınlık kazanamadı. Bütün zorluklarına karşın C/C++ sistemlerin temel
dilidir. Derleyicili dil olan C/C++ ile gerçeklenemeyecek hiçbir yazılım
projesi yoktur. Yeterki siz büyük projeleri gerçekleştirebilecek bilgi
birikimine sahip olun.
Her C++ derleyicisinin C dili ile yazılmış kodları da derleyebilmesi, NYP
dil olan C++ ya, C diline ait kodları kullanma olanağını verir. Bu yüzden
C++ dili çok güçlüdür.
Günümüzde merkezi işlem birimlerinde ortaya çıkan gelişmeler, yazılım
teknolojilerinde yeni uygulamalara gebedir. Özellikle yarıiletken
teknolojilerinde saat hız sınırına gelindiğine dair yaygın bir kanaat vardır.
Nesne yönelimli olan C++ dili önümüzdeki dönemde de koşut denetim
akışlı (multithreaded) çözümlerde kullanılabilir. Bazı derleyicilerin
sağladığı olanaklarla şu an bile eş anlı (concurrent) C++ programları
yazılmaktadır.
Aslında iyileştirme süreci ile ilintili olan koşut denetim akışı, son
dönemlerde yarıiletken üretim teknolojilerindeki gelişmeler (birim alana
sığdırılan transistör sayısı giderek artmakta) ve bunların saat hız sınırına
gelinmiş olması nedeni ile önümüzdeki yıllarda yazılım teknolojisinin
yıldızı olacaktır. Programlarda sorunlar, eş anlı çalışan çözüm birimleri
tarafından, aynı gövdede ki birden fazla işlemci çekirdeği tarafından
çözülecektir. Gelecekte yazılım teknolojisinde değişim bu doğrultudadır.
C/C++ bu konuda da önemli aktörlerdendir.
Ülkelerin gelecekle ilgili planlarında yer alması gereken, bilgisayar,
nükleer ve gen teknolojilerinden, bilgisayarların temel yakıtı yazılımdır.
Hatta yazılımları, bir bilgisayar donanımı gibi düşünmekte olasıdır. İşte bu
nedenle C/C++ dili gibi çok güçlü diller, enerji gibi ekonomik ve stratejik
değeri üst düzeyde olan ürünlerin üretilmesini sağlarlar.
Umarız okuduğunuz bu kitap yararlı ürünler geliştirmenize yol açar.
Selami KOÇLU
6-mart-2005
karşıyaka-izmir
1. BÖLÜM
Giriş
C++ yazılım dili, insanların kullandığı diller gibi, düşünceleri açıklamanın
aracıdır. İnsanlar kendi aralarında anlaşabilmek amacı ile çok sayıda dil
geliştirmişlerdir. Dilleri çevrelerinden öğrendikleri biçimde, gerektiği
zaman gerektiği yerlerde kullanırlar. Yazılım dilleri ise, insanın bilgisayar
denilen makine ile iletişim kurabilmesini ve bilgisayarı denetlemesini
sağlar. Çok sayıda yazılım dili bulunmaktadır. C++ dili bunlardan biridir.
Sorunların boyutu ve karmaşıklığı arttıkça, öteki dillere göre esnek ve basit
oluşu, bu çözüm ortamını daha başarılı kılar.
C++ diline sadece birtakım özellikler toplamı olarak bakılamaz, zira
bunların bazıları ortamlarından ayrılırsa anlam taşımazlar. Sadece kodlama
değil tasarım düşünülüyorsa, parçaların toplamını gözönünde tutarak
çözüm üretilir. Bu yolla C++ dilini anlayabilmek için önce sorunları,
mutlaka C dili ile anlamalı, ve genel olarak programlamayı bilmek
gerekmektedir. Bu kitap programlamanın sorunlarını, bunların neden sorun
olduğunu ve C++ ile bunların nasıl ortadan kaldırılacağını inceleyecektir.
Bunun için her bölümde bir problem, C++ özellikleri kullanılarak
çözülecektir. Bu yöntemin, C dilinden C++ dilini, anadiliniz yapana kadar
ilerlemenizi sağlayacağını umuyoruz. Önce C++ dili ilgili olarak kafanızda
bir model inşa edilmesi için çalışacağız. Böylece, bir sorun ile
karşılaştığınızda bu modelden yararlanarak, C++ ile çözüm üretebilirsiniz.
Burada amacımız, C++ diliyle düşünebilme yeteneği kazanmaktır.
Planımız C/C++ yı, 3 ciltte bütün ayrıntıları ile tam tekmil anlatmaktır.
Birinci cilt temel C/C++ özelliklerini, ikinci cilt standart kütüphaneleri ve
üçüncü cilt koşut denetim akış (multithreaded) tekniklerini anlatacaktır.
Böylece C/C++ ile yazamayacağınız program kalmayacaktır. Günümüzde
çok çekirdekli işlemcilerin ortaya çıkması, aslında tek işlemcili yapılara
göre tasarlanmış C++ dilinin esnekliğini kullanmayı gerektirmektir. Bu
yüzden üçüncü cilt, C++ eş anlı programlamanın el kitabı hüviyetinde
olacaktır.
Nesneler
Bilgisayar devriminin doğuşu makinelerde başlamıştı. Programlama
dillerinin ortaya çıkışı buna uyum sağlamıştır. Ama, bilgisayarlar
makinelerin çoğuna benzemez, daha ziyade akıl yükseltgecidirler. Fakat
hiç bir zaman akıl değildirler.
Apple bilgisayardan Steve Jobs'ın deyişiyle, bilgisayar aklın bisikletidir.
Tabii, o zamanlar 80 lerin başıydı.Yani
CPUlar daha 10-20 mhz hız sınırlarında dolanıyorlardı. Bugün 2003 lerde
3 ghz lerde, yani 25 yıl sonra epey hızlı bisikletler oldular. Aslında
bilgisayarlar insan aklını destekleyen farklı bir açıklama ortamıdır. Bu
araçlara bir makineden çok, aklın bir parçası olarak bakılmalıdır. Anlatı
ortamı olarak; yazı, resim, heykel yada film üretimini andırırlar. Nesne
yönelimli programlama, bilgisayarların anlatı ortamı olarak kullanımının
alt yapısıdır. Bu bölümde; nesne yönelimli programlamanın temel
fikirlerine giriş, ve NYP(nesne yönelimli programlama) geliştirme
yöntemlerine genel bir bakış verilecektir. Burada yeri gelmişken çok temel
bir konu olan değişken ve nesne tanımlarından bahsedelim; programa bir
girdi yerleştirilebilmesi bellekte onu koyacak yer olmasına bağlıdır,
bellekteki bu yer değişkendir, değişken aynı zamanda ad konmuş nesnedir.
Bundan şu anlaşılabilir; adı olmayan nesneler olabilir. Evet adı olmayan
nesne olabilir. Özellikle dinamik nesne oluşumlarında adsız nesnelere
rastlanır. Şimdi yine bir başka türkçe terim konusuna değinelim; biz
değişken terimini kitabımızda argüman, parametre ve variable yerine
kullandık. Bilindiği gibi argüman bir işlevin özelliklerini belgeleyen unsur,
parametre ise farklı değer alabilir anlamını taşımaktadır. Biz bu üç terim
yerine pek farklı olmayan değişken kelimesini kullanmayı uygun bulduk.
Nesne ve değişken tanımları, NYP (nesne yönelimli programlamaya) ye
göre açıklanmıştır belirtelim.
Soyutlama aşamaları
Programlama dillerinin tümünde soyutlama değişik seviyelerde bulunur. Karmaşık
bir sorunun çözülebilirliği, soyutlamanın uygunluğu ile doğrudan bağlantılıdır.
Soyutlamanın niteliği ve çeşidi, uygunluğu belirler. Çeşit, yapılan soyutlamanın ne
tip olduğudur. Assembly dili, temel makinenin küçük bir soyutlamasıydı.
Daha sonraki emredici diller (Fortran,Basic ve C) aslında Assembly'nin
soyutlanmasıyla ortaya çıkmışlardı. Bu diller Assembly 'e göre büyük gelişmeler
göstermelerine rağmen, temel soyutlama bilgisayar mimarisine dayanıyordu.
Nesne yönelimli dillerde ise, soyutlama çözülecek soruna bağlıdır. Programcı
emredici dillerde, makine modeli ile çözülecek sorunun modeli arasında, mutlaka
ilişki kurmak zorundaydı. Makine modelinden kastedilen; çözüm ortamı, yani
çözümün bulunduğu ortam, yani bilgisayar. Soru modeli ise, sorunun koşullarıdır.
Makine modeli ile soru modeli arasında bağlantı kurmak zorunluluğu ve bunun
programlama dilinden bağımsız olması, program yazmayı zorlaştırdığı gibi,
programların bakımını da pahalılaştırmaktaydı. Sırf bu amaç için ayrıca endüstriler
gelişmişti.
Makine modellemeye seçenek olarak, çözülecek sorunun kendisini modellemek
ortaya çıktı. Bu amaçla geliştirilen LISP ve APL gibi ilk diller dünyaya özel
açılardan bakmaktaydılar. Birincisi, bütün sorunların listelerle açıklanabileceğini
belirtirken, öteki, tamamı algoritmalarla açıklanabilir demekteydi. PROLOG ise
sorunları, kararlar zinciriyle çözümlüyordu. Diller belli sınırları olan
programlamaya göre oluşturulmuşlardı. Bunlar ayrıca grafik simgelerle işlemler
yapıyorlardı. Daha sonra bunun programlamada, ne kadar sınırlayıcı olduğu ortaya
çıktı. Bu anlatılan diller, özel sorunların çözümleri için yeterliydiler. Fakat,
kurgulandıkları özel durumların haricindeki sorunların çözümünde, derde deva
olamıyorlardı.
Nesne yönelimli yaklaşım, bir adım daha ileri giderek, sorun ortamı ögelerini
temsil eden araçlar sağladı. Bu temsil yeteneği o kadar gelişmiştiki, programcı belli
özellikteki sorunların çözümüyle kısıtlı değil, her soruna çözüm üretebilir hale
gelmişti. Sorun ortamındaki ögeler, çözüm ortamında nesneler olarak temsil
edilmekteydi.(tabii bu benzetmedeki nesnelerden başka yeni nesneleri
programcılar ekler.). Buradaki temel fikir; sorunun diline yeni nesneler ilave
ederek, programın kendisini uyarlamasına izin vermektir. Böylece çözümü
sağlayan kaynak kodları okurken, aynı zamanda sorunu da açıklayan kelimeleri
okumuş olacağız. Bu şimdiye kadar olan dillerdeki soyutlamalardan hem daha
esnek, hem daha güçlüydü. Böylece NYP sorunun çözümünü, sorunun kendisiyle
açıklamaktaydı. Artık eski dillerde olduğu gibi bilgisayarla, yani çözüm ortamıyla
açıklama ortadan kalkıyordu. Hala bilgisayarla bir bağlantı vardı ama, bu her
nesnenin küçük bir bilgisayar olmasından başka birşey değildi. Nesnenin bir
durumu vardı ve bu nesnenin istendiğinde gerçekleştireceği işlevler vardı. Bunun
gerçek hayattaki nesnelerin özellikleri ve davranışlarına benzediği pek
söylenemez.
C++ benzeri genel amaçlı dillerde uygulamaya özgü tipler bulunmaz. C++ bunun
yerine, daha ayrıntılı soyut veri tipleri oluşturmanız için kolaylıklar sağlar. Esasen
soyut veri tiplerinin amacı; C++ dilini belli bir sorun ortamına (programı
yazılacak) uydurabilmektir.
Buraya kadar anlatılanlardan programlama dilinin bir paradigma olduğunu
anlamışsınızdır. Paradigma; kuram, standart ve bunlara bağlı yöntemler
kullanılarak bilginin kurgulanmasıdır. NYP de bir paradigmadır. Kendi iç
tutarlılığı vardır. Bazı dil tasarımcıları, NYPnin çözümleyemediği sorunlar
karşısında çok paradigmalı programlama dillerinin değişik yaklaşımlarını
önermişlerdir. Aslında C++ çok paradigmalı bir dildir. Hem generik hem NYP
özellikler taşır. Bundan başka hemen her C++ derleyicisi C programlarını da
derleyebilir. Yani emredici (yapısal) bir dil olarakta kullanılabilir.
Alan Kay ilk başarılı nesne yönelimli dil olan Smalltalk'ın 5 temel özelliğini
belirlemişti. Smalltalk, C++ nın temel aldığı dillerden birisidir. Bu 5 özellik nesne
yönelimli programlamanın iskeletidir.
1-.Herşey nesnedir. Nesneyi şöyle düşünebilirsiniz: o öyle bir değişkendirki, veri
saklayabilir, siz ondan istekte bulunabilir ve ondan kendi üzerinde işlem yapmasını
isteyebilirsiniz. Çözülecek sorundaki herhangi bir fikir unsuru da, programda
nesne olarak temsil edilebilir. (akıl,köpek,bina, servis,ağaç vs.)
2-.Program, birbirlerine ne yapacaklarını iletiler göndererek bildiren nesneler
topluluğudur. Bir nesneden bir istekte bulunmak için, o nesneye bir ileti
göndermek gereklidir. Daha da açık söylersek, iletiyi nesneye ait işlev çağrısı
olarak kabul edebiliriz.
3-.Her nesnenin öteki nesnelerden oluşan kendi belleği vardır. Başka bir
deyişle,yeni bir nesne yaratmak için var olan nesneleri biraraya getirebiliriz.
Böylece programlarda dolambaçlı durumların arkasında nesnelerin basitliğinin
olduğu saklanmış olur.
4-.Her nesnenin bir tipi vardır. Her nesne bir sınıfın örneğidir. Sınıf ile tip
anlamdaş olup, sınıftaki fark; ona ileti gönderebilmenizdir.
5-.Belli bir tipe ait tüm nesneler aynı iletiyi alabilirler. Sonra ayrıntılarını
göreceğimiz gibi bu yüklenmiş (loaded) bir deyimdir. Örneğin çember tipinin bir
nesnesi, aynı zamanda şekil tipinin de bir nesnesidir, ve böylece çember nesnesi
şekil tipinin iletilerini de kabul eder. Bu demektirki şekillerle konuşabilen kodlar
yazabilir, ve şeklin biçimine uygun herhangi bir şeyi otomatik olarak
kullanabilirsiniz. Bu yerine koyulabilirlik, NYP nin en güçlü özelliklerindendir.
Nesnelerin arayüzü
Aristo büyük ihtimalle tip kavramı üzerine ilk ayrıntılı çalışmayı yapan bilim
adamıydı. O kuşların sınıfı, balıkların sınıfı gibi konular üzerinde kafa yoruyordu.
Burada ise benzer tüm nesneler, ortak davranış ve karakteristikleri olan bir sınıfın
parçasıydılar, ve bu ilk NYP olan Simula-67 de de kullanılmıştı. Böylece Simula67 programlamada yeni bir tip olan Class anahtar kelimesini bize tanıştırdı.
Simula programlama dili, adından da anlaşılacağı gibi, benzetimler (simulasyonlar)
oluşturmak için geliştirildi. Bunlardan biri; ünlü banka vezne sorunudur. Burada
veznedarlar, müşteriler, muhasebeciler, işlemler ve para birimleri vardır. Yani bir
sürü nesne. Programın çalışması esnasında, o anki durumları dışında, benzer olan
nesneler, kendi aralarında nesne sınıfları olarak gruplanmıştı.
İşte sınıf yani class kavramı buradan gelmekteydi. NYP de soyut veri tipleri (class)
yaratmak, temel konudur. Soyut veri tipleri yerleşik veri tipleri gibi çalışırlar. Bir
tipin değişkenlerini yaratabilir (nesne yönelimli deyimiyle; nesneler veya
özellikler) ve işlemler yaptırabilirsiniz (ileti gönderimi veya istemi, yani iletiyi
gönderirsiniz, nesnede onunla ne yapacağını hesaplar). Sınıf üyelerinin yani
ögelerinin bazı ortak yanları bulunur, her muhasebenin mizanı vardır, her veznedar
mevduat kabul eder vs. Aynı zamanda herbir üyenin kendi özel durumu
bulunur,farklı muhasebelerin mizanları farklı, veznedarların adları da birbirinden
farklıdır. Böylece muhasebeler, veznedarlar, işlemler ve müşteriler kendi
sınıflarıyla programda tek başlıkla temsil edilebilir. Bu başlık nesnedir ve ait
olduğu sınıfın özellik ve davranışlarını gösterir.
Böylece NYP de yaptığımız şey, yeni veri tipleri oluşturmaktan başka bir şey
olmayıp, tüm NYP dillerinde bu class terimiyle gösterilmektedir. class terimine
rastladığınızda, tip olarakta algılayalabilirsiniz .
Benzer karakteristik (veri öğeleri) ve davranışları (işlevler) gösteren nesneleri
tanımlayan class, yani sınıf, bir veri tipidir. Örnek olarak kayan noktalı sayı hem
karakteristik, hemde davranış sahibidir. Veri tipi ile class arasındaki fark; birini
programcı problemine uygun olarak tanımlıyor, öteki ise önceden tasarımlanmış
ve makinede bulunan bir bellek birimidir. Programcı ihtiyacına uygun veri tipi
ekleyerek, aslında programlama dilinin gelişmesine katkıda bulunur. Programlama
sistemi yeni sınıflara yani veri tiplerine, yerleşik veri tipler gibi davranır.
Nesne yönelimli yaklaşım sadece benzetim (simülasyon) oluşturulmasında
kullanılmaz. Gerçi kabul edin veya etmeyin, her program tasarımını yaptığınız
sistemin bir benzetimidir. NYP tekniklerinin kullanımı büyük bir sorun yumağını,
basit bir şekilde çözüme ulaştırır.
Bir sınıf kurgulandığında, bu sınıfa ait istenildiği kadar nesne oluşturulabilir. Ve
bunları, çözmeye çalıştığınız sorunun elemanlarıymış gibi kullanarak işlem
yapabilirsiniz. Nesne yönelimli yaklaşımın en önemli üstünlüğü çözüm
ortamındaki nesnelerle, sorun ortamındaki elemanlar arasında birebir temsiliyet
sağlamasıdır.
Bir nesnenin size yararlı iş yapmasını nasıl sağlarsınız?. Nesnenin iş yapmasını
sağlayan bir istek, mutlaka olmalıdır. Bitmiş bir işlem,ekrandaki bir çizim yada
ışığın açılması gibi istekler olabilir. Her nesne sadece belli istemleri yerine
getirebilir. Bir nesneden yapması istenenler, ait olduğu sınıfın arayüzü tarafından
tanımlanır. Tipin davranışını arayüz belirler. Basit bir örnek olarak elektrik ampul
sınıfını kurgulayalım.
tip(sınıf) adı ---> LAMBA
arayüz ---------> ON( )
OFF( )
PARLAK( )
AYARLI( )
Program bölümü: LAMBA lt;
lt.ON( );
aç diyor
//LAMBA sınıfının lt nesnesi oluşturuluyor
//lt nesnesi ON( ) işlevini çağırıyor, yani ışığı
Arayüz, belli bir nesneden hangi istemlerde bulunabileceğimizi gösterir. İstemi
açıklayan kod mutlaka yazılmış olmalıdır. Bu, görünmeyen verilerle uygulamayı
kapsamalıdır.
Önceki programlama yöntemlerinde, bu sorunun çözümü o kadar karışık değildi.
Her tipin istemlere uygun bir işlevi vardı. Nesneden özel bir istem de
bulunulduğunda ise, ilgili işlev çağrılır. İşlemleri özetlersek; siz nesneye mesaj
iletiyorsunuz (istem de bulunuyorsunuz), nesnede mesajla iletileni yerine getiriyor.
(kodu işlemliyor).
Örneğimizde tipin yada class ın adı LAMBA, bu LAMBA sınıfının özel bir
nesnesinin adı da lt dir. LAMBA sınıfının herhangi bir nesnesinden ON( ),OFF
( ),PARLAK( ) ve AYARLI( ) işlevlerinin yerine getirilmesini
isteyebiliriz.LAMBA lt; ile LAMBA sınıfından lt isimli bir nesne oluşturuyoruz.
Daha sonra bu nesneye bir mesaj gönderiyoruz. Bunuda lt isimli nesnemizin adını
bildirip daha sonra mesaj iletimizi bir nokta ile bağlayarak yapıyoruz; lt.ON( ); Bu
kod satırını lt LAMBA nesnesini aç şeklinde yorumlayabiliriz. Görüldüğü üzere
önceden tanımlanmış sınıfların kullanılması, kullanıcısı için NYP yi çok
kolaylaştırır.
Yukarıda gösterilen çizge biçemi Birleşik Modelleme Dili (Unified Modelling
Language) olarak adlandırılmaktadır. Her sınıf bir dörtgen tarafından temsil edilir.
Dörtgenin üst kısmında sınıfın adı,orta kısmında veri üyeleri ve en alt kısmındada
üye işlevler yer alır.
Üye işlevler, sizin nesneye gönderdiğiniz iletilerdir ve sizin nesnenize aittirler.
Veri üyelerini ise siz tanımlarsınız. Çoğunlukla sınıf adı ve public üye işlevler
BMD (birleşik modelleme dili) tasarım diyagramlarında gösterilmektedir. Bu
nedenle orta kısım atlanmaktadır. Sadece sınıf adıyla ilgiliyseniz en alt kısımada
gerek yoktur.
Gizli Kısım
Oyun sahamızı, sınıf geliştiricileri ve onların ürettiği sınıfları satın alan müşteri
programcılar diye ikiye ayıralım. Sınıf geliştiricileri yeni veri tiplerini oluşturanlar,
müşteri programcılar ise, uygulamalarında bu yeni veri tiplerini kullananlardır.
Müşterinin amacı hızlı program geliştirmek için sınıflarla dolu bir alet çantasına
sahip olmaktır. Sınıf geliştiricinin amacı ise sadece müşteri programcıya gereken
özellikte sınıfı inşa etmek, ve başka her şeyi gizlemektir. Niçin?.Çünkü eğer
gizlenirse, müşteri programcı kullanamaz. Müşterinin kullanamadığı kısımda, daha
sonra sınıf geliştirici değişiklikler yapabilir. Örtülmüş yani gizlenmiş kısım,
nesnenin zayıf iç özelliklerini kapsar. Dikkatsiz ya da bilgi yetersizliği olan bir
müşteri, sınıf yapısını bozabilir. Gizlemeyle müşteri programcı hatalı kod
üretmekten kurtulur. Gizlenmiş bölüm hakkında daha fazla birşey söylenemez.
Herhangi bir ilişkide, ilgili tarafların gözönünde bulundurduğu sınırlar çok
önemlidir. Bir sınıf kütüphanesi yazdığınızda, kendisi de programcı olan müşteri
programcı ile ilişki kuruyorsunuz. Fakat o sizin sınıf kütüphanenizi kullanarak bir
uygulama yazar. Olasıdır ki sizin ona verdiğinizden daha büyük bir program inşa
etmektedir.
Bir sınıfın tüm üyeleri herkese açık olsaydı, müşteri programcı o sınıfla herşeyi
yapmaya kalkar, ve onu kimse engelleyemezdi. Böylece sınıf geliştiricisi
oluşturduğu sınıfın denetimini kaybederdi. Ve bunu da engellemenin tek yolu
erişim denetimidir. Yani erişim denetiminin ilk nedeni, sınıfın dokunulmaması
gereken yerlerini müşteriden saklamaktır. Saklanan bu kısımlar iç kurgu olup,
kullanıcının sorunlarını çözmesi için gerekli olan arayüzün parçası değildirler.
Kullanıcılar buradan kendileri için neyin gerekli, neyin gözardı edilmesi
gerektiğini kolaylıkla görebilirler.
Erişim denetiminin varlığının ikinci nedeni; kütüphane tasarımcısına, sınıfın iç
çalışmasında değişiklik yapmasına izin vermesidir. Değişiklikten, müşteri
programcı erişim denetimi sayesinde etkilenmez.Örneğin basit usulle, uygulama
geliştirme için bir sınıf yazıyorsunuz. Daha sonra aynı işi yapan fakat daha hızlı
çalışan aynı sınıfı yeniden yazıyorsunuz. Eğer arayüz ve işlemleme birbirlerinden
ayrık ve korumalı ise, bunu başarmak kolaydır. Kullanıcı için sadece yeni bağlantı
lazımdır.
C++ sınıf içerisinde, sınırları kuran üç özel anahtar kelime kullanır; public,
private ve protected .Bunların anlamları ve kullanımları çok basittir. Erişim
anahtarlarının belirttiği; kendisini takibedenleri, kimlerin kullanacağıdır. Public'te,
takibeden tanımlar herkese açıktır. Private'de ise, takibeden tanımlara sınıf yazarı
hariç hiç kimse erişemez. Private müşteri programcı ile sınıf geliştirici arasındaki
duvardır. Birisi sınıfın private üyelerine erişmeye kalkarsa, derleme esnasında
hatayla karşılaşır. Protected anahtarı da private özelliklerine sahiptir. Yalnız
buradaki fark; Protected üyelere kalıtım yoluyla yazılmış sınıfların
erişebilmesidir. Fakat private üyelere hiçbir şekilde erişilemez.
Uygulamanın yeniden kullanılması
Bir sınıf bir kez yazılıp denendiğinde, (başarılı olursa) o artık bir birimlik faydalı
kod olmuştur. Bu bir birimlik kodun defalarca kullanımı çok kolay değildir.
Deneyim ve iyi tasarım bakışı gerekir. Böyle bir tasarımınız varsa bu tasarım,
tekrar kullanım imkanı sağlayabilir. NYP nin en büyük üstünlüklerinden bir tanesi
de kodların tekrar kullanılabilmesidir. Bir sınıfın tekrar kullanımının en basit yolu,
doğrudan o sınıfın nesnesini kullanmaktır. Ayrıca o sınıfın nesnesini yeni bir
sınıfın içine de yerleştirebilirsiniz. Buna programcılıkta "üye nesne oluşturmak"
diyoruz. Yeni sınıfınız değişik tiplerdeki çok sayıda nesnelerden oluşabilir. Ve
arzulanan işlevleri yerine getirmek amacıyla değişik kombinasyonlar olabilir. Var
olan sınıflardan yararlanarak yeni bir sınıf oluşturulduğu için, buna harç
(kompozisyon) denir. Biz bundan sonra harç terimini kullanmayı tercih edeceğiz.
Zira inşaatlarda kum, kireç ve çimento ile harç yapılır.
Ve burada bir şekilde inşaat yapılmaktadır. Harçlama tekniği çok esnek bir yapıdır.
Buradaki üye nesnelerin erişimi genellikle private dir. Böylece müşteri programcı
bunlara erişemez. Sınıf geliştirici ise ana kurguyu bozmadan bunlarda değişiklik
yapabilir. Ayrıca programın çalışması sırasında da, üye nesnelerde değişiklik
yapılabilir. Böylece program daha dinamik davranışlar sergileyebilir. Daha sonra
ayrıntılarıyla anlatılacak olan kalıtım, böyle bir esnekliğe sahip değildir. Çünkü
derleyici kalıtımla yazılmış olan sınıflar üzerinde derleme zamanı kısıtlamaları
koymuştur.
Kalıtım NYP da çok önemli olduğu için burada vurgulanmıştır. Programcılığa yeni
başlayan birisi kalıtım hakkında bilgi sahibi olduğunda onu hemen her yerde
kullanmak isteyebilir. Başlangıçta yazdığı programlarda kalıtımı beceriksizce ve
zayıf bir şekil de de kullanabilir. Onun yerine yeni sınıflar yazarken harcı tercih
etmelidir. Zira bu yöntem hem daha esnek, hem daha basittir. Eğer bu yaklaşımı
tercih ederseniz,tasarımınız daha berrak olur. Yeterli deneyimi kazandığınızda
kalıtım yöntemini de kullanabilirsiniz.
Kalıtım: arayüzün yeniden kullanımı
Fikir olarak nesnenin kendisi çok kullanışlıdır. Veri ve işlevi biraraya
getirebilmektedir. Bu nedenle sorun ortamı makinaya bağlı olmaksızın
betimlenebilir. Bunları ifade edebilmek class anahtar kelimesi ile olmaktadır.
Class C++ dilinin temel birimidir.
Bütün sorunları giderecek bir sınıf tanımlamak mümkün değildir. Bunun yerine
temel bir sınıf tanımlamak, benzer özellik ve işlevleri bulunan başka sınıflar
gerektiğinde ise temel sınıftan alt sınıflar türetmek, NYP nin en temel
üstünlüklerindendir. Bir sınıftan başka bir sınıf türetmeye kalıtım diyoruz.
Türetilen sınıfın temel sınıftan farkı, bazı değişiklikler ve eklemelerdir. Burada
kendisinden sınıf türetilen sınıfa temel,ana yada baz sınıf denmektedir. Bir
sınıftan birden fazla alt sınıf türetilebilir. Türetilen sınıflar tek bir sınıftan
türetiliyorlarsa, buna tekli kalıtım, birden fazla sınıftan türetiliyorlarsa, çoklu
kalıtım denir.
Çoklu kalıtım C++ nın en tartışmalı konularındandır. Tekli kalıtımda iki sınıf
vardır. Bunların her ikisindede, özellikler ve işlevlerde ortak yanlar vardır.Yalnız
birinde, daha fazla özellik ve işlevler vardır. Temel sınıf kendinden türetilen
sınıfın, bütün özellik ve davranışlarını gösterir. Çoklu kalıtımda türetilen sınıf
birden fazla sınıftan türetilmiştir. Nesnelerle ilgili düşüncelerin çekirdeğini temsil
eden temel sınıfı oluşturabiliriz. Daha sonra bu temel sınıftan, gereken sınıflar
türetiliebilir.
Örnek olarak atık yeniden kazanım makinelerini ele alalım. Bu makineler atıkları
belli amaçlara göre sıraya koyarlar. Burada ana sınıf, atık olur. Atığın belli bir
ağırlığı ,değeri ve başka özellikleri vardır. Atık parçalanır,eritilir ve ayrıma tabi
tutulur. Buradan çok farklı özellikleri olan atıklar (belli renk şişeler vs) ve davranış
tipleri türetilebilir (alüminyum kutu parçalanabilir,çelik mıknatıslanabilir vs).
İlaveten bazı davranışlar farklı olabilir (kağıdın ederinin, durumuna ve tipine bağlı
olması gibi). Kalıtımı kullanarak sorunu çözmek için tip hiyerarşisi meydana
getirilir. Böylece akraba tipler yani sınıflar sayesinde problem daha kolay çözülür.
İkinci örnekte klasik şekil örneğidir. Bilgisayar destekli tasarım yada oyun
benzetimlerinde kullanılır. Burada ana sınıf, yani tip; şekildir. Şekilin boyutları,
rengi, pozisyonu vs. özellikleri vardır. Her şekil çizilebilir, silinebilir, hareket
ettirilebilir, renklendirilebilir vs.işler yaptırılabilir. Buradan yola çıkarak şeklin
özel tipleri türetilebilir; daire, üçgen, kare ve diğer tipler. Bunların herbiri kendine
has özellik ve davranışlara sahiptirler. Tip hiyerarşisi, şekiller arasındaki hem
benzerlikleri, hem de farklılıkları gözönüne alır.
Çözümü problemin terimleriyle yapmak çok kazançlıdır. Zira problemin
tanımından çözümün tanımına giden yolda çok sayıdaki ara modele ihtiyaç yoktur.
Nesneleri kullanırken tip hiyerarşisi temel yapıdır. Bu sayede gerçek dünyadaki
sistemin tanımından, kodlarla sistemin tanımına doğrudan doğruya geçilir. Nesne
yönelimli tasarıma sahip birisi için zorluklardan biri budur. Baştan sona çok basit
oluşu yani.
Varolan bir tipten, kalıtım yoluyla yeni bir tip oluşturulur. Yeni türetilen tip,
varolan tipin sadece bütün üyelerini kapsamaz (private üyeler hariç, zira onlar
erişilemez) daha da önemlisi ana sınıfın arayüzünü tekrarlar. Yani ana sınıfın
nesnelerine gönderdiğiniz bütün iletileri, türetilen sınıfın nesnelerine de
gönderebilirsiniz. Sınıfın tipini ona gönderdiğimiz iletilerden bildiğimize göre,
türetilmiş sınıfın tipi de ana sınıfın aynısıdır. Önceki örnekte "daire bir
şekildir"gibi. Kalıtım yoluyla sağlanan tip eşdeğerliliği, nesne yönelimli
programlamayı kavramanın adımıdır.
Madem ki hem ana sınıfın hem de türetilmiş sınıfın arayüzleri aynı, bu arayüzlerin
uygulayıcı kodlarıda beraberin de bulunmak zorundadır. Yani bir nesne özel bir
ileti aldığında, yerine getireceği işlevin kodları yanında olmak zorundadır. Bir sınıf
türetir ve başka hiçbirşey yapmazsanız, ana sınıfın arayüz yöntemleri doğrudan
türetilmiş sınıfa geçer. Bunun manası türetilmiş sınıfın sadece tipi aynı değil aynı
zamanda davranışıda aynıdır, ki bu çok ilginçtir. Türetilmiş sınıfı ana sınıftan
farklı kılmanın iki yolu vardır. İlki oldukça basittir;basit bir şekilde türetilmiş
sınıfa yeni işlevler eklersiniz. Bu eklenen işlevler ana sınıfın işlevleri değildirler.
Bunun anlamı da; basitçe ana sınıfın, istediğimiz işlevlerin hepsini yerine
getirememesidir.Yeni işlevi türetilmiş sınıfa ekliyoruz. Kalıtım, bu basit ve ilkel
kullanımı ile, soruna mükemmel çözüm sağlar. Belki yakından incelediğinizde, ana
sınıfta bu işlevlere ihtiyaç duyabilir.
Araştırma süreci ve tasarımın tekrarlanması nesne yönelimli programlama da
düzenli olarak yapılır.
Kalıtımın arayüze yeni işlevler ekleteceği sanılsa da, bu tamamen doğru değildir.
Yeni sınıfı ana sınıftan ayıran ikinci ve daha önemli fark ise davranış
değişikliğidir. Buna işlev bindirimi yada aşırı yükleme denmektedir. İşleve
bindirim yapmak demek, türetilmiş sınıfta ki aynı isimli işleve, ana sınftakinden
farklı tanım vermektir. Burada söylenen; aynı arayüz işlevini kullanıyorum, fakat
yeni tipimde onunla farklı şey yapacağım. İşlev bindirimi bir işlevin değişik
yerlerde değişik tanımlara sahip olmasıdır.
...dire karşılık ...gibidir
Kalıtım ile ilgili bazı konularda, tartışmalar sürmektedir; kalıtımda bindirim,sadece
ana sınıf işlevlerinemi uygulanmalı.
(veya ana sınıfta olmayan yeni üye işlevler yeni sınıfa eklenemezmi?). Bunlar olmaz
doğal olarak türetilmiş sınıfın tipi ana sınıfın tamamen aynı olur. Çünkü arayüz aynıd
Netice olarak ana sınıfın bir nesnesinin yerine türetilmiş sınıfın bir nesnesini
yerleştirebilirsiniz.Bunu saf yedekleme olarak adlandırırız, yedekleme prensibi diye
bilinir. Anlam bakımından kalıtım uygulamasının ideal yolu budur. Ana sınıf ile
türetilmiş sınıf arasındaki bu ilişki dir (dır) ilişkisidir. Daire bir şekildir.
Türetilmiş sınıfa yeni işlevler ekleyerek, arayüzü genişletip, yeni bir tip yaratmak
için erken olmalı. Zira yeni tip hala, ana sınıf için yedek olarak kullanılabilir. Ama
mükemmel bir uygulama değildir,çünkü yeni işlevlere ana sınıftan
erişilememektedir. Bunun adı, gibidir ilişkisidir.Yeni tip eski tipin arayüzüne
sahiptir, fakat başka işlevler de vardır. Böylece tamamen aynı olduklarını
söylenemez. Anlamak için örnek olarak, hava ısı düzenleme aracını inceleyelim;
Termostat ------------denetleme-----------Soğuklama
daha_düşük_sıcaklık( )
soğut( )
___|____
|
|
hava_ısı_düz
ısı_pompası
soğut( )
soğut( )
ısıt( )
Burada ısı_pompası, hav_ısı_düz gibidir. Isı pompası daha fazla iş yapabilir. Zira
soğutma yanında ısıtma işlevi de vardır. Burada olay şöyledir; evimizin hava ısı
düzenlemesi termostat sınıfı tarafından denetlenmektedir. Ayarlanan sıcaklığın
üzerinde ki sıcaklıklarda, Soğuklama sınıfına görev verilmekte, ve türetilmiş sınıfı
hava_ısı_düz sınıfının soğut( ) işlevi görevlendirilmektedir.hava_ısı_düz sınıfı
bozulunca getirip yerine ısı_pompası takıyoruz. Bu yeni sınıf önceki
hava_ısı_düz'ün işlevine ek olarak ısıt( ) işlevine de sahip. Bu işleve ana
soğuklama sınıfından erişilemez. Fakat soğut( ) işlevi hava_ısı_düz deki gibi
erişilip kullanılabilir. Yani burada gibidir durumu vardır. Ya da ısı pompası
hava_ısı_düz gibidir. Ana yapı soğutma ihtiyacı üzerine kurulu olduğundan yeni
tipin gelişkin nesnesi ilk arayüzdeki ortak işlev hariç diğerini tanımaz. Yukarıdaki
çizgeye bakıldığında ana Soğuklama sınıfı yeterli değildir.Ve adı
sıcaklık_denetleme olmalı ve ısıtmayı da kontrol etmelidir. O zaman yedekleme
prensibi geçerli olur. Yukarıdaki örnek olup, tasarım tarafında ve gerçek dünyada
neler olduğunu göstermektedir.
Çokbiçimlilikle nesne değişimi
Tip hiyerarşileriyle haşır neşir olurken, genellikle nesneyi belli bir tip olarak
işlemlemek istemeyiz. Bunun için kodları belli tipe göre yazmayız. Şekil
örneğimiz de, işlevler başlangıç şekil üzerinde işlem yaparken onun dairemi,
üçgenmi, karemi olduğuna bakmaz. Tüm şekiller çizilebilir,silinebilir ve hareket
ettirilebilir. Bu işlevler şekil nesnesine basitçe bir ileti gönderirler. İşlevler
nesnenin bu ileti ile yapacaklarından şüphe duymazlar. Yeni tiplerin ilavesi
durumunda kodlar etkilenmezler.Yeni tipler nesne yönelimli programlamada, yeni
durumlara uygunluk sağlamak için gereklidir. Örnek olarak, başlangıçtaki
şekillerle ilgili işlevleri değiştirmeksizin, altıgen isimli yeni aratip şekli
türetebiliriz. Yeni aratipin kolaylıkla türetiminin önemi; yazılımı geliştirdiği gibi,
yazılım bakım maliyetlerini de düşürür.
Türetilmiş tip nesnelerle, onların başlangıçtaki ana tipi gibi işlem yaparken, sorun
ortaya çıkar (şekil olarak daire,vasıta olarak bisiklet,kuş olarak karabatak vs). Bir
işlev başlangıçtaki şekilden kendisini çizmesini isterse,başlangıçtaki araç
sürmesini isterse, ya da başlangıçta ki kuş hareket etmesini isterse, o zaman
derleyici hangi kodu derleyeceğini tam olarak bilemez. Esas nokta budur. İleti
gönderildiğin de, programcı hangi kodun işlemleneceğini bilmek istemez. Çizim
işlevi daireye,kareye ve üçgene aynı şekil de uygulanır. Ve nesne onun tipine bağlı
uygun kodu işlemler. Eğer hangi kod parçasının işlemleneceğini bilmek zorunda
değilseniz, daha sonra yeni bir ara tip eklediğiniz de işlemlenecek kod, işlev
çağrısında değişiklik olmadığı halde, farklı olabilir. Bundan dolayı derleyici, hangi
kod parçasını işlemleyeceğini tam olarak bilemez. O zaman ne yapılır?.Şimdi bir
örnekle anlatalım:
kuş_denetleyici -------------------- kuş
hareket_et( )
yer_değiştir( ) komutu verilince ne olur? hareket_et( )
kaz
hareket_et( )
penguen
hareket_et( )
Yukarıda kuş ana sınıfından kaz ve penguen sınıfları türetilmiştir. Gerek ana sınıf
kuşun, gerekse türetilmiş kaz ve penguen sınıflarının arayüzleri aynı olup
hareket_et( ) işlevidir. Kaz hareket eylemini koşma,uçma ve yüzme olarak
gerçekleştirirken penguen koşar ve yüzer.Yukarıdaki yapıda kuş_denetleyici
nesnesi sadece başlangıçtaki kuş nesneleriyle çalışır. hareket_et( ) komutuyla
doğru davranış doğru kuşa göre belirlenir. Bunun belirlenmesi NYP nin en kıvrak
özelliklerindendir. Bu sorunu aşmak için geç bağlama işlemini açıklamalıyız.
NYP de işlev çağrı eylemi ile işlemlenecek işlev kodunun adresi, eğer çalışma
sırasında belirlenirse buna geç bağlama denir. Derleme sırasında yapılsaydı erken
bağlama olurdu. NYP de nesneye bir ileti gönderdiğinizde, işlemlenecek ileti
kodu, çalışma zamanı gelinceye kadar belli değildir. Geç bağlamada derleyici
işlevin varlığından emin olup, dönüş değeri ve değişkenlerin tip denetimini yapar
(buna dilde yanlış olarak zayıf tipleme denilmektedir). Ama derleyici
işlemlenecek kodu tam olarak bilememektedir.
Geç bağlamayı C++ derleyicisi şöyle gerçekler; mutlak çağrının yerine özel bir
kod yerleştirilir. Bu kod işlev adresini hesaplar. Hesaplama için nesne bilgilerinden
yararlanır. Konu daha sonra ayrıntılarıyla işlenecektir. Her nesne yerleştirilen özel
kod bitine göre farklı davranır. Nesneye bir ileti gönderildiğinde nesne iletilen
mesajla yapılacak olanı hesaplar. Bir işleve geç bağlama özelliklerine sahip olma
esnekliğini, virtual anahtar kelimesi sağlar. Şu an virtual kelimesini kullanmak
için, mekanizmayı bilmeye gerek yoktur. Fakat onsuz NYP nin olmadığının
bilinmesi lazım. virtual kelimesi mutlaka ilave edilmelidir, zira C++ da üye
işlevler dinamik olarak bağlı değildirler. Bu sanal işlevler, aynı ailenin
sınıflarındaki farklı davranışları açıklamayı sağlarlar. Bunlar çokbiçimliliğe neden
olan farklardır.
Şekil örneğine tekrar geri dönelim. Tamamı aynı tek arayüze sahip sınıflar ailesi,
bu konunun baş kısımlarında gösterilmişti. Çokbiçimliliği anlatmak için, tipin çok
özel ayrıntılarını gözardı ederek, sadece ana sınıfla konuşan bir parça kod yazalım.
Bu kod, tipe özgü bilgiden habersizdir. Bunu yazmak ve anlamak kolaydır.
Kalıtıma altıgen tipini ekleyecek olursak, yazdığımız kod şekil ana sınıfının önceki
tipleri için ne kadar çalışırsa, bu yeni altıgen tipi için de öyle çalışır. Böylece
program genişletilebilir. C++ da bir işlev (fonksiyon) yazalım; (daha sonra nasıl
yazıldığı anlatılacak)
void doStuff(Shape& s){
s.sil( );
//...
//...
s.ciz( );
}
Bu işlev herhangi bir şekile, yapabileceklerini anlatmakta olup, bunların ayrıca
nesnenin tipinden bağımsız olduğunu da göstermektedir. Yani bu işlev herhangi
bir şekli siler ve çizer (& işareti s şekil nesnesinin adresini doStuff ( ) a aktarır.)
( Bu ayrıntıları şimdilik anlamak önemli değil.). doStuff( ) işlevini programın
başka yerlerinde kullanabiliriz;
Daire d ;
Uçgen u ;
Cizgi c;
doStuff(d) ;
doStuff(u) ;
doStuff(c) ;
Nesnenin çeşidine bakılmaksızın doStuff( ) işlevi çağrıldığında her şey
kendiliğinden doğru bir şekil de yapılır.
Bu işlem size şaşırtıcı gelmemelidir.Şimdi
doStuff(d) ;
Ne olduğuna bakalım; şekil bekleyen işleve daire nesnesi gönderilmiştir. Çünkü
daire bir şekil dir . doStuff( ) tarafından şekile gönderilen bir ileti, daire tarafından
da kabul edilir. Bu işlem tamamiyle güvenli ve mantıklıdır. Türetilmiş tipte ana tip
gibi yaptığımız bu sürece yukarıdöküm diyoruz. Bu ifade dökümcülükten
kaynaklanmaktadır. Yukarılığı kalıtım hiyerarşisinden kaynaklanmaktadır. Ana
sınıf en yukarıda yer almakta, türetilmiş sınıflar ise aşağıya doğru yayılmaktadır.
Böylece ana sınıfa yapılan dökümden dolayı yukarıdöküm diyoruz. NYP da
herhangi bir yerde yukarıdöküme rastlanabilir. Bu çalışılan tipin tam olarak
hangisi olduğunu bilmeyi gerektirmez. Yukarı döküm, bir tipin hiyearşide yukarıda
bulunan tipe dönüştürülmesidir. doStuff( ) taki kodlara bakalım;
s.sil( ) ;
//..
//..
s.ciz( ) ;
Örnekte; daireyse silme,çizme kareyse ciz ya da sil denmiyor. Böyle bir kodunuz
varsa Shape olabilecek bütün tiplere bakılır. İhtiyaç olduğunda biraz karışık
görünse de yeni tip Shape ler eklenebilir.Yani burada siz, şekil olabilen unsurlara
"siz şekilsiniz kendinizi silin, çizin ve ayrıntıları doğru biçimde uygulayın"
diyorsunuz.
doStuff( ) ta ki etkileyici olan, kodun doğru olanları yapmasıdır. Daire için
çağrılan ciz( ) işlevi ile kare için çağrılan
ciz( ) işlevinin işlemledikleri kodlar birbirlerinden farklıdır. ciz( ) iletisi genel
shape e aktarıldığında,onun doğru tipine uygun davranış ortaya çıkar. Daha önce
anlatıldığı üzere C++ derleyicisi, doStuff( ) kodlarını derlerken kodların, tiplerin
hangisiyle ilgili olduğunu bilmemektedir. Doğru olanı bulması oldukça şaşırtıcıdır.
Doğal olarak beklenen, shape için ciz( ) ve sil( ) sürümleridir yani daire,üçgen ya
da çizgi gibi özel tipler için değil. Ama ciz( ) ve sil( ) doğru bir şekil de özel tipler
için yapılabilmektedir. Bunun sebebi ise çokbiçimliliktir. Derleyici ve çalışan
sistem ayrıntıları kullanır, bilinme ihtiyacı duyulan herşey oluşur, ve daha önemli
olanda onunla tasarımın yapılma şeklidir. Üye işlev virtual ise, nesneye bir ileti
aktarıldığın da nesne doğru olan eylemi yapar . Bu yukarıdöküm olduğunda da
böyledir.
Nesneleri oluşturma ve yoketme
Teknik anlamda NYP; veri soyutlama, kalıtım ve çokbiçimlilik özelliklerine
sahiptir. NYP de enaz bunlar kadar önemli başka unsurlarda vardır. Şimdi bunları
kısaca ele alacağız.
Özellikle önemli olanlar; nesnelerin oluşturulmaları ve yokedilmeleridir. Nesneleri
oluşturursunuz kullanırsınız sonra işi bitince silersiniz. Böylece bellekte yeni
işlemler için yer kalır. Belleği en verimli şekilde kullanmış olursunuz. Nesnenin
verileri nerededir ve ömrü nasıl belirlenir?. Bunun için, farklı dillerde farklı
yaklaşımlar vardır. C++ da verimlilik en önemli kıstastır. O nedenle bu konuya
yaklaşımı, verimlilik yönünde programcıya seçenekler sağlar. En hızlı çalışan
program için, hafızada tutma ve ömür, program geliştirilirken belirlenir. Bunun
için nesneler ya statik belleğe, veya yığıta konur. Yığıt mikroişlemci tarafından
doğrudan kullanılan bellek alanı olup, veriler buraya program çalışırken saklanır.
Yığıttaki değişkenlere otomatik veya hedeflenmiş değişkenler denilmektedir.
Statik bellek alanı ise belleğin sabit bir yeri olup, buraya program çalışmaya
başlamadan önce veri yerleşimi yapılır. Statik bellek alanı veya yığıt kullanımı,
bellek yazımı ve boşaltımının hızında önceliği belirler. Bu, bazı durumlar için
önemlidir. Programı yazarken nesnelerin tipi ve ömürleri ilgili tam ölçüleri
mutlaka bilmek lazımdır. Her ne kadar esnekliği azaltsa da, ölçülerin bilinmesi
zorunludur. Atık yönetimi, bilgisayar destekli tasarım ve hava kontrol sistemi gibi
genel amaçlı sorunların çözümü hedeflendiğinde, bu durum çok sınırlayıcıdır.
Nesneleri oluşturmada ikinci yaklaşım; küme (heap)denilen bellek havuzun da,
nesneleri dinamik olarak oluşturmaktır. Bu yaklaşımda, programın çalışma anına
kadar, kaç tane nesneye ihtiyacımız olduğunu, bunların ömürlerini ve tam tiplerini
bilemeyiz. Bu konulardaki kararlar program çalışırken o anki isteklerle
belirlenir.Yeni bir nesneye ihtiyaç duyulduğunda, basit bir şekilde küme (heap)
üzerinde new anahtar kelimesi ile oluşturulur. Bu nesneyle işimiz bittiğinde
bellekten silmek için, delete anahtar kelimesini kullanırız.
Çalışma sırasında bellekte saklama dinamik olarak yönetildiği için, küme de
saklama işleminin aldığı süre, yığıtta saklama işleminden daha uzundur.(Yığıt
üzerinde saklamada; tek mikroişlemci komutuyla, yığıt göstergeci aşağı iner, ve
tek komutla da geri alınır.). Dinamik yaklaşım başlangıçta nesnelerin karmaşık
olma eğiliminde olduğunu kabul eder, ve nesnelerin oluşmasında hiçbir faydası
olmayan, bellek yeri arama ve sonra orayı boşaltma gibi fuzuli işlerle uğraşır.
Bunun sağladığı esneklik daha genel programlama sorunlarında esastır.
Bir diğer unsur da nesnelerin ömrüdür. Statik bellekte veya yığıtta nesne
oluşturulduğunda derleyici nesnenin ömrünü belirler ve eceli geldiğinde otomatik
olarak onu yokeder. Bu nesne küme üzerinde oluşturulduysa, derleyici onun ömrü
hakkında hiçbirşey bilmez. C++ da programcı bu nesnenin ne zaman ortadan
kaldırılacağını programda mutlaka belirtmelidir. Ortadan kaldırma işlemini de
mutlaka delete anahtar kelimesi ile yapmak zorundadır. Bütün bu bellek yönetim
çeşitlerine ek olarak; artık toplayıcı denilen bir öge, ortama yerleştirilebilir. Artık
toplayıcı, vadesi dolan bir nesneyi derhal ortadan kaldırır. Artık toplayıcı
kullanarak program yazmak çok büyük kolaylık sağlar. Fakat gözönünde
bulundurulması gereken; bütün uygulamalar artık toplayıcının varlığını kabul
etmeli ve bunun bedelini ödemeleri gerekir. Bunlar, C++ da tasarım koşullarına
uygun düşmez , verimliliği azaltır, o nedenle C++ sisteminde bulunmaz. Fakat
üçüncü kişilerce geliştirilmiş C++ artık toplayıcıları vardır.
İstisna yönetimi :hatalar
Programlama dillerinin başlangıcından beri hata konusu, en zor unsurlardan biri
olmuştur. Bunun sebebi, iyi bir hata yönetim planı oluşturmanın çok zor olmasıdır.
Hatta bazı diller bu konuyu ya geçiştirirler, ya da gözardı ederler. Ama sorunun
çözümü genellikle kütüphane tasarımcılarına aktarılır. Onlar da birçok duruma
çözüm üretebilmelerine rağmen kolaylıkla yanıltılabilirler, ve genellikle gözardı
ederler. Hata yönetim planlarında ki en büyük zorluk, bütünüyle programcının
yeteneklerine bağlı kalınmasıdır. Eğer programcı yeteri kadar yetenekli olmayıp,
bir de acelecilikle programı çabuk bitirmek istediğin de, hata yönetimi uygulaması
kolaylıkla unutulmaktadır. İstisna yönetimi, hata yönetimini doğrudan
programlama dili ile ilişkilendirir ve hatta bazen işletim sistemine de aynı şekilde
davranır. İstisna bir nesnedir ve hata tarafından "atılır" ve istisna yöneticisi
tarafından "yakalanır". İstisna yöneticileri özel hataları yönetmek için
tasarlanmışlardır. Yanlış giden bir şey olduğunda, istisna yönetimi, işlemlemeye
paralel olan farklı bir yolmuş gibi algılanabilir. Normal çalışan koda sarkma
ihtiyacı olmadan ayrı bir işlem yolunu takibeder.Bu da kodun yazılmasını
kolaylaştırır, zira hataları bulmak için sürekli zorlanmazsınız. Bunlara ilaveten
"atılan"ın istisnası, bir işlev ya da bayrağın hatasından kaynaklanan hata dönüş
değerine benzemez. İşlev ve bayrakların hata dönüş değerleri bir işlev tarafından
belirlenmiştir. Bunların hataları gözardı edilebilir. Bir istisna gözardı edilemez, ve
bazı durumlarda çözümü garanti edilir. Sonuç olarak istisnalar kötü bir durumdan
güvenilir şekilde kurtulmayı sağlar. Programınızı hemen bitirmek yerine,
ögelerinizi doğru kullanmalı ve programın çalışmasını en düzgün şekile getirmeli,
en sağlam sistemleri üretmelisiniz. Zira sonra başınız ağırır. Burada belirtmemiz
gereken bir şeyde; istisna yönetimi NYP özelliği değildir ve genellikle NYP de,
istisna bir nesne ile temsil edilir. İstisna yönetimi NYP dan önce de vardı. Bu
bölümde istisna yönetimi çok hafif bir şekilde geçiştirildi. Daha sonra konu
ayrıntıları ile işlenecektir.
Çözümleme ve tasarım
Nesne yönelimli paradigma, programlamaya farklı bir bakış getirmiştir. Çok kişi
NYP projeleriyle çözüm üreteceği zaman, başlangıçta sorunla karşılaşmışlardır.
Herşeyin nesne olduğunu biliyoruz ve nesne yönelimli biçimde düşünmeyi
öğrendiğimizde, NYP nın üstünlüklerini kullanarak iyi ve sağlam tasarımlar
kurabiliriz.
Yöntem (çoğunlukla metodoloji olarak anılır.) süreçler kümesidir. Programlama
sorununun karmaşıklığını parçalamak için alt çözümler kullanılır. Ayrıca çok
sayıda NYP yöntemi, başlangıçtanberi formülleştirilmiştir. Bu bölümde yöntem
kullanımıyla, başarılacak olanı hissettirmeye çalışacağız.
Özellikle NYP da çok önemli olan metodoloji, birçok deneysel çalışmaya konu
olmuştur. Sorunu doğru anlamak, çözüm için uygulanacak yöntemin doğru
seçilmesini sağlar. Bu konu C++ da çok önemlidir. Yani sorunu doğru anla ki,
doğru yöntemi kullanabilesin. C++ dili, sorunların karmaşıklığını azaltan
unsurlarla donatılmıştır. Bu da karmaşık yöntemlere olan ihtiyacı azaltır.
NYPdışındaki dillerin daha karmaşık yöntemleri kullanarak çözüm ürettiği
sorunları, C++ daha basit yöntemlerle çözer.
Yöntemlerin mükemmel bir şekilde uygulanması çok önemlidir. Yöntem, bir
programı tasarlarken ve yazarken yapılan herşeydir. Yöntem, kendinize özgü de
olabilir, ve siz bunun farkında olmayabilirsiniz, çünkü bu bir oluşturma sürecidir.
Eğer üretkenliğinizden ve uyguladığınız yoldan hoşnut değilseniz, takip ettiğiniz
yolu bırakın, onun yerine bilinen çözümleri seçin, veya bunların içinden küçük
parçaları kullanın.
Geliştirme süreci boyunca ilerlerken, dikkat edilecek en önemli unsur;
kaybolmamaktır. Yapılması kolaydır. Çözümleme ve tasarım yöntemlerinin çoğu,
sorunların en büyüklerini çözmek için hazırlanmışlardır. Ama bilindiği üzere
sorunların çoğu bu kategorilere girmemektedir. Dolayısı ile, bir yöntemin
önerdiğinin kısmen küçük alt kümesiyle sorunların çözümlemesini ve tasarımını
başarıyla yapabiliriz. Bazı süreç çeşitleri ne kadar kısıtlı olduğuna bakılmaksızın,
çözüme kod yazmamızdan daha uygun olabilirler. Bu tercih bazen daha tutarlıdır.
Bulunulan aşamada, bütün ayrıntılara hakim olunamadığı için, programınızda
ilerleyemeceğiniz duygusuna kapılabilirsiniz. Bu yüzden kolayca, çözümleme
hastalığına yakalandığınızı düşünebilirsiniz. Ama bilinmesi gereken; sistem
hakkındaki bazı unsurlar, ne kadar çözümleme yaparsanız yapın, ancak tasarımda
ortaya çıkar, birçok unsurda kodlamada belirir, ve hatta bazıları program bittikten
sonra, denenirken farkına varılır. Bu nedenle çözümlemeden tasarıma hızla
geçmek, ve önerilen sistem için deneme yapmak, daha önemlidir.
Bir noktayı vurgulamalıyız; eskiden, yapısal dillerle yapılan programlarda tasarıma
geçmeden evvel her ayrıntı çözümleme aşamasında övünülerek belirlenirdi.
Veritabanı programları gibi özel işler için bu hala gerekebilir. Ama biz
programlarımızda joker tabir edilen programlarla uğraşacağımız için, çözümleme
aşamasını gereğinden fazla uzatmayacağız. Bu tarz programlarda çözümleme
aşaması için yeterli veri olmadığı için çözüm için tasarım aşamasında çözüme
giden tekrarları denemeliyiz. Zaten bu tarz sorunların çözümünde çözümleme
aşamasında fazla israrcı olmak, çözümleme hastalığına yolaçar. Aslında program
yazmak birazda risk almak demektir. Risk almaktan kastedilen; ilk yazılan
deneme, kullanılamayacak kadar berbat olabilir. Bu bir risktir. Ama bazen
kullanılabilecek kadar nitelikli de olabilir. Yani program yazma aşamasına hızla
geçmek, sağlam program yapısı oluşturmak için gereklidir. Unutulmaması gereken
tek şey; ürün geliştirmenin risk yönetiminden başka bir şey olmadığıdır.
Program geliştirmeye başlarken kaçınılmaz asgari bazı unsurlar vardır;
öncelikle;
1-nesnelerimiz neler?.(projeyi nasıl bölümleyeceğimiz bununla ilintilidir)
2-Bu nesnelerin arayüzleri neler?.(hangi iletilerin nesnelere gönderilmesi gerekli)
Bu temel bilgiler kod yazmaya başlamak için gereken asgari unsurlardır. Elbette
daha fazla veri ve belge lazım, ama yukarıdaki belirtilen iki unsur kod yazmaya
başlamak için olmazsa olmazlardır. Program yazma süreci 5 evrede incelenebilir.
İlk evre sıfırıncı evre (0.evre) olup yapı çeşidi kullanımıyla ilgilidir.
0.Evre ;plan yapma
Öncelikle süreçte hangi adımlar atılacak mutlaka karar verilmeli. Basit gibi
görünmesine rağmen (aslında herşey basit görünüyor) çoğunlukla bu kararı
programcılar programı yazmaya başlamadan evvel almazlar. Sizin de planınız önce
programın içine atlayalım ve sonra kodlamaya başlayalım ise, iyi.(Bazen böyle
yapmak iyi algılanmış bir sorun için daha uygun olmaktadır) En azından bu da bir
plandır.
Bu evre de yapılması gereken şeylerden biri de süreç yapılandırmasıdır. Ama
tamamı değil elbette. Sorun yeteri derecede anlaşılmış olduğunda bazı
programcılar, böyle yapılar kullanmaksızın programlarına başlamakta, ve sorun
çözüldüğünde, çözülmüş olur anlayışıyla olaya yaklaşmaktadırlar. Böyle bir durum
kimilerine çekici gelebilir. Usta programcılar ise program seyahati boyunca
yollarındaki kilometre taşlarını kendilerine yardımcı olarak kullanırlar. Projeyi
bitirme hedefiyle birlikte, bunun tercih edilmesi projenin sağlığına da faydalıdır.
Bunlara ilaveten projenin çiğnenebilecek kadar daha fazla parçaya ayrılabilmesi
de, programın yenilip yutulması için programcıya kolaylık sağlar. Ünlü böl,
parçala, yönet taktiği. Biraz emperyalistçe oldu ama neyse. Sorunu ortadan
kaldırıyoruz ne yapalım (!).(kilometre taşlarının fırsatları ayrıca ileride ödül olarak
geri döner, hatırlatalım)
Hikaye veya roman yazmaya kalkanlar, başlangıçta yapı kurmaya içten içe bir
direnç gösterirler. Ama daha sonra çarşafa dolanınca, kurgu oluşturmak
gerektiğine kani olurlar. Hele teknik konularda kitap yazmak başlı başına kurgu
sorunudur.
Bazılarıda, programlarını, kafalarında yarı bilinçli olarak yapılandırırlar. Siz-belkidoğrudan programınızı kodlamaya başlarsanız, o zaman belli soruları sorarak, ve
yanıtlarını bularak ara evreleri geçebilirsiniz.
Özel amaç
İnşa edilecek sistemin, ne kadar karmaşık olduğuna bakılmaksızın, bir ana amacı
olmalıdır. Yapılacak iş ve yerine getirilecek temel ihtiyaç budur. Önceden yazılı
arayüzlere,donanım yada sisteme özgü ayrıntılara,kodlama algoritmalarına veya
verimlilik sorunlarına bakarsanız, bunların hepsinin yapmakla görevli olduğu
temel, basit ana görevi görürsünüz.
Buna hollywood filmlerinde "high concept " yani anafikir deniyor. Sinan Çetin
ustanın deyişi ile yönetmenin aklındaki “cümle”. Bunu bir veya iki cümle ile ifade
etmek mümkündür. Başlangıç noktası tam burasıdır.
Anafikir konusu oldukça önemlidir. Zira yazdığınız programın sonraki gücünü
doğrudan etkiler. Aslında anafikir, misyondan başka birşey değildir. Bunu, proje
ile ilgili konularda tam emin olmadan, belirlemek gerekmez.
Projenin ilerleyen safhalarında herşey netleştikten sonra, doğru şekilde saptamaya
çalışmalıdır. Daha iyi açıklamak için, hava trafik kontrol sistemi projesini
inceleyelim. Başlangıçta anafikir olarak "kulenin programı, uçakları yönetiri"
alırsak, o zaman küçük bir havaalanında işlemlerin nasıl olacağını düşünelim;
orada sadece insan vardır, başka da birşey yoktur. Buradaki en kullanışlı model
bizim yukarıdaki çözümümüze benzememektedir. Uçak varır, boşaltır, bakıma
girer, yeniden yüklenir, ve alandan ayrılır. Bunlar sadece insan denetimiyle yapılır.
Yani anafikir buradaki duruma uymaz. Bundan dolayı doğru program için, anafikir
doğru belirlenmelidir.
1.evre:Ne yapıyoruz?
Program tasarımının, önceki nesli olan ardışık tasarımda; istenenlerin
çözümlenmesi ve sistem özelliklerinin tanımlanmasıyla işe başlanırdı.
Düzenlemeler gayet iyiydi. İstenenlerin analizi neticesinde, işlerin yapılma zamanı
ve, müşteri memnuniyetinin sağlanması için, yönlendirme listeleri yapılırdı. Sistem
özelliklerinin tanımlanması ise; istenenlerin yerine getirilebilmesi için programın
ne yapacağının (nasıl yapacağının değil) belirlenmesidir. İstenenlerin analizi,
programcı ile müşteri arasındaki bir anlaşmadır. Sistem özellikleri ise sorunun üst
düzey incelemesi olup, bir anlamda yapılabilirliğinin araştırması, ve ne kadar
zaman alabileceğinin belirlenmesidir. Bunların her ikiside kişiler arasında
hemfikirlilik gerektirirler. En iyisi bunları açıkça belirleyip-listeleyerek ve temel
diyagramlarla- zamandan tasarruf sağlamalıdır. Sizi kısıtlayan öteki sınırlamaları
daha ayrıntılı olarak belgeleyebilirsiniz. Bizim önerimiz başlangıçta
belgelendirmeleri olduğunca kısa ve özet olarak hazırlamak en iyisidir. Bunu
gerçekleştirmek, tanımı hazırlayan başkanın öncülüğünde, bir ekibin akıl
yürütmeleriyle olur. Herkesten yalvararak elde edilen şeyler değildir bunlar.
Bunları takımdaki kişilerle anlaşma yaparak satın almak, en teşvik edici olandır.
Fakat herhalde en önemlisi, bir projeyi en fazla tetikleyen şey içten gelen hevestir.
Zaten hayat dediğiniz ne ki; iki nefes, bir heves. Doğuyorunuz bir nefes
alıyorsunuz sonra sadece heves ve sonra ölüyorsunuz bir nefes geri veriyorsunuz.
Bu aşamada çözülecek sorunun özüne odaklanmak gereklidir. Sistemin ne
yapacağının belirleyin. Bunun için en faydalı araçlar kullanım durumlarıdır.
Kullanım durumları sistemdeki ana özellikleri tanımlarlar. Kullanılabilecek bazı
sınıfları ortaya çıkarırlar. Tanımlama aşağıdaki sorulara verilen yanıtlarla
oluşturulur.
--Sistemi kim kullanacak?.
--Bu aktörler bu sistemle ne yapabilirler?.
--Bu aktör bu sistemle nasıl yapar?.
--Başka biri yapıyor olsaydı bu başka nasıl çalışırdı?. veya aktörün farklı amacı
olsaydı?. (değişik yaklaşımları incelemek için)
--Sistemle bu yapılırken ne sorunlar ortaya çıkıyor?. (istisnaları belirlemek için)
Örnek olarak, otomatik para çekme makinesi tasarlanacak olunursa, otomatik para
çekme makinesinin kullanım durumları için, otomatik para çekme makinesinin
yapacaklarını belirlemek gerekir. Burada kullanım durumlarının her biri, bir
senaryoya karşılık gelir. Kullanım durumlarının hepsini, senaryolar toplamı olarak
düşünebiliriz. Senaryolardan birine şu soruyla başlayabiliriz;sistem ne yapar
eğer..? Buradaki örnekte ise otomatik para çekme makinesi, mudi hesabında para
yokken para çekmeye kalkarsa ne olur? şekline çevrilebilir. Bunları önce çizgeler
halinde planlamak, programcıya kolaylık sağlar. Böylece sistemi oluştururken,
batağa saplanmaktan kurtulunur.
Çizgedeki her çubuk unsur bir aktördür. Genellikle bu insan ya da serbest bir
unsurdur.(Bunlar başka bilgi sistemleride olabilir ATM gibi-otomatik para çekme
makinesi-). Çizili kare, sistemin sınırlarını göstermektedir. Elipsler kullanım
durumlarına karşılık gelmektedir. Bilindiği gibi, kullanım durumu; sistemin yaptığı
işe yarar eylemlerdir. Kullanım durumları ile aktörler arasındaki çizgiler ise
etkileşimlerdir. Kullanıcıya yukarıdaki şekilde gösterildiği sürece, sistemin
gerçekleniş biçimi önemli değildir.
Sistem ne kadar karmaşık olursa olsun, kullanım durumlarının o kadar karmaşık
olması gerekmez. Zaten bunun hazırlanma amacı; kullanıcıya nasıl görüneceğini
planlamaktır. Örnek olarak;
Kullanım durumları, kullanıcının sistemle bütün etkileşimlerini tanımlayarak,
istem özelliklerini üretir. Sisteminizin kullanım durumlarının tamamını bulmaya
çalışın, böylece sisteminizin yapacağını sandığınız şeyleri oluşturabilirsiniz.
Burada programcı için iyi olan konu ;ilerde geriye dönüp temel unsurları inceleyip
ana unsurlardan uzaklaşmayı önlemesidir. Yani eğer bütün kullanım durumlarını
sistemimiz için oluşturursak, bir sonraki evreye de kolaylıkla geçeriz. Herhalde ilk
seferde bütün ayrıntıları oluşturmak olası değildir,fakat bunu bilerek başlamak bile
yeterlidir. Zamanla herşey ortaya çıkar. Bu aşamada tam bir sistem tanımlaması
istenirse, biriktirme yoluyle oluşturulabilir. Yığına sahip olduğunuzda kaba bir
yaklaşımla bu evreye bir adımda başlayabilirsiniz. Sistemi birkaç paragrafta
tanımlayabilir ve daha sonra isimleri ve fiilleri belirlersiniz. İsimler aktörleri ,
kullanım durumu bağlantılarını, ya da kullanım durumlarındaki unsurları belirler.
Fiiller, aktörlerle kullanım durumları arasındaki etkileşimleri gösterir, ve kullanım
durum adımlarını tanımlar. Tasarım sırasında, isim ve fiiller nesne ve iletileri
üretirler.(kullanım durumları ara sistemler arasındaki iletişimleri de düzenlerler.
Kullanım durumları ortaya çıkmayacaksa, isim ve fiil yöntemi konu üzerinde fikir
jimnastiği yapmak amacıyla kullanılabilir).
Kullanım durumuyla bir aktörün arasındaki sınır, bir arayüzün varlığına işaret
eder. Fakat bu sınır, arayüzü tanımlamaz.
Kullanıcı arayüzü tanımlama ve oluşturmak için, ayrıntılı kitaplar vardır.bkz;
www.foruse.com
Şimdi bu noktada bir miktar cetvelleme denilen gizli sanatla ilgilenelim.
Cetvelleme programda zamanı planlamaktan başka birşey değildir. Buraya kadar
olan bölümde ne inşa edeceğiniz konusunda önbilgi edindiniz. Herhalde bu işin ne
kadar zaman alacağı hususunda da bir fikir sahibisiniz. Burada çok sayıda unsur
rol oynar. Belki çok uzun zaman alan bir plan yaparsanız, program talebinde
bulunanlar isteklerinden vazgeçebilirler.(Böylece onların kaynaklarını daha akılcı
kullanabilirsiniz. Bu iyi bir şeydir) Veya yönetici projenin ne kadar zaman
alacağına karar verir, ve sizi bu konuda uyarmaya çalışır. Fakat en iyisi; dürüstçe
baştan itibaren, temel konularda bir cetvel yapmak,kesin kararlar vermek gerekir.
Programlamada zaman yönetimini mükemmelleştirmek amacıyla, çok sayıda
çalışma yapılmıştır. Fakat hepsinin en iyisini, siz kendiniz deneyim ve
sezgilerinizle elde edebilirsiniz. Cesaretle, ne kadar zaman alacağını hissedin daha
sonra, bunu ikiyle çarpın ve yine buna % 10 ekleyin. Yaptığınız tahmin, herhalde
doğru olacak, ve herşeyin beklendiği gibi gittiğini göreceksiniz. Buradaki ikiye
katlama, bazı konuların ayrıntılarından kaynaklanmaktadır. % 10 ise cila, parlatma
ve geri kalan ayrıntılar içindir. Her türlü muhalefete karşılık, yaptığınız zaman
yönetim planı ortaya çıktığında, sorun kalmadığı görülecektir.
2.Evre ;Nasıl inşa edeceğiz?.
Bu aşamada mutlaka, sınıfları ve onların birbirleriyle ilişkilerini tanımlayan bir
tasarıma sahip olmalısınız. Sınıflar ve onların ilişkilerini açıklamada kullanılan en
iyi teknik sınıf-sorumluluk-işbirliği (SSİ) kartlarıdır. Bu yöntemin kıymeti çok alt
düzeyde teknik bilgi gerektirmesindendir.3x5 santimetrelik kartlarla işe başlayıp
doğrudan kartlara yazılır. Her kart bir sınıfı tanımlar. Kart üzerine yazdıklarımız;
1-Sınıfın adı.Sınıf adının önemi, sınıfın ne yaptığını anlamamıza yardımcı
olmasından kaynaklanır. Böylece karta bir bakışta, anlamı kavrayabiliriz.
2-Sınıfın "sorumlulukları" yani yapması lazım gelen işler. Bunu da tipik olarak,
üye işlevleri adlandırma yoluyla halledileceğini göreceğiz. Üye işlev isimlerinin,
işlevi tanıtıcı olmasına dikkat edilmeli. Fakat bunlar öteki bilgileride engelleyici
olmamalı. Süreci köklendirmek istediğinizde soruna tembel programcı gözüyle
bakmalısınız. Yani sorununuzu hangi nesnelerin çözer görünmesini istiyorsanız
onları belirlemelisiniz.
3-Sınıfın ortaklıkları; sınıfımız hangi öteki sınıflarla etkileşimde bulunmaktadır.
Etkileşim, planlı bir şekilde söylenmiş geniş anlamlı bir terimdir. Birleşme
manasınada gelir. Veya basitçe sınıfın bir nesnesi için servis yapan başka bazı
nesneler vardır denilebilir. İşbirlikleri, bu sınıfın gözlemcilerini de dikkate alması
lazımdır. Örnek olarak donanma fişeği sınıfını kimin gözleyeceğini oluşturacaksak
bunun kimyacımı yoksa seyircimi olduğunu bilmemiz lazımdır. Donanma fişeği
patladığında oluşan renk ve şekiller kimyasalın yapısını verir. Böylece talep
edicinin istediği malzeme özelliği saptanabilir.
SSİ kartlarına her şeyi yazmak isteyeceğinizden dolayı, oldukça büyük hacimde
kart oluşacağını düşünebilirsiniz. Fakat aslında kartlar küçük şekilde düzenlenir ve
sadece sınıfların kısa şekilde tanımlanmasını sağladığı gibi başlangıçta fazla
ayrıntılardan korur. Küçük bir karta sınıfla ilgili herşeyi yerleştiremediğinizi
düşünüyorsanız, yazdığınız sınıf çok karmaşık demektir.(ya çok ayrıntıya indiniz,
ya da birden fazla sınıf oluşturmanız lazım ). İdeal sınıf; karta bir bakışta
anlaşılabilmelidir. SSİ kartlarının temel fikri size tasarımın ilk aşamasına gelirken
yardımcı olmak ve büyük resmi elde edip tasarımınızı fazlalıklardan arındırmaktır.
İletişim SSİ kartlarının faydalı olduğu alanlardan biridir. En iyi şekilde gerçek
zamanlı olarak bilgisayarsız bir şekilde grup içinde yapılır. Birkaç sınıfın
sorumluluğunu herbir kişi üzerine alır (başlangıçta isim ve başka bilgi
kullanılmaz.).
Canlı olarak bir seferde bir senaryoyu oynayarak benzetim yaparsınız. Her
senaryodaki değişik nesnelere gönderilecek iletilere bu uygulamayla karar verilir.
Bu süreç boyunca ilerlerken sınıfların sorumlulukları ve yapması gereken
işbirlikleri ile ilgili kararları verirken bunlarla ilgili olan SSİ kartlarını
doldurursunuz.Bütün kullanım durumlarını geçtiğiniz de artık tasarımın ilk
aşaması bitmiş olur.
SSİ kartları kullanılmadığı önceki dönemlerde, en başarılı yolgösterici deneyimler,
daha önce hiç NYP yapmamış bir takıma, beyaz bir kağıda nesneler çizerek
getirilen başlangıç tasarımlarıyla sağlanırdı. Hangi nesnelerin hangileriyle
iletişimde bulunduğunu,bazen bazılarını silerek veya yenilerini yazarak aramızda
konuşarak kararlar alır ve tasarımda o dönemlerde oldukça da başarılı olunurdu.
Etkin olarak SSİ kartlarını beyaz tahtada göstererek yönetiyordum. Takım, tasarımı
oluşturuyordu. Takım doğal olarak projenin işlevini biliyordu. Tasarım da böylece
onlara verilen bir şey değil onların bizzat kendi malı oluyordu. Benim yaptığım
doğru soruları yönelterek onları doğru yönlendirmekti. Önkabulleri deneyerek
daha sonra takımdan bu konudaki fikirlerini alıp, gerekirse bu önkabulleri
değiştirerek yönlendirmeler devam etti. Uygulamaların en güzel yanı, takımın NYP
hakkında yaşayarak bilgilenmesiydi. Zira çoğu kez NYP bilgilenmeleri soyut
örneklerle sağlanmaktadır. Takıma en ilginç gelen tasarımın üzerinde kendilerinin
çalışmış olmaları yani tasarımın kendilerine ait olmasıdır.
Birkez SSİ kartlarıyla çalışmaya başladığınızda daha sonra birleşik modelleme
dilini kullanmak isteyebilirsiniz. Zira SSİ kartları bu geçişi çok kolaylaştırır.
Aslında buna ihtiyacınız yok ama duvara birleşik modelleme çizgesi koyarak
takımın düşünmesini sağlamak iyi bir fikirdir. Birleşik modelleme diline seçenek
olarak nesnelerin ve arayüzlerinin metin olarak ifade edilebilir. Veya kullanılan
dille hazırlanmış kodun kendisi de başka bir seçenektir.
BMD (Birleşik modelleme dili) sisteminizin dinamik modelini ayrı çizge olarak
size sunar. Bu bir sistemin ya da alt sistemlerin geçiş durumlarının etkin olduğu
koşullarda oldukça yararlıdır. Zira bunların kendi çizgelerine gerek duyulur.(örnek
olarak denetim dizgelerinde olduğu gibi). Verinin etkin olduğu sistem yada alt
sistemlerde (veritabanları gibi) veri yapılarını tanımlamak ihtiyacı vardır.
Nesneleri ve onların arayüzlerini tanımlarken ikinci evrede yapılacak şeyleri
biliyoruz. Çoğunluğu doğru olmasına karşın, az sayıda kaçak yapılanma da
olabilir. Bu kaymaları 3.evreye kadar, yani iyice bilinir hale gelene kadar, kendi
kendinize düzeltmeye çalışmayın. Şu ana kadar her şeyi onaylayalım. Yani
yapabildiğiniz kadarı yeterli.Başlangıçta NYP den yararlanarak tamamını
gerçeklemek çok iyi olsada, gerekmez. Program geliştirme süreci sırasında nesne
tasarımı 5 aşamada oluşturulur.
Nesne tasarımının 5 aşaması
Program yazımı esnasında, nesne tasarım süresi herhangi bir zaman ile kısıtlı
değildir. Nesne tasarımı daha ziyade aşamalar dizisi olarak sürer. Bu bakış açısı
başlangıçta mükemmeliyeti gereksiz kılar. Bunun yerine, nesnenin ne yaptığı
anlaşılmalı, ve zaman içindeki davranışını belirlemelisiniz. Bu bakış açısı farklı
özellikteki program tasarımlarına da uygulanır. Problem çözümüyle uğraşırken,
birden ortaya bazı program tipleri için, çözüm örnekleri ortaya çıkar. Bu çözüm
örnekleri ingilizcede pattern denilen düzeneklerdir. Nesneler içinde, anlaşıldığında
benzer örnekleri olduğu görülecektir. Bu, kullanım ve yeniden kullanım açısından
gözardı edilmemelidir.
1-nesnenin belirlenmesi
Bu aşama program başlangıcındaki çözümleme bölümünde yeralır. Nesneleri
belirlemek dış unsurları ve sınırları,sistemde yinelenen ögeleri ve en küçük
düşünce birimlerini arayarak olur. Bir sınıf kütüphanesine sahipseniz bazı nesneler
sizin için artık aleniyet kazanmış demektir. Sınıflar arasında benzerlikler varsa o
zaman ortada bir ana sınıf var demektir, bu ya kalıtımın o anda varlığına işaret
eder, ya da tasarım sürecinde kalıtım oluşturulabileceğine.
2-nesne kurulumu
Nesneyi inşa ederken, nesnenin belirlenmesi sırasında fark edilmeyen, yeni üyelere
gereksinim duyduğu görülür. Nesnenin iç gereksinimleri, öteki sınıfların kendisini
desteklemesi durumunu ortaya çıkarabilir.
3-sistem inşası
Bu aşamada birkez daha nesneden daha fazla istemlerde bulunulabilir. Bilindiği
gibi nesneler evrimleşirler. Sistemde öteki nesnelerle iletişim ve bağlantı gerektiği
zaman, sınıfların gereksinimleri değişebilir veya yeni sınıflar gerekebilir. Örnek
olarak kolaylaştırıcı veya yardım edici sınıflara ihtiyaç olabilir, çok az veya hiç
durum bilgisi taşımayan öteki sınıflara basit şekilde yardımcı, ilişimli listeler gibi.
4-sistem genişlemesi
Sisteme yeni özellikler eklemek, önceki tasarımın kolaylıkla yeni yapıya ayak
uyduramamasına yolaçar. Bu yeni özelliklerle, sistemi yeniden yapılandırmak
gerekir. Bu, yeni sınıflar ve sınıf hiyerarşileri eklenmesi demektir.
5-nesnenin yeniden kullanımı
İşte bir sınıf için gerçek anlamda yeterlilik denemesi. Birisi bir sınıfı tamamı ile
yeni bir durum için yeniden kullanmaya çalışırsa, ihtimalki bazı sorunlarla
karşılaşır. Bir sınıfı yeni programlara uyarlamak amacı ile değiştirirken sınıfın
genel prensipleri belli olmalıdır. Çünkü o zaman gerçek anlamda yeniden
kullanılabilir sınıf oluşturulur. Ama siz yine de bir sistemdeki nesnelerin çoğunun
yeniden kullanılabileceğini düşünmeyin. Mükemmel olanlar genellikle sisteme
özgüdür. Yeniden kullanılabilir tipler az yaygın olanlardır. Ve yeniden kullanmak
için daha genel sorunlara çözüm üretirler.
Nesne geliştirmede yolaçıcılar
Bu aşamalar sınıf geliştirmeye yolaçarlar. Nesneler, sınıflar kullanılarak
oluşturulur.
1-Özel bir sorun için bir sınıf oluşturalım. Daha sonra bu sınıfı başka sorunları
çözmesi için geliştirip olgunlaştıralım.
2-Sistem tasarımının büyük bölümü, gereksinim duyulan sınıfları (ve arayüzleri)
bulmak olduğunu hatırlayın. Bu sınıflar varsa, projeniz artık çok kolay demektir.
3-Başlangıçta herşeyi bilmek zorunda değilsiniz. İlerledikçe bulur ve öğrenirsiniz.
Bu bir şekilde olur.
4-Programlamaya başlayın; bazı şeylerin çalışması tasarımınızı onaylar ya da
onaylamaz. Korkmaya gerek yok.NYP öteki dillerdeki yanlışlıklar gibi herşeyi
sona erdirmez. Zira sınıflar sorunu bölmüştür. Kötü sınıflar iyi sınıfları bozamaz.
Bu nedenle kötü sınıfları düzeltmek her zaman olasıdır.
5-Her zaman basit olun. Açık kullanımlı temiz küçük nesneler, büyük karmaşık
arayüzlerden daha iyidir. Karar anları geldiğinde yaklaşımınız şöyle olmalıdır;
seçenekleri inceleyin,ve bunların en kısa ve basit olanını seçin. Zira en basit
sınıflar, hemen hemen her zaman en iyileridir. Küçük ve basit başlayın,daha sonra
sorunu daha iyi anladığınızda arayüzü genişletirsiniz. Bu arada zaman ilerler.
Böyle yapmak daha iyidir. Zira başlangıçta fazla konmuş üyeleri sınıftan sonra
atmak çok zordur.
3.Evre;çekirdek inşası
Bu aşama,denenecek kodun kaba tasarımdan, derlenmiş ve işlemlenmiş yapıya
çevrildiği ilk kısımdır. Özellikle mimari yapınızın onaylanıp onaylanmayacağını
gösterir. Bu bölüm tek geçişli bir süreç olmayıp 4. evrede de görüleceği üzere
ardışık adımlarla tekrarlanan inşa sürecidir. Amaç, çalışan bir sistemi oluşturmak
için gereken sistem mimarisinin çekirdeğini bulmaktır. Başlangıçtaki sistem
eksiklikleri sorun addedilmemeli. Burada daha sonraki eklemelerin yapılacağı bir
çerçeve yapı oluşturulmaktadır. Ve ilk kez çok sayıda sistem birleştirmeleri ve
denemeleri yapılır. Ayrıca sistemin görünümü ve gelişimi hakkında, destekleyici
arka plan bilgisi elde edilir. Ayrıca bazı krıtik riskler de ortaya çıkar. Orijinal
mimari de, sisteminizi gerçeklemeden bilinemeyecek, bazı değişiklikler ve
geliştirmeler büyük olasılıkla gerekecektir. Sistem inşası kısmı, istem
çözümlemenizin ve sistem özelliklerinizin gerçek bir sınamasıdır. Sınama
sonuçlarının, istemleri ve kullanım durumlarını onayladığından mutlaka emin
olunmalıdır. Sistem çekirdeği kararlı olduğunda, daha fazla işlev ekleyerek devam
edebilirsiniz.
4.Evre;kullanım durumlarının yinelenmesi
Çekirdek çerçeve, bir kez çalıştığında eklenen her özellik, kendi içinde küçük bir
projedir. Yineleme sırasında eklenen özellik kümesi, geliştirmenin oldukça kısa
süresini alır.
Bir yineleme ne kadar büyük olur?. İdeal olarak her bir yineleme bir ila üç hafta
sürer.(kullanılan uygulama diline bağlıdır.). Bu sürenin sonunda öncekinden daha
işlevsel, bütünleşik ve sınanmış sisteminiz olur. Özellikle ilginç olan ise
yinelemenin tek kullanım durumlu kökenidir. Her kullanım durumu bir pakettir.
Bu paketin içi birbiriyle ilişkilidir ve yineleme esnasında hepsi bir seferde sisteme
inşa edilir. Bu, sadece kullanım durumunun olması lazım gelen faaliyet alanını
değil, aynı zamanda kullanım durumu fikrinin geçerliliğini gösterir. Yazılım inşa
süreci boyunca temel geliştirme birimi olarak kabul edildiği için çözümleme ve
tasarımdan sonra, kullanım durumu uygulaması terkedilemez. Amaç olarak
belirlenen işlevsellik elde edildiğinde ya da dış istem yerine getirildiğinde ve
müşteri o anki sürümle ikna olduğun da, yinelemeyi durdurmalısınız.(yazılım
abone usulü bir iştir.)
Sürecin yinelenebilirliği nedeniyle, tek bir bitişin olmamasından, ürün
gönderildiğinde çok sayıda fırsat ortaya çıkar; açık kaynak kodlu projeler
yinelemeli olduğu için çok sayıda geriye bilgi aktarılır ve bu da programların daha
başarılı yazılmasını sağlar.
Yinelemeli geliştirme süreci birçok sebeple değerlidir. Hassas riskleri başlangıçta
ortaya çıkarır ve çözümlersiniz, müşterilerinizin fikirlerini değiştirmede, geniş
fırsatlar olur, programcının hoşnutluluğu üst düzeyde olur,ve proje çok hassas
şekilde idare edilebilir. Fakat esas önemli kazanç; herşeyi ile ürünün son
durumunu görebilen yandaşlara giden bilgidir. Bu doğal olarak yandaşların hem
desteğini hem de güvenini arttırır, ve böylece beyin uyuşturan durum irdeleme
toplantılarına ihtiyaçlar azalır, ya da kalmaz.
5.Evre;evrim
Bu aşama geleneksel geliştirme eyleminde "bakım" adıyla bilinir. Fakat aşağıdaki
bütün kavramları kapsar;"ilk adımda gerçekten düşünüldüğü gibi çalışmasını
sağlamaktan" "müşterinin belirtmeyi unuttuğu özellikleri eklemeye kadar " ve çok
daha bilinen geleneksel "hataları tespit etme ve göstermeden" "ihtiyaç hasıl
olduğunda yeni özellikler eklemeye kadar " herşeyi sayabiliriz."Bakım" terimine
çok sayıda yanlış anlamlar yüklenmiştir. Bunları farklı nitelemelerle şöyle
açıklayabiliriz; ilk önce bir program yazıyorsunuz ve gereksinim duyduğunuz
herşeyi, parçaları değiştirerek yapıyorsunuz, sonra yağlayıp pasından
temizliyorsunuz. İşte bakım. Mutlaka çok daha iyi biçimde neler olduğunu
aktarabilecek, terim ve kavramlar vardır.
Evrim kelimesini kullanıyorum. Zira "siz ilk seferde doğru olanı elde
edemeyeceksiniz, kendinize geriye dönüp değişiklikler yapabilmek için öğrenme
özgürlüğü bırakın". Sorunu daha derin bir şekilde anlayıp öğrenirken çok sayıda
değişiklik yapmak gereksinimi doğar. Doğru olanı elde edene kadar
evrimleştirirseniz, üreteceğiniz başarılı sonuç, hem kısa hem de uzun dönemde
gereken faydayı sağlar. Evrim, programınızın iyiden daha iyiye gittiği, ilk geçiş
açık şekilde belli olduktan sonra, birçok şeyi gerçekten anlamamış olduğunuz
yerdir. Ayrıca sınıflar, tek projelik kullanımdan yeniden kullanılabilir kaynaklara
evrimleşebilirler.
Doğru olanı elde etmenin manası, programın sadece istemlere ve kullanım
durumlarına göre çalışması demek değildir.Bunun manası ayrıca kodların iç
yapısının size bir anlam ifade etmesidir,herşeyin uyumlu olduğunu
hissettirmesidir,gereksiz yazımın, fazla kapsamlı nesnelerin,ya da faydasız kod
bitlerinin olmamasıdır. İlaveten mutlaka program yapınızın değişiklikleri
yaşatabileceği algısına sahip olmalısınız. Bu, programın kullanımı süresince
kaçınılmaz şekilde ortaya çıkacağından, değişiklikler kolayca ve basit şekilde
yapılabilmelidir. Bu küçük bir başarı değildir. Sadece ne inşa edeceğinizi anlamak
zorunda değil, ayrıca nasıl evrimleşeceğini de bilmek zorundasınız.(değişim
vektörü dediğimiz şey). Allahtanki NYP dilleri sürekli değişimi destekleme
konusunda üstün özelliklere sahiptirler.-Nesneler tarafından yaratılan sınırlar
yapının çökmesini engellediği gibi ayrıca değişimlerin yapılmasını sağlar.-NYP
nin en önemli yararı evrimi gerçekten desteklemesidir.
Evrim sayesinde en azından inşa etmeyi düşündüğünüz şeye yakın şeyler elde
ederiz. Sonra yaptıklarımızı sınayarak, istenenlerle karşılaştırmalar yapıp,
nerelerde eksiklikler olduğunu görürüz. Daha sonra geriye dönüp, programın
çalışmayan ya da eksik olan yerlerini belirleyip,yeniden tasarımlayarak
kodlamaları yaparız. Doğru çözüme varmadan evvel sorunun çözümüne, ya da
anlamına defalarca gerek vardır.
Evrim ayrıca sistem inşası esnasında da oluşur;istemlerinize uyanlara bakarsınız,
daha sonra istediklerinizin aslında olmadığını farkedersiniz. Sisteme, çalışırken
baktığınız da da gerçekte farklı bir sorunu çözdüğünü görürsünüz. Eğer bu tarz bir
evrimi oluşturacağınızı düşünürseniz, o zaman programınızın ilk sürümünü
mümkün olan en kısa sürede inşa etmelisiniz, böylece gerçekten istediğinize
ulaşabilirsiniz.
Belki de bilinmesi gereken en önemli şey varsayımla -tanımla-, gerçekte bir sınıfı
değiştirirseniz onun ana sınıfı ve alt sınıfları hala işlev görürler. Sınıfta değişiklik
yapmaktan korkmaya gerek yoktur.(özellikle eğer yaptığınız değişiklikleri
sınayacak, yerleşik doğruluk onaylama kümeleriniz varsa). Değişikliğin programı
durdurması gerekmez,değişime uğramış olan sınıfın, özel işbirlikçileri ya da alt
sınıflarında meydana getirdiği sonuçlardaki farklılıkta sınırlıdır.
Planların bedeli
Bir ev inşa etmeye kalktığınızda, mutlaka ayrıntılı proje ve planlar kullanacaksınız.
Ama bir iskambil destesi yada köpek kulübesi yapmaya kalktığınızda bu kadar
ayrıntılı ne plana ne de projeye gerek vardır. Çok basit şemalar bile
yeterlidir.Yazılım geliştirme ise, çok uç noktalara varmıştır. Uzun süreler boyunca
insanlar geliştirmelerinde yapılara sahip değillerdi,bu yüzden büyük projelerde
hüsrana uğruyorlardı. Bu nedenle korkutucu miktarda yapı ve ayrıntı içeren
yöntem biçimi terkedildi.Bunlar temelde büyük projeler için hazırlanır. Bu
yöntemlerin kullanılması sanki bütün zamanı belge yazmaya harcayacakmışız da,
programa hiç zaman kalmayacak endişesine yolaçar.(Çoğunluklada öyledir.).
Umudum, burada uyulması gereken orta yolu size anlatabilmektir.İhtiyaçlarınıza
uyan yaklaşımı kullanmalısınız (ve kişiliğinize). Seçtiğinizin planın ne kadar
küçük boyutlu olduğunun hiçbir önemi yoktur. Hiç plansız olmanın tersine, bazı
planlar projede çok büyük atılımlar sağlar.
Bilmenizi istediğim bir şeyde projelerin yaklaşık %50 (bazılarına göre %70) si
çöpe gitmektedir.
Kısa ve basit olan bir planı takip edip, kodlamadan önce tasarım kurgusuna
gelirseniz,çalışmayacak unsurları çok daha kolay farkedersiniz. Böylece programın
içine tam girip açıkları yakalamaya gerek olmadan, gönül rahatlığıyla her şeyi
gerçekleyebilirsiniz. Deneyimlerimin gösterdiği; iyi bir programın teknolojiden
ziyade sanata daha yakın durduğudur. İyi program havai meşgale değildir. İyi
program sadece inşası ve onarımı daha kolay olan değil, aynı zamanda anlaması ve
sürdürülebilirliği daha kolay olandır. İşte zaten parasal gücüde buradan
kaynaklanır.
Uç programlama
Tasarım ve çözümleme teknikleri ile ilgilenirken, karşılaştığım uç programlama
(extreme programming) gördüğüm en köktenci ve heyecan verici
uygulamaydı.www.xprogramming.com dan bakabilirsiniz.
UP, programlama işiyle ilgili hem bir felsefe, hemde uygulamalarda gerekli yol
göstericiydi. Programlamada yaptığı yol gösterciliklerden en önemli ikisi; "önce
sınamayı yazma" ve "ikili programlama"dır. Uç programlama aslında bütün süreç
için sağlam fikirler ileri sürmüştür. Ama biz sadece bu iki fikri uygulasak bile
oldukça başarılı programlar yazabiliriz.
Sınamayı önce yazma
Projelerde sınama, genellikle gelenek olarak en son aşamada yerine getirilen bir
işlemdi.Yani herşeyin çalıştığını gördükten sonra, emin olmak amacıyla yazılırdı.
Öncelik sıralamasında arka sıralarda yer alan ve bu işte uzman kişilerin gerçek
programcı sayılmadığı ve hatta merkezden uzak tutulduğu zamanlar hala devam
ediyor sayılabilir. Fakat zaman içinde sınama takımları yanıt vermekte
gecikmediler. Bazı programlara sızdıklarında, kırdıklarında ya da çaldıklarında
artık onlar siyah elbiseli ve siyah şapkalılar olarak düşünülmeye başlanılmıştı.
(Aslında birçok kırıcı bu duyguya sahiptir.).
Uç programlama sınamaya kodlar kadar hatta daha fazla değer verince
programlamada devrim yarattı. Aslında kodları yazmadan, sınamaları yazınca
sınanmış kodlar yazılmış oluyor ve sınamalar kodlarla ömür boyu kalıyor. Projeye
eklemlenen sınamaların tamamı her seferinde mutlaka başarılı şekilde
çalıştırılmalıdır.(Çoğunluklada günde birden fazla).
Sınamaları önce yazmanın iki çok önemli etkisi vardır;
Birincisi; sınıf arayüzünün açık bir tanımını zorunlu kılar. Bir sistemi tasarlamaya
çalışırken, araç olarak "sorunu çözecek mükemmel bir sınıf" hayal edilmesini
isteriz. Uç programlama bundan da öteye gider. Sınıfın tam olarak sınıfın
müşterisine nasıl görünmesi gerektiğini açıklar. Ve de ayrıca nasıl davranmak
zorunda olduğunu hiçbir muğlak ifadeye gerek kalmadan açık net bir şekilde
gösterir. Aslında bir sınıfın nasıl davranması gerektiği, ya da ne gibi göründüğü
hakkında sıkıcı uzunlukta yazılar yazabilir, akış çizgelerinin hepsini
oluşturabiliriz. Ama hiçbiri sınama kümeleri kadar gerçek ve işe yarar
değildir.İstek listesi bir kalıp, sınamalar ise derleyici ve çalışan program tarafından
yerine getirilen bir anlaşmadır. Bir sınıfı sınamalardan daha iyi tanımlayabilmek
çok güçtür.
Sınamalar hazırlanırken tamamen sınıf üzerine yoğunlaşıldığından, daha önce
BMD çizgeleri, SSİ kartları veya kullanım durumlarıyla düşünülürken, gözden
kaçan işlevlerin farkına varılır.
Sınamaları önce yazmanın ikinci önemli etkiside; yazılımın inşasını yaparken her
seferinde sınamaları çalıştırabilmekten kaynaklanır. Bu eylem, derleyici tarafından
yerine getirilen sınamaların öbür yarısıdır. Programlama dillerinin evrimine bu
açıdan bakılacak olursa, görülecektir ki teknolojide ki ilerlemeler gerçekte,
sınamaların etrafında oluşmaktadır. Assembly diline sadece sözdizimi açısından
bakılırdı, ama C dili bazı anlam kısıtlamaları koymuştu, ve bunlarda belli
tiplerdeki hataların yapılmasını önlüyordu. NYP dilleri çok daha fazla
anlamlandırma kısıtlamaları koydu.Ve bunlar aslında sınamalardı. "Kullanılan veri
tipi uygunmu?","işlevin çağrılışı uygunmu?"gibi sınamalar, derleyici veya sistem
çalışırken yapılan sorgulamalardı. Dilin içinde yerleşik bulunan bu sorgulamaların
neticelerini görebilmekteyiz. İnsanlar çok daha karmaşık sorgulama sistemleri
yazabilir, çok daha az zaman ve gayretle çalıştırabilirler. Başlangıçta bu size garip
gelebilir,zira ne gerek var sınama programı yazmaya diyebilirsiniz?. Bu
sınamaların anlamı :siz bazı şeyleri yanlış yapıyorsunuz, ve yerleşik
sorgulamaların güvenlik ağı ise, size sorun olduğunu söylüyor ve sorunun yerini
gösteriyor .
Programlama dilinin kendi içinde bulunan yerleşik sınamalar, belli bir yere kadar
fayda sağlarlar. Ama bazı noktalarda programcının sisteme sınama müdahalesinde
bulunması gerekir. Böylece -derleyici ve çalışma zamanı işbirliğiyle- bütün sınama
ortamı, geri kalan sınamaların eklenmesiyle program onaylamak amacıyla oluşur.
Aslında derleyici ile gelen sınama sorgulamaları sanki omuzdan bakmaya
benzemektedir. Kafadan görmek, tarafımızdan yazılan sınama sorgulamalarıyla
sağlanır. Yani sorgulamalar en baştan itibaren başlamalıdır. Bundan dolayı
öncelikle sınamalar yazılmalı ve sistemin inşa aşamalarının her birinde otomatik
olarak çalıştırılmalıdır. Bu yazdığımız sınama sorgulamaları dilin kendisi
tarafından sağlanan güvenlik ağının uzantısı olur.
En güçlü programlama dillerini kullananların farkına vardığı bir şey de, çok aşırı
denemelerle desteklenmelerinin faydalarıdır. Böylece daha sonra programcı vaktini
program hatası arayarak harcamaz. UP sınama sorgulamalarıda bütün proje
boyunca aynı işlevi görür. Yazılan sınama sorgulamalarının, programdaki sorunları
her zaman yakalayacağını bilmeniz (hataların varlığını farkettikçe eklenen yeni
sorgulamalarla) sayesinde, şüphe duymaksızın gereken büyük değişiklikleri
yapabilirsiniz. Değişiklikler sorunların bütün projenin dışına atılmasını sağlar. Bu
inanılmaz bir güçtür.
İkili Programlama
İkili programlama, bireyci yaklaşıma karşı duruştur. Çok eskidenberi programcılar,
bireyciliğin kusursuz örnekleri olarak düşünülürler. Hepsi yalnız kurt kodçulardır.
Tabii bunlar UP ortaya çıkmadan evvel olan kabullerdi. UP ortaya çıkıp her
projenin aynı iş istasyonunda iki ayrı programcı tarafından iki defa yazılması
gerekli olduğunu belirtene kadar.
İkili program yazmanın erdemi; programcının biri program kodlarını yazarken,
öbürünün yazılanlar üzerine düşünmesinden kaynaklanıyordu. Düşünen arkadaş,
büyük resmi kurguluyordu. Bu kurgu sadece sorunu değil, aynı zamanda UP nin
kurallarınıda gözönünde bulunduruyordu.İkili programlama esnasında taraflardan
biri yaptığı işten vazgeçip, öteki tarafa geçebilir. Yani düşünen kod yazmaya, kod
yazan da düşünme işlevine geçebilir. Bu tür programlama, sosyal etkinliği de
arttırır. Böylece imece usulü programlama, daha hızlı ve etkin olur.İkili
programlama sayesinde, programcıların birey olarakta deneyimleri artar.
C++ Programlamada başarı etkenleri
C++ o kadar başarılı olmuştur ki, bu nedenle C dilini NYP ye çevirmeye çalışmak
amaç olmaktan çıkmıştır. Gerçi C++ dilinin başlangıcı bu doğrultuda olmuştu.
Fakat bazı sorunların çözümünde kolaylıkla kullanılan C diline önceden yapılmış
yatırımlar yüzünden, program geliştiriciler, C diline bütünleşik bulunan C++ dilini
genellikle birlikte kullanmaktadırlar. Alışkanlıklar, NYP yi olumsuz yönde
etkilemektedir. Zira bilinen eski yaklaşımlar terkedilecek, yerine yeni fikir ve
sözdizimleri gelecek ve bunlara alışılacak. Zaten eskiye rağbet olsaydı bitpazarına
nur yağardı. Yeni dil kavramları anlaşılıp, uygulanırsa herşey daha iyi olur.
Aslında uzun dönem için doğru olan bu yaklaşımın, kısa dönemde bazı sorunları
vardır. Çünkü bazı şeyler özellikle de eski program yapısının zihni kurgusu, hala
değerli bir unsurdur.Eski kodlar o kadar değerli değillerdir. Çalışan bir C
programcısıysanız yeni bir programlama diline başlarken eski alışkanlıkların
hepsini bırakmalısınız. Zaten başlangıçta bir kaç ay bu yeni dille çok az verimli
olacaksınız.Ta ki zihniniz yeni dile uygun paradigmaya alışana dek. Eğer C dilini
bir kaldıraç gibi kullanır, yani C dilinin üzerine NYP yi inşa ederseniz,
üretkenliğiniz eskisi gibi hatta artarak devam eder. Aslında her programcının
aklında bir program modeli vardır.Bu modele yeni dilin eklenen özellikleri,
başlangıçta biraz karışıklık getirir. C++ dilinin başarısı ekonomik olmasından
kaynaklanmaktadır. Ama NYP ye geçişin hala bir maliyeti vardır, fakat C++
maliyetsiz olabilir.
C++ dilinin hedefi üretkenliği arttırmaktır. Bu amaç değişik yollarla elde edilir.
Dilin amacı programcıya mümkün olduğunca yardımcı olmaktır. Bunun için
gelişigüzel kurallardan ve ince ayrıntılardan programcı kurtarılır. C++ dili
uygulamaya özgü tasarlanmıştır; C++ dili tasarım kararları, programcıya en fazla
yararı sağlayacak şekilde düzenlenmiştir.
Daha iyi bir C dili
C diliyle program yazmaya devam etmeniz, size anlık kazanç sağlar. Çünkü C++
dili C dilinde bulunan çok sayıdaki boşluğu doldurduğu gibi tip kontrolünü ve
derleme sırasındaki çözümlemeleri daha iyi yapar.Derleyici, kullanımlarını
denetlediği için, işlevlerin tanıtımına zorunluluk duyarsınız.Saptanması zor
hataların kaynaklarından macrolar ve yedek değer atamaları olmadığından bunlar
için önişlemleyicinin varlığı gereksiz olur. C++ dilinin özelliklerinden biri olan
referans (dayanç), işlev değişkenlerinin ve geri dönüş değerleri adreslerinin
yönetilmesinde kolaylıklar sağlar.İsimlerin yönetimi de, işlev bindirimi ile daha
esnek hale getirilir.İşlev bindiriminde aynı isimle farklı işlevler kullanılır.Ayrıca
isim alanları ile de, isim denetimi sağlanır.C dilinin güvenliğini arttıran çok sayıda
küçük özellik daha vardır.
Öğrenmeye başladınız.
Yeni bir programlama dili öğrenmeye başlamadaki amaç verimliliktir. Hiçbir şirket
birdenbire verimli bir yazılım mühendisini yeni bir dil öğreniyor olduğundan
kaybetmek istemez. C++ dili C dilinin ardından geldiği için, tamamı ile farklı
sözdizimi ve programlama modeline sahip değildir. Böylece eski alışkanlıkları
kullanabileceğiniz gibi, C++ ya ait yeni özellikleri de öğrenip anladıkça zamanla
uygularsınız. Bu özellik C++ dilinin başarısındaki en önemli sebeplerden biridir.
Bunlara ek olarak var olan C kodlarınızın hepsini de C++ da, hala
kullanabilirsiniz. Ama C++ derleyicisinin özelliğinden ötürü, yeniden C++ da
derlendiğinde, gizli C hatalarıyla çoğu defa karşılaşılır.
Verimlilik
Programcının üretkenliği için işlemleme hızını ticarileştirmek, bazen daha
uygundur.Örnek olarak bir finansman modeli sadece belli bir zaman aralığı için
kullanışlı olabilir .O zaman modelin yaratılma hızı modelin işlemleme hızından
daha önemlidir.Uygulamaların çoğunluğu bir dereceye kadar verimlilik isterler.
C++ dili her zaman daha fazla verimliliğiyle, bu yönden yanıltıcı oldu.C
programcılarının daha verimli oldukları hissine sahip olmaları, onların
kullandıkları dilin daha yavaş ve temiz olduğunu anlamalarına engel olmaktadır.
Üstelik C++ da üretilen kod yeteri kadar etkin olmadığın da, gerekli düzeltmeler
için bir çok ek araçta vardır. C++ ile, C diliyle yapılabilen alt seviyeli denetimler
sağlanabildiği gibi, (assembly ile C++ içinde doğrudan program yazılabilir.) ayrıca
NYP olan C++ dili ile yazılan programların hızı aynı işlevi gören C ile yazılmış
programların hızının +/- %10 u kadardır (ve hatta çok daha yakındır). NYP için
yapılmış bir tasarım C için yapılan benzerine göre çok daha etkin ve verimlidir.
Anlaması ve açıklaması kolay sistemler
Soruna uygun tasarlanmış sınıflar sorunu daha iyi açıklar. Bunun anlamı, kodu
yazarken çözüm tanımlamasını, sorun ortamının kavramlarıyla yapıyoruz demektir.
Eskiden olduğu gibi çözüm ortamı olan bilgisayar kavramlarıyla değil. Daha üst
seviyeli düşünme sağlanıyor, ve tek satır kodla daha fazla şey yapılıyor.
Yukarıdaki açıklamanın basitliğinin öteki kazancı ise bakımdır. Programların
kullanıldıkları sürece maliyetlerini etkileyen en önemli unsuru, bakım giderleridir.
Bir program ne kadar kolay şekilde anlaşılabilirse, bakım giderleri de o derece
düşük olur.Bu ayrıca yardım belgelerinin yazımının ve sürdürülmesininin
maliyetini de azaltır.
Kütüphanelerden daha fazla yararlanma
Bir programı en kısa sürede yazmanın yolu önceden yazılmış kütüphane kodlarını
kullanmaktır. C++ da ana hedef, kütüphane kullanımını kolaylaştırmaktır. Bu da
yeni veri tiplerine (sınıflara) kütüphaneleri yerleştirerek başarılır. Üretilen yeni
sınıflar aslında C++ diline eklenen yeni tiplerdir. C++ derleyicisi kütüphanenin
nasıl kullanılacağını bildiğinden, siz kütüphanenin nasıl yapacağına değil ne
yapacağına odaklanabilirsiniz.
İsimler, programların belli bölümlerine C++ isimalanları aracılığı ile
elkoyduklarından, programınızda bulunan C deki isimlerle çatışmaksızın
istediğiniz sayıda kütüphane kullanabilirsiniz.
Kaynak kodların yeniden kullanımı için kalıplar
Kaynak kodlarında değişiklik yaparak yeniden etkin şekilde kullanılabilen sınıf
tipleri vardır. C++ daki kalıp (template)
özelliği kaynak koddaki değişimi otomatik olarak yapar. Kütüphane kodunun
yeniden kullanımını sağlayan bu özellik onu çok güçlü bir araç haline getirmiştir.
Tasarımladığınız bir tip, kalıplar kullanarak çok sayıda öbür tiplerle de,
uğraşmadan sorunsuz çalışabilir. Kalıplar (template) çok yararlıdırlar, ve ayrıca
kodun yeniden kullanımıyla ilgili karmaşayı müşteri programcıdan saklar.
Hata yönetimi
C dilinde hata yönetimi en fazla adı çıkan konudur. Genellikle gözardı edilir yada
parmaklar çapraz yapılıp iş allaha havale edilir. Eğer çok büyük ve karmaşık bir
program inşa ediyorsanız, hiç bir şey farkına varılamayan kenarda kalmış bir
hatadan daha kötü olamaz. Çünkü hatanın nereden kaynaklandığı ile ilgili, hiç bir
ipucu yoktur. Bunun için, ileride anlatacağımız C++ da hata yönetimi kısmını iyi
okuyun.
Büyük çaplı programlama
Çok sayıdaki programlama dili, programların boyut ve karmaşıklığı açısından, iç
sınırlamalara sahiptirler. Örnek olarak BASIC dilini ele alalım; bu dil belli
sorunların çözümlenmesinde çok hızlı sonuç verir, fakat sorun birkaç sayfadan
uzun olduğunda, ya da dilin normal sorun ortamının dışında kaldığında, dilin kendi
iç sınırlamaları ortaya çıkar. Bu aynı, yoğunluğu çok fazla olan ortamda yüzmeye
benzer. C dilinde de doğal olarak bazı sınırlamalar vardır. C dilinde program kod
satırları 50.000 i aştığında, isim çakışmaları ortaya çıkar. Böylece işlev ve
değişkenler etkin bir şekilde kullanılamaz olur. Başka bir sorunda, C dilin de
bulunan küçük deliklerden kaynaklanır. Bilindiği gibi, büyük bir program da
gömülü, gizli duran bir hata da, bulunması en zor hatalardandır. Aslında
kullandığınız dilin sizi ne zaman hataya sevkedeceği hakkında kesin çizgi yoktur.
Olsaydı o dili kullanmaktan, anında vazgeçerdiniz. Yoksa BASIC programım çok
fazla büyük oldu, programımı C dilinde tekrar yazmak zorundayım demezsiniz.
Bunun yerine birkaç kod satırı ekleyerek yeni bir özellik oluşturmak en iyisidir.
Böylece maliyetlerin aşırı artmasını engellemiş olursunuz.
C++ dili, büyük çaplı programlamaya göre tasarlanmıştır. Bunun anlamı;
C++dilinde, büyük ve küçük programların yazılmasının herhangi bir sınır
olmaksızın gerçeklenmesidir. Mutlaka "merhaba dünya" tarzı bir program
yazarken NYPye, şablonlara ya da isimalanlarına gerek yoktur. Bunlar
gerektiğinde kullanılsın diye varlar. Ve son olarak C++ derleyicileri, hata üreten
kodları büyük ve küçük programlarda ısrarla izler.
Geçiş uygulamaları
NYP dilini satın aldığınızda sonraki sorununuz; yöneticim, meslektaşım,bölümüm
nesneleri kullanmaya nasıl başlayabilirler? olacaktır. Önce siz kendiniz yeni bir
dili öğrenmeye başladığınızda nasıl davrandığınızı düşünün. Bundan evvel eğitim
ve örneklere başlanmıştı, daha sonra deneme projesi geldi, böylece yeni dilin
uygulamadaki temelleri çok fazla şaşırtıcı olmadan öğrenildi. Daha sonra gerçek
dünya proje uygulamalarına sıra geldi. Böylece öğrenilenler kullanılabilir oldu.
Aslında ilk projeler sırasındada öğrenmeler devam etti, yani okumalar, uzmanlara
sormalar ve meslektaşlarla deneyimleri paylaşmak gibi. Anlatılanlar, C dilinden
C++ diline geçiş sırasında yaşanması gereken deneyimlerle elde edilmişlerdir.
Şirketin bir dilden ötekine geçmesi grupta bazı hareketlenmelere neden olacaktır,
fakat her adımda birilerinin yardımı alınır.
Yol göstericiler
Aşağıda NYP ve C++ ya geçiş yaparken gözönünde tutulması gereken bazı yol
göstericiler verilmiştir.
1-Eğitim
İlk adım bir çeşit eğitimdir. C diline şirketin yaptığı yatırımları gözönünde
bulundurarak, yeni dilin çoklu kalıtım yapısının nasıl çalıştığının şaşkınlığı
sürerken eskileri 6-9 ay arası kullanmak akıllıca olur. Önce yeni dilin temellerini
öğretmek için hevesli küçük bir grup seçilir,grubun kendi içinde iyi anlaşması
önemlidir. Bu grup eğitimlerine devam ederken ağdaki çalışmalarını da
sürdürürler.
Başka bir seçenekte, bütün şirketin elemanlarının hepsini tek seferde aynı seviyeye
getirecek eğitimi vermektir.Burada strateji yöneticileri için genel, proje insaatçıları
için tasarım ve programlama eğitimleri verilir. Özellikle küçük firmalar için ya da
büyük bir firmanın belli bir bölümü için böyle kökten değişimler tercih edilmelidir.
Bazen de maliyetlerin yüksekliğinden dışarıdan bir danışmanın önderliğinde bir
proje yaparak proje düzeyinde eğitim alınır. Bu eğitimden sonra proje takımı
şirketin geri kalanlarına aynı eğitimi aktarırlar.
2-Az riskli proje
Önce az riskli projelerle ve hata yapılmasına izin vererek başlayın. Bir miktar
deneyim kazandıktan sonra öbür ilk projeleri ya ilk takımın elemanlarına yazdırın
ya da bu takımın elemanlarını NYP danışmanı olarak kullanın.Yazılan ilk program
hemen ilk seferde çalışmayabilir. Bunun firma için hayati derecede önemi yoktur.
Basit,tutarlı ve eğitici olmalıdır. Bunun anlamı; şirketteki öteki programcılar içinde
anlamlı sınıflar oluşturulmasıdır. Zira bu programlarla eğitim almakta ve C++
dilini öğrenmekteler.
3-Başarılı olanı kullanma
Çözüme başlamadan önce iyi NYP tasarım örneklerini araştırmak gerekir.
Sorunumuzu daha önce başka bir programcının çözmüş olma olasılığı her zaman
mevcuttur. Eğer tam uyan bir çözüm olmayıp yakın bir çözüm varsa,öğrendiğimiz
soyutlamaları uygulayarak üzerinde değişiklik yapar ve gereksinimlerimizi
karşılayacak hale getiririz. Bu, tasarım düzenekleri (design patterns) konusu olup
ileride açıklanacaktır.
4-Sınıf kütüphanelerini kullanma
NYP programlamaya geçmedeki en ekonomik unsurlardan biri, sınıf kütüphaneleri
olarak bulunan hazır kodların kolaylıkla kullanılabilmesidir. En kısa uygulama
geliştirme süreci; hiçbir kodun yazılmak zorunda olunmadığı durumdur. Bu sırada
main( ), kütüphane rafından alıp oluşturduğu nesneleri kullanır. Bazı yeni
programcılar bunu anlayamamaktadırlar, kütüphane sınıflarını farkedememekte ve
hatta aynılarını yazmaya kalkmaktalar. NYP de başarı, var olan kodları en iyi
şekilde kullanmayla gelir. Kod arayıp incelemelerde bulunmak, C++ programcısı
için başarının anahtarıdır.
5-Varolan C kodlarını C++ da yeniden yazma
Önceden varolan C kodları, C++ derleyicisi ile derlendiğinde çok sayıda (ve
belkide anormal) hata üretmesine rağmen, eski kodlardaki hataları görme
bakımından çok yararlıdır. Aslında zamanı en iyi kullanmanın yolu; var olan
çalışan kodları alıp, C++ da tekrar yazmak değildir.(kullanmak zorındaysanız C
kodlarını C++ sınıflarının içine atabilirsiniz). Özellikle yeniden kullanım amacıyla
kodlar bozulup yazılarak bir miktar yarar sağlanabilir. Başlangıçtaki ilk projelerde,
projeniz yeni bir proje değilse, bu yöntemin getirdiği üretkenlik artışı çok fazla
değildir. C++ dili, bir projede düşünce aşamasından gerçekleme aşamasına kadar
alıp kullanıldığında çok başarılı olur.
Yönetim engelleri
Yöneticiyseniz göreviniz; takımınıza kaynak bulmak, takımın başarısının önündeki
engelleri kaldırmak,genellikle de takımınızın en hoşlandığı, üretkenliği arttıran
ortamı sağlamaktır. Böylece takım, sizden istenen mucize sonuçları üretir. C++ ya
geçiş, yöneticinin görevlerinin hepsini yapmasını gerekli kılar. Maliyet
düşünüldüğü kadar çok olmazsa, sonuç mükemmel olur. C program takımı için
C++ diline geçmenin maliyeti, öteki NYP dillerine geçmekten daha ucuza mal
olmasına rağmen, bedava da değildir. Şirket içinde, C++ dili satın alınmadan
evvel, değişik engellerin varolduğunun bilinmesi lazımdır.
Başlangıç maliyeti
C++ diline geçişin maliyeti, C++ derleyicisinin işlemleme maliyetinden daha
fazladır.(kaldı ki en iyi derleyicilerden olan GNU C++ derleyicisi bedavadır.).
Eğitime yatırım yapılırsa orta ve uzun dönem maliyetleri en aza indirilmiş olur.(ilk
proje de bir danışman tutsanız bile). Sorunu çözecek sınıf kütüphanelerini satın
alma maliyeti, aynı kütüphaneleri takımınızın inşa etme maliyetinden daha
düşüktür. Bütün bunlar masraf çıkaran maliyetler olup önerileri gözardı
etmemelidir. Başlangıçta yeni bir dil öğrenmekten kaynaklanan üretkenlikteki
azalışta gizli maliyetlerdendir. Eğitim ve danışmanlık hizmetleri mutlaka bunları
azaltıcı unsurlardır, ama yine de esas olan takım elemanlarının kendi çabalarıyla
yeni teknolojiyi kavramalarıdır. Bu süreçte hatalar olacaktır ve üretkenlikte
azalacaktır. Aslında bilinen bir gerçekte şudur; bilgilendiren hatalar, öğrenmenin
en kestirme yoludur. Hatta o zaman bile, programlama sorunlarının bazı tiplerinde,
doğru sınıflar ve doğru geliştirme ortamıyla, C++ yı öğrenirken daha üretken
olmak mümkündür.(çok sayıda hata yapmayı ve daha az kod yazmayı göze alarak).
Üretkenlik artışı C dilinde kalmış olduğunuz varsayılarak ortaya çıkmaktadır.
Başarım (performans) unsurları
Sık rastlanılan sorulardan biri; "NYP benim programımı oldukça büyük ve yavaş
yapmazmı?". Sorunun yanıtı: "duruma bağlı". En bilinen öbür NYP dillerinin
zihinlerdeki tasarımları, ince eleyip sık dokumadan ziyade hızlı üretim ve
deneylere yaslanarak oluşturulmuştu. Böylece program hacminin artmasına ve hız
düşüşüne neden olmuşlardı.C++ dili ise, üretkenliği arttırmak için hazırlandı. Hızlı
program üretmeye odaklanıldığında, verimliliği gözardı ederek, bazı unsurları
devreden çıkarıp bu sağlanabilir. Yan sanayinin ürettiği kütüphaneleri
kullanıyorsanız, bunlar üreticilerinden iyileştirilip geldiği için, hızlı program
geliştirmede herhangi bir değişiklik yapılmaz. Yeteri kadar hızlı ve küçük,
hoşlandığınız bir sisteminiz varsa herşey tamam demektir. Yok eğer böyle değilse,
hızölçer aracıyla ayarlara bakılır, C++ da yerleşik özelliklerin basit
uygulamalarından başlayarak hızlanmalar sağlanabilir. Yarar sağlamazsa,
yapılabilecek başka değişiklikleri araştırmalıyız. Yapılacak ilk değişiklikler, hiçbir
kod tarafından kullanılmayan sınıflardaki değişikliklerdir. Başka yaklaşımlar da
sorunu çözemiyorsa yapılacak tek şey; tasarımı değiştirmektir. Başarım gerçeği,
tasarımın hassas unsuru olup temel tasarım ölçütüdür. Başlangıçta hızlı
geliştirmeyle bunu elde etme, en kazançlı olandır
Daha önce de belirttiğimiz gibi C ve C++ arasında program hacmi ve hızları
açısından +/- 10% fark vardır, veya çok daha az. Neredeyse başabaş denilebilir.
Belirtmemiz gereken C++ da, program boyutu ve hızını geliştirmek için elimizdeki
olanaklar, C diline göre çok fazladır. Zira C++ nın tasarımı, C nin tasarımından
çok farklıdır.
C ve C++ arasındaki hız ve boyut karşılaştırmalarının belgeleri, anlatılanlardan
kaynaklandığı için burada bırakmak en iyisidir. Kaç kişinin firmaya aynı proje için
C veya C++ dilini önerdiğine bakılmaz, zira hiçbir firma böyle bir araştırma için
para harcamaz. Eğer çok büyük ve konuyla ilgili bir proje değilse. Öyle bile olsa
parayı harcayacak daha uygun yerler kolaylıkla bulunabilir. Aslında C (ardışık
akışlı dillerden) den C++(NYP dillerine ) diline geçiş, programcıların bireysel
üretkenliklerinin itici gücü, kendi deneyimleriyle sağlanır. Söylenebilecek en temel
unsur budur.
Yaygın tasarım hataları
Takım NYP ve C++ programlamaya başladığında, programcılar çok bilinen bazı
hatalar da yapmaya başlarlar. Başlangıçtaki projeler de, tasarım ve uygulama
sırasında, uzmanlardan geriye bilgi aktarımının az olması, program geliştirme
sürecinde uzman olmaması, veya şirket içinde uzmana direnç gösterilmesi, hataları
arttırır. Sürecin başlangıcında NYPyı iyi anladığını sanıp, eğitimden vazgeçmek,
çok rastlanır. Deneyimli programcılar için çok belli olan bir şey, acemiler için uzun
tartışma konusu olabilir. Bu hastalıkla başetmenin en iyi yolu dışarıdan eğitimci,
danışman uzmanlar tutmaktır.
Bu tür tasarım hataları yüzünden C++ dan vazgeçmeye, kolaylıkla karar
verilebilir.Vazgeçme, C ile uyumlu olmasındandır. Ama aslında esas gücü de
buradan gelir. C kod olarak derlenebilme mucizesini başarabilmek, dillerin bazı
ortak yanlara sahip olmasını gerektirir. Anılan uzlaşı noktaları dilin karanlık
köşeleridir. Gerçek olan, tüm bunlar, dilin öğrenilme alanlarının çoğunu kapsar.
C++ ile çalışırken bazı tuzak çukurlar olduğunu farkedeceksiniz. Biz bundan
sonra, bu tür güvenlik eksiklerini yeri geldikçe size bildireceğiz. Burada genel bir
uygulamayıda açıklayalım; C++ derleyicileri, hem C hem de C++ ile yazılmış
kaynak kodları derleyebilir. C derleyicileri ise sadece C kaynak kodlarını derler.
Yaygın uygulama bu yöndedir.
ÖZET
Programlamanın, ardışık akışlı ve NYP olmak üzere iki ayrı biçimi olduğu
düşünülerek C ve C++ anlatılmıştır.
Bu bölümde C++ ve nesne yönelimli programlamanın kapsama alanını göstermeye
çalıştık. Özellikle, C++ ile beraber, nesne yönelimli programlamanın farkını, NYP
yöntemlerinin temel fikirlerini, ve en son olarakta şirketiniz, NYP ya da C++
diline geçtiğinde karşılaşacağınız durumları açıklamaya çalıştık.
NYP ve C++ herkese göre olmayabilir. Önce gerekenlerin doğru belirlenmesi çok
önemlidir.Ve bu gerekenleri C++ veya öbür sistemlerden hangisinin, en iyi şekilde
yerine getirdiğine karar vermelidir.(şu anda kullandığınızı da seçebilirsiniz).
Tahmin edilebilir gelecekte, çok özel gereksinimlerinizi biliyorsanız, ve özel
koşullar C++ tarafından size sağlanamıyorsa, başka seçenekleri de
araştırabilirsiniz.Bunların arasından PYTHON, JAVA veya Tcl/Tk tercih
edilebilir. Sonunda C++dilini tercih etseniz bile, var olan seçenekleri en azından
öğrenmiş olursunuz. Ve ayrıca seçtiğiniz yol hakkında da net bir görüşünüz olmalı.
Ardışık akışlı programlar, veri tanımları ve işlev çağrılarından oluşmuştur.Bu tür
bir yapıyı anlayabilmek için biraz çabalamak gerekir.İşlev çağrılarına bakarken, alt
seviyeli temel program yapısını zihinde canlandırmak lazımdır.İşte bu sebeple,
ardışık akışlı program tasarlarken, ara açıklamalara ihtiyaç vardır.Bu açıklamalarda
genellikle soruna yönelik olmayıp,bilgisayarın bizzat kendisiyle yani çözüm
ortamıyla ilgilidir.Bu tür programları karışık gösterende bilgisayar donanımının
işin içine girmesidir.
Genel olarak C dilinden farklı yapıda oluşturulan C++ dili, (aslında C++, C dili
değildir ve yapılanması tamamen farklıdır, sadece kolaylık olması için C++
derleyicileri C programlarını da derleyebilmektedir. Objective C dili ise C diline
nesne yönelim eklenmesi ile oluşturulmuştur) insanların çoğu tarafından daha
karmaşık bir main() yapısına sahip olduğu sanılır.Aslında iyi tasarımlanmış bir
C++ programı kendi eşdeğeri C programına göre hem basit, hem de anlaşılması
çok daha kolaydır. C++ da gördükleriniz, sorun ortamının nesne tanımlarıdır,
(bilgisayarın belirtimi değil) ve nesnelere gönderilen iletilerde ortamdaki
etkinliklerdir. İyi tasarımlanmış bir NYP programının en hoş yanı, programı
okuduğunuzda onu çok iyi anlayabilmenizdir.Ve genellikle de sizin yazdığınız çok
az kod olacaktır, zira kullanıma hazır, sorununuza uygun, çok sayıda kütüphane
bulunmaktadır.
• 2. BÖLÜM
Nesne Oluşumu ve Kullanımı
Bu bölümde C++ sözdizimi, program inşa bilgileri açıklanacak, ve
anlatılanlar kullanılarak, basit nesne yönelimli programlar yazılıp
çalıştırılacaktır. Takip eden bölümde de, C ve C++ sözdizim ayrıntıları
ile incelenecek.
Böylece, C++ da nesnelerle program yapmanın tadını alacaksınız. C++
ile neden coşkuyla program yapılmak istendiğini anlayacaksınız.
Buralarda anlatılanlar şimdilik yeterli olmalı zira, 3. bölüm C dilinin
bütün ayrıntılarını da içerdiğinden, biraz yorucu olacak.
Kullanıcı tanımlı veri tipi olan sınıf (class), C++ dilini ardışık akışlı
öteki dillerden ayırır. Sizin veya başka birinin oluşturduğu sınıf, özel
bir sorunu çözen yeni bir veri tipidir. Sınıf bir kez oluşturulunca, onun
nasıl inşa edildiğine veya nasıl çalıştığına bakılmaksızın, başkaları
tarafından da rahatlıkla kullanılabilir. Bu bölümde sınıflar, programda
ki öteki yerleşik veri tipleri gibi anlatılacaktır.
Başkaları tarafından oluşturulan sınıflar, kütüphaneler halinde
paketlenerek sunularlar. C++ derleyicileri ile birlikte gelen sınıf
kütüphanelerinin birçoğunu kullanacağız. iostream kütüphanesi
özellikle önemli olup, dosyadan klavyeden veri okumaya, dosyaya ve
ekrana veri yazmaya olanak sağlar. Ayrıca çok kullanışlı string sınıfı
ve vector kabı (container), standard C++ kütüphanesinde
bulunmaktadır. Bölümün sonunda, hazır olarak gelen sınıf
kütüphanelerini kullanmanın yararlarını göreceksiniz.
İlk programınızı yapabilmek için, inşa uygulama araçlarını iyi
kavramanız gerektiğini de hatırlatalım.
Dil çevirme süreci
Bütün programlama dilleri insanların anlayabildiği biçimlerden
(kaynak kodlar), bilgisayarların anlayabildiği biçimlere (makine
kodları) çevrilirler. Bu aşama programcının programını kolaylıkla
yazabilmesi için gerekmektedir. Kaynak kod yazmak, makine kodu
yazmaktan çok daha kolaydır. Çevirme işlemi çeviriciler tarafından
gerçeklenir. Çeviriciler genel olarak ikiye ayrılırlar; yorumlayıcılar
ve derleyiciler.
Yorumlayıcılar (interpreters)
Yorumlayıcı, kaynak kodları etkinliklere çevirir (bunlar bir grup
makine kodudur) ve hemen bu etkinlikleri işlemlerler. BASIC en ünlü
yorumlayıcılı dildir. Bilinen BASIC yorumlayıcıları her seferinde bir
satır çevirir ve işlemler ve daha sonra çevrilen o satırı unutur.
Tekrarlanan kodları yeniden çeviren yorumlayıcılar, bu nedenle
yavaşlarlar. BASIC dili, hızlanmak amacıyla derlenebilmektedir de.
Günümüz de modern yorumlayıcılar, PYTHON da olduğu gibi, önce
tüm programı ara bir dile çeviriyorlar, sonra, daha hızlı bir
yorumlayıcı ile işlemliyorlar.
Yorumlayıcıların çok sayıda üstünlükleri vardır. Yazılı koddan
işlemlenmiş koda geçiş hemen olmaktadır. Hataların derhal
belirlenebilmesi ve kaynak kodların orada bulunması yorumlayıcıları
hata yönetimi bakımından da özel yapmaktadır. Yorumlayıcıların
üstünlükleri, programların hızlı geliştirilmesi (programın çalıştırılması
değil) ve programla kolay iletişim de görülür. Örnek olarak; Tcl/Tk
gibi yorumlayıcılı dil ile, çok hızlı grafik arayüzü geliştirip, bunu C++
ile geliştirilen ana programa eklemlemek oldukça sık rastlanılan bir
uygulamadır.
Büyük programların geliştirilmesi sırasında, yorumlayıcılı diller
birçok katı sınırlamalara sahiptirler (Python bunlardan istisna
gözükmektedir). Kodların işlemlenmesi için yorumlayıcı (kısıtlı hali
bile olsa) her zaman bellekte bulunmak zorundadır, ve hatta en hızlı
yorumlayıcı bile kabul edilemez hız sınırları getirebilir.
Yorumlayıcıların çoğu, kaynak kodun tamamının bir seferde,
yorumlayıcıya getirilmesini isterler. Bu da sadece bellek
sınırlamalarına değil, aynı zamanda da bulunması zor hatalara yolaçar.
Tabii ilgili dil, farklı kod parçalarının etkilerini yörelleştirebilen
kolaylıklar sağlıyorsa, sorun ortadan kalkabilir.
Derleyiciler (compilers)
Derleyici kaynak kodu, doğrudan assembly veya makine komutlarına
çevirir. Derleyicinin doğal ürünü makine kodlarını içeren dosya ya da
dosyalardır. Buradaki işlemler, kapsamlı bir süreç olup, birden fazla
adımdan oluşur.Yazılı kodlardan çalışır bir koda geçiş, derleyici ile
oldukça uzun sürer.
Derleyici yazarının zeka keskinliği, o derleyiciden üretilen
programların boyut ve hızını belirler. Kullanılan bellek alanı ve hız,
derleyicilerin seçilme nedenlerinin başında gösterilselerde, çoğu kez
en önemli nedenler değillerdir. Bazı programlama dillerinin (örnek
olarak;C dili ) yapısı programı birbirinden bağımsız parçalar halinde
derlemeye izin verir. Daha sonra birbirinden bağımsız derlenmiş
parçalar çalıştırılabilir program haline getirilmek için ilişimci (linker)
denilen araçla birleştirilirler. Bu süreç ayrık derleme olarak
adlandırılır.
Ayrık derlemenin çok sayıda yararı vardır. Bunlardan bazıları;
programın tamamının bir seferde derlenmesi derleyici sınırlarını
aşabilir, ayrık derleme ile bu engellenmiş olur, veya derleyici düzeni
sadece parçalı derlemeye uygundur. Programların her seferde bir
parçası inşa edilip sınanır. Bir defa da bir parça çalışır, saklanır ve inşa
bloku olarak işlemlenir. Sınanmış ve denenmiş parçalar öbür
programcıların kullanımına sunulmak için kütüphanelere toplu olarak
yerleştirilebilir. Herhangi bir parça oluşturulurken öteki parçaların
karmaşıklıkları gizlenir. Bütün bu özellikler çok büyük boyutlu
programların yazılabilmesini sağlar.
Derleyicilerin hata önleme yetenekleri zaman içinde önemli düzeyde
gelişmiştir. İlk derleyiciler, zamanında, sadece makine kodları
üretmekteydiler, programcılar programda işlerin nasıl gittiğini görmek
için, print deyimini programlarında sıksık kullanıyorlardı. Fakat bu
işlem her zaman istenen sonucu vermiyordu. Modern derleyiciler ise
çalışan programın içine kaynak kod hakkında bilgi
yerleştirebilmekteler. Yerleştirilen bu bilgiler programda tam olarak ne
olduğunu göstermek için, kaynak düzeyinde bakımcılar tarafından
kullanılırlar. Bakım işlemi kaynak kodun gelişimi izlenerek yapılır.
Bazı derleyiciler, derleyici hız sorununu, bellekte derleme ile aşarlar.
Derleyicilerin çoğunluğu dosyalarla çalışırlar, derleme sürecinin her
aşamasında dosyaları okur ve yazarlar. Bellekte derleme yapan
derleyiciler derleme programını RAM de tutarlar. Bu tür derleme
uygulaması, küçük programlarda yorumlayıcılar kadar hız sağlar.
Derleme süreci
C ve C++da program yapmak için derleme sürecindeki araçları ve
aşamaları bilmek gerekir. Bazı diller (özellikle C ve C++) derlemeye
kaynak kod üzerinde, önişlemleyici (preprocessor) çalıştırarak başlar.
Önişlemleyici basit bir programdır ve görevi; kaynak kodlardaki
kalıpları programcının yazdığı kalıplarla değiştirmektir. Programcının
yazdığı kalıplarda önişlemleyici yönergeleri kullanılır. Önişlemleyici
yönergeleri yazılanı saklamak ve okunabilirliğini arttırmak amacıyla
kullanılırlar. Kitabın ilerleyen bölümlerinde, C++ da program tasarımı
aşamasında, önişlemleyicinin neden olabileceği hatalar yüzünden çok
fazla kullanılmamaya çalışıldığı görülecektir. Önişlemden geçmiş kod,
çoğunlukla bir ara dosyaya yazılır.
Derleyiciler genellikle işlerini iki aşamada görürler. İlk aşama,
önişleme tabi olmuş kodları kısımlara ayırarak gerçeklenir. Derleyici
burada kaynak kodu küçük birimlere ayırır ve ağaç denilen yapı
biçiminde yeniden düzenler. A+B gibi açıklamayı; "A","+",ve "B"
yapraklı ağaç olarak ele alır.
İlk ve ikinci aşamalar arasında, kodun boyutunu azaltmak ve hızını
arttırmak için bazen, genel iyileştiriciler (global optimizers)
kullanılmaktadır.
İkinci aşamada, kod üreteci ağaç yapılanmasını kullanarak, ya
assembly kodları, ya da makine kodlarını üretir. Kod üreteci assembly
kod ürettiyse daha sonra mutlaka assembler çalıştırılmalıdır. Her iki
halde de son ürün, uzantısının sonunda .o ya da .obj bulunan hedef
(object) modüllerdir. İkinci aşamada assembly kod ihtiva eden yapıda
assembly kodları azaltmak amacıyla, özel tip iyileştiriciler
kullanılmaktadır. Nesne manasına da gelen "object" kelimesinin
belirttiği; "büyük kod parçası" anlamı yüzünden, aynı kelimenin iki
değişik anlamda kullanılması yapay bir talihsizliktir. Kelime, nesne
yönelimli programlamadan çok önce kullanıma girmiş olup, ilk önce
büyük kod parçası anlamını taşıyordu. Nesne yönelimli
programlamada taşıdığı "sınırları olan unsur" anlamı, derleme işlemi
incelendiğinde, nesne amaç olduğu için , her iki haldede aynı manayı
taşıyor kabul edilebilir.
İlişimci (linker) hedef modülleri, işletim sisteminin yükleyip
çalıştırabilmesi için, biraraya getirip çalıştırılabilir program haline
getirir. Bir hedef modüldeki işlev başka bir modüldeki işleve veya
değişkene referans olduğunda ilişimci bu referansları çözümler.
Derleme sırasında bulunması gereken dış işlev ve verileri emniyete
alır. İlişimci ayrıca başlatma etkinlikleri için özel bir hedef (object)
dosya eklemesi de yapar.
İlişimci kütüphane denilen özel dosyaları da referanslarını
çözümlemek için inceler. Kütüphane tek dosya içinde çok sayıda
hedef modül barındırır. Kütüphane oluşumu ve bakımı kütüphaneci
denilen bir program tarafından sağlanır.
Statik tip incelemesi
Derleyici ilk aşamada, tip incelemelerinde bulunur. İşlev
değişkenlerinin doğru kullanılması için yapılan tip denetimleri,
program hatalarının birçoğunu engeller. Programın çalışması sırasında
değilde, derleme esnasında tip denetimi yapılması, statik tip denetimi
olarak adlandırılır. JAVA gibi bazı nesne yönelimli diller, bir kısım tip
denetimlerini programın çalışması esnasında gerçeklemektedir. Bu tür
tip denetimine, dinamik tip denetimi denilmektedir. Dinamik ve statik
tip denetimleri birarada kullanılırsa, statik denetimin tek başına
sağladığından, çok daha yararlı olur. Tabii bunlar programa eklenen
külfetlerdir.
C++ dili statik tip denetimini kullanır, çünkü; C++ kötü işlemlerde
çalışma sırasında özel destek kabul etmez. Statik tip denetimi,
derleme esnasında programcıyı yanlış kullanılan tipler hakkında
bilgilendirir. Bu sayede programın çalışma hızı en üst düzeye çıkarılır.
C++ dilini öğrenirken farkedeceğiniz gibi, C dilinin çok tanındığı
yüksek hız, üretim tabanlı programlama, dil tasarım kararlarında aynen
geçerlidir.
C++ nın başka bir özelliğide, statik tip denetimini
engelleyebilmenizdir. Eğer birkaç kod yazmaktan üşenmezseniz,
dinamik tip denetimi de yapabilirsiniz. Statik tip denetimi özellikle
gerçek zamanlı işletim dizgelerinde büyük önem taşır. Gerçek zamanlı
işletim dizgeleri savunma sanayi, uzay uygulamaları, nükleer teknoloji
ve daha birçok hassas uygulamanın olmazsa olmazıdır. Windows,
Linux benzeri işletim dizgeleri gerçek zamanlı işletim dizgeleri
değildir. Linux için gerçek zamanlılığı sağlayan özel eklentiler
bulunmaktadır (rtlinux, rtai gibi).
Ayrık derleme araçları
Büyük projelerin inşasında ayrık derleme özellikle önemlidir. C ve
C++ da, bir program küçük, yönetilebilir,bağımsız olarak sınanmış
parçalardan oluşturulabilir. Bir programı parçalara ayırabilmenin en
temel aracı altprogram ve altrutinler oluşturabilmektir. C ve C++ da alt
programlar işlevlerdir. İşlevler değişik dosyalara yerleştirilebilen kod
parçaları olup, ayrık derlemeyi olanaklı kılarlar. Burada başka bir şey
daha belirtelim; işlevler kodun en alt birimi olup, işlevler parçalanıp
herbir parçası, başka bir dosyaya konamaz. Bir işlevin tamamı tek bir
dosyaya konmalıdır. Dosyalar ise, birden fazla işlevi barındırabilirler.
İşlev çağrıldığında, işlevin değişkenlerine çalışması sırasında
kullanacağı değerler atanır. İşlevin çalışması bittiğinde geri dönüş
değeri denilen, işlevin yukarıda aktarılan değerlerle çıkan sonucu elde
edilir. Değişkenleri olmayan ve geri dönüş değeri üretmeyen işlevler
de yazmak olasıdır. Bunlar zaman harcamada kullanılabilir.
Birden fazla dosyalı bir program oluşturabilmek için, bir dosyadaki
işlevler mutlaka diğer dosyalardaki işlev ve verilere erişebilmelidir.
Bir dosya derlenirken C ve C++ derleyicisi öteki dosyalardaki işlev ve
verileri, ismen ve kullanım olarak bilmek zorundadır. Derleyici işlev
ve verilerin kullanımını güvenceye alır. Anlatılan bu işlemler dış işlev
ve verilerin bildirimi (declaration) ve bunların nasıl göründüğünü "
derleyiciye anlatma" sürecidir. Bir işlev veya değişken bildirildimi,
derleyici doğru kullanım için gereken bütün denetimleri yapar.
Tanıtım; tanımlar ve bildirimler
Bildirimler ve tanımlar arasındaki farkı anlamak çok önemlidir. Tüm
C ve C++ programlarına bildirimler gerekir. İlk programı yazmadan
önce, bildirim yazabilmeyi öğrenmek lazımdır. Bildirim derleyiciye
bir isim bildirir. Bunun anlamı; "bu isim altında bir işlev veya
değişken, programın herhangi bir yerinde bulunmakta, ve şöyle
görünmekte "demektir. Tanımın anlamı ise "bu değişkeni burada
oluştur" veya "bu işlevi burada oluştur" demektir. İsim için saklama
yapılır. Değişken veya işlevden ikisi için de, bu anlamlandırma
geçerlidir. Derleyici her iki durum için de bellekte yer açar. Değişken
için; derleyici değişkenin büyüklüğünü saptar ve ona bellekte
değişkene atanacak verileri saklamak için yeterli yeri ayırır. İşlev için
ise derleyici bir kod üretir ve onu bellekte saklayarak işlemi
sonlandırır.
Değişken veya işlevleri değişik yerlerde bildirebilirsiniz. Fakat C ve
C++ da her biri için tek bir tanıtım olmalıdır.
Buna tek tanıtım kuralı denir. İlişimci hedef modülleri biraraya
getirirken aynı işlev ya da değişken için birden fazla
tanıtıma rastlarsa hatalı işler yapar.
Tanıtım bir bildirim de olabilir. Derleyici x adını daha önce
görmediyse ve siz onu int x olarak tanımladıysanız,
derleyici onu bildirim olarak kabul eder, ve bellekte ona göre yer
ayırır.
İşlev bildirilmesi
C ve C++ de işlev bildirimi; işlev adı, işlevin değişkenleri ve işlevin
geri dönüş değeri ile olur. Şimdi örnek olarak bir işlev bildirimi
yapalım (tamsayılar C ve C++ da int olarak gösterilirler.);
int fonk1(int,int);
Buradaki ilk int geri dönüş değeridir, fonk1 işlev adıdır, ayraç
içindekiler ise değişkenlerdir, en sondaki noktalı virgül ise deyimin
sonlandığını gösterir. Bu satırın derleyiciye anlattığı ise; "hepsi bu,
burada işlev tanımı yok, bildirimi var".
C ve C++ bildirimleri kullanılan unsurları taklit edebilirler. Örneğin a
diğer bir integer olsun, yukarıdaki işlevi aşağıdaki şekilde kullanmak
olasıdır;
a=fonk1(2,3);
fonk1( ) in geri döndürdüğü değer integerdir, C ve C++ derleyicisi
fonk1( ) in değerini a nın kabul edip edemeyeceğini değişkenlerede
bakarak denetler.
İşlev bildirimlerinde değişkenlerin adları da bulunabilir. Derleyici
bunları gözardı etmesine rağmen, kullanıcı için çok yararlıdırlar.
Örnek olarak yukarıdaki fonk1( ) işlevi, aynı anlamda, ama farklı bir
biçimde gösterilebilir;
int fonk1(int uzun,int gen);
DİKKAT!!
C ve C++ arasında işlev bildirimlerinde, değişken gösterimi olmadığın
da önemli anlam farkı vardır.(Yani parantez içinde değişken
olmadığında). C dilinde;
int fonk2( );
burada, C dilin de, ayraç içinde herhangi sayıda ve herhangi tipte
değişken var demektir. Bu anlam C derleyicisinin tip denetimi
yapmasını engeller. C++ dilinde yukarıdaki bildirimin anlamı ise
işlevin değişkeni yok demektir.
İşlev tanımları
İşlev tanımları işlev bildirimlerine benzer, fark; tanımlardaki
gövdeden kaynaklanır. Gövde, zincirli ayraçlar arasında bulunan çok
sayıda deyimden oluşur. Zincirli ayraçlar, kod bütününün başlayıp
bittiği yerleri belirtir.
fonk1( ) işlevine boş gövdeli tanım aşağıdaki gibi verilir (boş gövde,
içinde kod bulunmayan gövde demektir.);
int fonk1(int uzun,int gen){ }
İşlev tanımında, işlev bildiriminde ki noktalı virgülün yerine, zincirli
ayraçlar gelmiştir. Burada dikkat edilecek başka bir konu da, işlev
gövdesinde kullanılacak değişkenlere, işlev tanımın da mutlaka bir
isim verilmesidir.(örnekte hiç kullanılmayacağı için, seçenek olarak
sunulmuştur).
Değişken bildirilmesi
Değişken bildirimi ifadesine eskiden beri tutarsız, şaşırtıcı anlamlar
yüklenmektedir. Doğru tanımı bilmek kodları doğru okumayı sağlar.
Değişken bildirimi derleyiciye değişkenin ne gibi göründüğünü söyler.
Yani değişkenin derleyiciye söylediği;"biliyorum bu ismi daha önce
duymadın, sana söz veriyorum bir yerde rastlayacaksın ve o
değişkenin tipi X" der.
İşlev bildiriminde; tip (dönüş tipi), işlev adı, işlevin değişkenleri ve
noktalı virgül vardı. Bunlar derleyicinin bildirimi kabul etmesi için
yeterlidir. Benzer biçimde değişken bildiriminde; tipin ardından bir
isim gelerek olmalıdır.
Örnek olarak;
int a;
yukarıdaki mantığa göre a değişkeni int olarak bildirilmektedir.
Burada bir çatışma var; yukarıdaki kod, derleyiciye
a integerine bellekte yer ayırması için yeterli bilgiyi veriyor. Konu
bildirim ile tanım arasındaki fark. Bu ikilemi çözmek için C ve C++ da
"bu sadece bildirimdir, başka bir yerde tanımlanacaktır" diyecek bir
anahtar kelimeye gerek vardı. Anahtar kelime;
extern
dir. extern, dosyaya dışarıdan aktarılacağını, veya daha sonra ilerde
dosya da tanım verileceğini bildirir.
Değişkeni tanımlamaksızın bildirimde bulunmanın yolu, değişkenden
önce extern kelimesini kullanarak sağlanır.
Örnek olarak;
extern int a;
extern kelimesi işlev bildirimlerine de uygulanır. fonk( ), extern
kelimesiyle şöyle gösterilir;
extern int fonk1(int uzun,int gen);
Bu deyim daha önceki fonk1( ) bildirimleriyle eşdeğerdir. İşlev
gövdesi olmadığında derleyici, işlev tanımı yerine işlev bildirimi
olarak işlemlemek zorunluluğunu duyar. O zaman extern anahtar
kelimesi işlev bildiriminde gereksiz ve opsiyoneldir. Herhalde
talihsizlikten olsa gerek C dili tasarımcıları işlev bildirimlerinde,
extern i kullanmak istememişlerdir; aslında daha tutarlı ve daha az
karışık olurdu (belkide daha fazla klavye tuşu kullanmak istemediler).
Birkaç bildirim örneği aşağıda verilmiştir;
// : B02:bildirim.cpp
// Bildirim & tanım örnekleri
extern int i ;
extern float f(float) ;
float b ;
float f(float a) {
return a+1.0;
}
//Tanımsız bildirim
//İşlev bildirimi
//Bildirim ve tanım
//Tanım
int i ;
//Tanım
int h(int x) { // Bildirim ve tanım
return x+1;
}
int main( ) {
b=1.0 ;
i=2 ;
f(b) ;
h(i) ;
} ///:İşlev bildirimlerinde, değişken belirteci bir tercih olarak bulunur.
Tanımlarda ise bir gerektir. Belirteçler sadece C dilinde gerektir, C++
da değil.
Başlıkları ekleme
Kütüphanelerin çoğunda hatırı sayılır miktarda işlev ve değişken
vardır. Bunlara dış bildirimlerde bulunulduğu zaman, tutarlılığı
sağlamak ve çalışmaları güvende tutmak için, C ve C++, başlık
dosyası (header file) denilen bir araç kullanır. Başlık dosyası,
kütüphane için dış bildirimleri içeren bir dosyadır; ve uzantısında 'h'
harfi olur,
headerfile.h gibi.(nadir olarak eski kodlarda .hxx ya da .hpp gibi
uzantılara da rastlanabilir.)
Kütüphaneyi oluşturan programcı başlık dosyasını da temin eder.
Kullanıcı kütüphanede ki dış değişkenleri ve işlevleri bildirmek için
başlık dosyasını basit şekilde sistemine ekler. Başlık dosyasını
eklemek için önişlemleyicinin #include yönergesi kullanılır. Bu,
önişlemleyiciye anılan isimdeki başlık dosyasını açmasını
ve içeriğini de #include deyiminin bulunduğu yere yerleştirmesini
söyler. Bir #include dosyasında isimlendirme iki türlü yapılır; ya açılı
(< >) ayraç işaretleriyle, ya da çift tırnak işaretleriyle (" ").
Açılı ayraç işaretlerle gösterim ;
#include < header.h>
Önişlemleyici, ilgili başlık dosyasını bulunulan ortama özgü bir
şekilde arar, tipik olarak include arama rotası ya ortamda belirtilir, ya
da derleyici komut satırında belirtilir. Arama rotasını yerleştirme
işlemi, işletim sistemlerine,
makinelere ve C++ nın kendi yapısına bağlıdır. Sizdeki için, biraz
araştırma yapmanız lazım. Çift tırnak içindeki başlık dosya isimleri ise
aşağıdaki gibidir ;
#include "local.h"
önişlemleyici local.h dosyasını kurgu tanımlı yolla (özelliklere göre )
arar. Bunun tipik anlamı bulunulan dizine
göre ara demektir. Dosya bulunamazsa önişlemleyici dosyayı çift
tırnak yerine açılı ayraç işaretlerine göre yeniden işlemler iostream
başlık dosyasını eklemek için ;
#include <iostream.h>
yazılır. Önişlemleyici iostream başlık dosyasını bulur ve gerekli yere
koyar.
Standart C++ include biçimi
C++ evrimleşme sürecindeyken değişik derleyici üreticileri, dosya
isimleri için değişik uzantılar seçiyorlardı.
Bunlara ilaveten, farklı işletim sistemleri dosya isimlerine farklı
kısıtlamalar koyuyorlardı, özellikle de isim uzunluklarına. Bütün
bunlar kaynak kodlarının taşınabilirliğine darbe vuruyordu. İşte keskin
köşeleri düzeltmek,
standart olmak için, 8 karakterden uzun dosyalarda, uzantılar
kaldırıldı. Eski tip yazımda iostream;
#include <iostream.h>
yeni yazımda iostream başlık dosyası;
#include <iostream>
Çevirici, derleyici ve işletim sistemine özgü ihtiyaçlara uygun yolu
kullanarak, include deyimlerini işlemler. Burada gereken isim
kısaltmaları ve uzantı eklemeleri varsa gözönüne alınır. Ayrıca
derleyici üreticisi tarafından
verilmiş olan başlıkları uzantısız olanlara kopyalayabilirsiniz. Bu tarzı
kullanmak isterseniz derleyici üreticisinin
önceden desteğini almalısınız. C dilinden gelen kütüphaneler hala
geleneksel ".h" uzantısıyla kullanılmaktadır.
Bu kütüphaneler modern C++ da adının başına "c" harfi koyarak
kullanılmaktadır.
#include <stdio.h>
#include <stdlib.h>
C++ da ;
#include <cstdio>
#include <cstdlib>
olarak kullanılır. Bütün standart C başlıkları için bu yöntemi
uygulayıp, C++ dilinde de kullanabiliriz. C++ kütüphanelerine
karşılık, C kütüphanelerini kullanmadaki ince farklılığı da görürüz.
Yeni include biçimiyle eskisinin etkisi aynı değildir; .h kullanmak
eski, kalıp olmayan sürümü verir,.h atlamak ise yeni kalıp sürümü
verir. Aynı programda her iki hali de kullanmaya kalkarsanız
genellikle sorunlarla karşılaşırsınız. O nedenle birini tercih
etmelisiniz.
İlişim (linking)
İlişimci, derleyici tarafından üretilen hedef kodları(uzantıları
genellikle .o ya da .obj dir) biraraya getirerek, işletim sisteminin
yükleyip çalıştırabileceği, çalıştırılabilir kodlar haline getirir. Bu
aşama derleme sürecinin son evresidir.
İlişimcinin özellikleri sistemden sisteme değişir. İlişimciye, birarada
iliştirilecek hedef modüllerin, kütüphanelerin
ve çalıştırılacak programın adı verilir, o da gerekenleri yapıp
çalıştırılabilir kodu üretir. Bazı sistemler ilişimciyi
sizin uyarmanızı isteyebilir. C++ paketlerinin çoğunda ilişimciyi
C++derleyicisinden siz uyarırsınız. Birçok durum da da ilişimcinin
çalışmasını farketmezsiniz, görünmeden çalışır.
Bazı eski ilişimciler, hedef dosyaları ve kütüphaneleri bir defadan
fazla aramaz, bu tür ilişimciler sizin ona verdiğiniz listeden, soldan
sağa bakarak arama yaparlar. Bu, hedef dosyaların ve kütüphanelerin
sıralamasının
önemli olduğu anlamına gelir. İlginç bir hatanız varsa ve siz bunu
bunu ilişim aşamasına gelene kadar farkedemediyseniz, o zaman
ilişimciye aktarılan dosyaların sıralamasında hata olasılığı var
demektir.
Kütüphane kullanımı
Şu ana kadar kütüphanelerin kullanımı ile ilgili sıralamayı öğrenmiş
olmalısınız. Kütüphaneyi kullanmak için;
1-Kütüphanenin başlık dosyasını ekle.
2-Kütüphanedeki işlev ve değişkenleri kullan.
3-Kütüphaneyi çalışan programa iliştir.
Yukarıdaki aşamalar, hedef modüller kütüphanelere birleşik olmasa
bile uygulanır. Başlık dosyasının eklenmesi ve hedef modüllerin
ilişiklenmesi, C ve C++ da ayrık derlemenin en temel adımlarıdır.
İlişimcinin kütüphane araması
C ve C++ da bir işlev ya da değişkene dışarıdan referans verildiğinde,
ilişimci bu referansa rastladığı zaman, iki şeyden birini yapabilir.
Birincisi; işlev veya değişkenin tanımına o ana kadar rastlamadıysa,
"kabullenmemiş
referanslar" listesine bir işaret ekler. Yok eğer, tanıma daha önceden
rastladıysa, ilişimci referansı kabul eder.
İlişimci hedef modüllerin listesinde tanıma rastlamazsa kütüphaneleri
araştırır. Kütüphaneler dizinli
oldukları için, ilişimcinin kütüphanedeki bütün hedef modüllere
bakması gerekmez,sadece dizine bakması yeter.
İlişimci, kütüphanede bir tanıma rastladığı zaman, sadece işlev
tanımını değil, bütün hedef modül çalışabilir programa iliştirilir.
Dikkat, sadece kütüphanedeki ilgili tanımın bulunduğu hedef modül
programa
iliştirilir, kütüphanenin tamamı değil (yoksa programlar aşırı büyük
olurdu). Kendi kütüphanelerinizi inşa ederken kaynak kod
dosyalarının her birine tek işlev koymak, çalıştırılabilir programın
boyutunu en aza indirger.
Bu yazım işlemini arttırsa da, kullanıcı açısından çok yararlıdır.
İlişimci dosyaları, sizin verdiğiniz sıralamaya göre aradığından,
kütüphane işlevinin kullanımında öncelik almak
için, kendi işlevinizi içeren dosyayı, aynı adı kullanarak, kütüphane
adı görünmeden önce listeye yerleştiriririz.
Böylece ilişimci kütüphaneyi aramadan evvel sizin işlevinizi
kullanarak bu işleve verilen herhangi bir referansı kabul edebilir.
Burada kütüphane işlevinin yerine sizin işleviniz kullanılmış olur.
Böyle bir uygulama, bazen
C++ da isim alanlarını engellediği için hatalara neden olabilir, o
nedenle dikkatli seçimlerde bulunmalıdır.
Gizli ekler
C ve C++ da çalıştırılabilir bir program oluşturulduğunda belli bazı
unsurlar programa gizlice iliştirilir. Bunlardan
birisi başlatma modülüdür. Bu modül C ve C++ programlarının
çalışmaya başlaması için gereken başlatma rutinlerini içerir. Bu
rutinler yığıtı kurar ve programdaki belli değişkenleri çalışmaya
başlatır.
İlişimci programda çağrılan "standart" işlevlerin derlenmiş sürümünü,
her zaman standart kütüphane de arar.
Kütüphane her zaman arandığı için, kütüphanedeki herhangi birşeyi,
istediğiniz zaman kullanabilirsiniz, yapmanız gereken tek şey; uygun
başlık dosya adını programınıza eklemektir. Burada ayrıca
kütüphaneyi ara diye emir vermeye gerek yoktur. iostream işlevleri
standart C++ kütüphanesi olup, bu işlevleri kullanabilmek için sadece
programımıza <iostream> başlığını eklemek yeterlidir.
Sonradan eklenmiş kütüphaneler varsa, ilişimcinin kullanacağı
dosyalar listesine, kütüphane adı mutlaka açık bir biçimde
eklenmelidir. Kütüphaneler, çoğu kez işlevleri alınıp kullanıldığı için,
işlevlik olarak ta adlandırılabilir.
C kütüphanesinin kolay kullanımı
C++ programları yazıyorsunuz diye, C kütüphane işlevlerini
kullanamayacaksınız demek değildir. Aslında bütün
C kütüphanesi Standart C++ ya eklenmiş varsayılmaktadır. Bu
işlevlerle yapabileceğiniz, bir sürü vakit harcamanızı önleyen, çok iş
bulunur.
Biz kitabımız da, uygun olduğu sürece, standart C++ (ayrıca standart
C) kütüphane işlevlerini kullanacağız. Sadece
standart kütüphane işlevleri programların taşınabilirliğini güvenceye
alabilir. C++ standartına uymayan çok nadir durumda kullanılan
kütüphane işlevlerinin POSIX uyumlu olmasına bakılacaktır. POSIX
standartı UNIX işletim sisteminin standartı olup, buradaki işlevler
C++ kütüphanesinin ötesine geçmektedir. POSIX işlevleri UNIX ve
LINUX işletim sitemlerinde kullanılır, sıklıklada DOS ve WINDOWS
ta rastlanır. Örneğin koşut denetim akışı (multithreaded)
kullanıyorsanız POSIX kütüphanesini kullanmak, kodları anlaşılabilir,
taşınabilir ve bakımı kolay yapar. Zira POSIX koşut denetim akışı
kütüphanesi oldukça zengindir. (POSIX denetim akış kütüphanesi,
eğer varsa işletim sisteminin sadece denetim akış kolaylıklarını
kullanır.). Eş anlı programlama olarak bilinen koşut denetim için NYP
de değişik seçenekler bulunmaktadır. Bu seçenekler farklı işletim
sistemleri ve tekleşik (embedded) dizgelerde de kullanılabilir, yeri
geldiğinde seçenekleri anlatacağız. Zira günümüzde yarıiletken
teknolojilerinde ortaya çıkan gelişmeler (çok işlemcili yongalar) bu tür
yazılım süreçlerini gerekli kılmaktadır.
İlk C++ programı
Şimdi bir program oluşturmak ve derlemek için yeterli hemen hemen
herşeyi biliyorsunuz. Program standart C++
iostream sınıflarını kullanacak. Bunlar verileri dosyalardan okuyacak,
dosyalara yazacak ve standart giriş ve çıkışlara da aynı işlemi yapacak.
(normal de konsola girilir ve çıkılır, fakat dosya ya da başka araçlara
da yönlendirilebilir.) Bu basit programda ekrana ileti yazdırmak için
bir akış nesnesi kullanılacaktır.
iostream sınıfının kullanılışı
iostream sınıfındaki işlev ve dış verileri kullanabilmek için deyim ile
birlikte başlık dosyası eklenir.
#include <iostream>
İlk programımız "genel amaçlı çıkışın gönderildiği yer olan" standart
çıkışı kullanacak. Diğer örneklerde standart çıkış değişik biçimlerde
kullanılacaktır. Fakat şimdi burada konsol (ekran) çıktı yeri olacak.
iostream paketi, otomatik olarak tanımladığı cout değişkeni (nesnesi)
ile, standart çıkışa gönderilen bütün verileri kabul eder.
Standart çıkışa veri göndermek için << işleçleri kullanılır. C
programcıları bu işleci sola bit kaydırma işleci olarak anımsayacaklar.
Burada sola bit kaydırmanın çıkışta etkin olmadığını söyleyelim. C++
işleçlere de bindirimlerde
bulunur . Bir işlece bindirim de bulunulduğunda, özel bir nesne ile
kullanıldığı zaman işlece yeni bir anlam
verilmiş demektir. (bindirim; aynı ada, değişik anlamlar
yükleyebilmektir)
iostream nesneleriyle << işleci "gönder" anlamını taşır. Örnek;
cout<< "selami !";
cout nesnesine (kısaca konsol çıkışı) "selami" kelimesini gönderir.
İşleç bindirimi için başlangıç olarak bu açıklama yeterli. Daha sonra
ayrıntıları ile incelenecektir.
namespace
C dilinin karşılaştığı sorunlardan biri; program belli bir büyüklüğe
vardığı zaman işlev ve belirteç isimlerinin devre dışı kalmasıdır. Tabii
isimleri gerçekte devre dışı bırakmazsınız ama, bir süre sonra yeni
isimleri eklemek mümkün olmaz. Daha da önemli olan, program belli
bir büyüklüğe vardığı zaman, değişik grup ve kişiler tarafından inşa
edilip bakımı yapılan parçalara ayrılır. C , bütün işlev ve belirteçlerin
isimlerinin bulunduğu yegane yer olduğu için geliştiricilerin tamamı,
çakışmaya yolaçacak aynı ismi kullanma konusunda çok dikkatli
olmak zorundadırlar. Böyle bir isim çakışması cansıkıcı, vakit alıcı, ve
son olarakta çok pahalıdır.
Standart C++ da çakışmayı engelleyecek mekanizma bulunur;
namespace anahtar kelimesi bu görevi yapar.
Bir program veya kütüphanede, C++ tanımlarının her kümesi bir isim
alanın da sarmalanmışlardır. Eğer başka bir tanım aynı ada sahip
olursa, fakat başka isim alanında bulunduğunda, o zaman çakışma
olmaz.
İsim alanları elverişli ve faydalı araçlardır. Bu araçların varlığı,
programlarınızı yazmaya başlamadan önce onlar hakkında yeterli
bilgiye sahip olmayı zorunlu kılar. Başlık dosyasını ekleyip, o
başlıktan işlev ve nesneleri kullandığınızda, programı derlediğiniz
zaman herhalde acayip görüntülü hatalarla karşılaşmıssınızdır.
Bunların
kaynağı; başlık dosyasına eklediğiniz unsurların tanımlarından
herhangi birini, derleyicinin bulamamasıdır. Hata iletisine birkaç kez
rastladıktan sonra, anlamı hakkında bilgi sahibi olursunuz.(anlamı :
"başlık dosyasını eklediniz fakat bütün bildirimler bir isim alanı içinde
ve siz derleyiciye, o isim alanı içindekileri kullanmak istediğinizi
söylemediniz.")
Bir anahtar kelime de "ben tanım ve/veya bildirimleri bu isim alanı
içinde kullanmak istiyorum" u size söyletir.
Bu anahtar kelime using tir. Standart C++ kütüphanelerinin hepsi bir
isim alanı ile sarmalanmıştır. Bu isim alanı standarta karşılık gelen std
dir. Kitapta, standart kütüphaneler kullanılırken sürekli olarak hemen
her programda,
aşağıdaki using yönergesini görürsünüz.
using namespace std ;
Bunun anlamı; bütün ögeleri std isim alanında gösteriniz demektir. Bu
deyimden sonra artık özel kütüphane elemanınızın isim alanı hakkında
endişelenmenize gerek yoktur, zira using yönergesi yazıldığı yerden
itibaren, dosya boyunca size isim alanı temin eder. Sorunla
karşılaştıktan sonra, bütün ögeleri bir isim alanında
sergileyerek bunları saklamak, üretkenliği azaltır ve bunu iyice
düşünmek lazımdır (ilerde göreceğiniz gibi). using yönergesi isimleri
sadece bulunulan dosya için gösterir. O nedenle ilk duyulduğunda, o
kadar etkili değildir.(Ama başlık dosyasında kullanırken iki defa
düşünün, önemlidir.)
İsim alanları arasında, eklenen başlık dosyaları aracılığı ile bir ilişki
vardır. Modern başlık dosyaları standard hale gelmeden önce (yani .h
uzantısı kaldırılmadan önce,(<iostream>) gibi değilken), başlık
dosyaları tipik olarak .h uzantısı alıyorlardı (yani <iostream.h> gibi).
O zamanlar isim alanları herhangi bir dilin ögesi değillerdi. Bu tür var
olan kodları şimdiki kodlarla uyumlu yapmak için;
örnek olarak ;
#include <iostream.h>
yerine
#include <iostream>
using namespace std;
kitabımızda .h uzantısız başlık dosyaları kullanılacağı için, using
yönergesi açıkça belirtilmelidir.
Şu ana kadar isim alanları ile ilgili öğrendikleriniz sizin için şimdilik
yeterli olup, ayrıntılar ilerleyen bölümlerde anlatılacaktır.
Program yapısının temelleri
Bir C ve C++ programı değişkenlerin, işlev tanımlarının ve işlev
çağrılarının biraraya gelmesinden oluşur. Program çalışmaya
başlarken başlatma kodunu işlemler ve özel main( ) işlevini çağırır.
Programın temel kodu buraya konur. Daha önce belirtildiği gibi, bir
işlev tanımın da; dönüş tipi (C++da mutlaka bulunmalı), işlev adı,
ayraçlar arasında değişken adları ve zincirli ayraçlar arasında işlev
kodları bulunur. Örnek işlev tanımı ;
int function( ){
//işlevin tanım kodu buraya yerleştirilir. Bu bir yorumdur.
}
Yukarıdaki işlevin değişkeni yoktur ve gövdesinde de sadece yorum
vardır. Bir işlev tanımın da, çok sayıda zincirli ayraç kümesi olabilir,
fakat işlev gövdesini taşıyan en az bir çift ayraç bulunmak zorundadır.
main( ) işlevi de bu kurallara uyar. C++ da main( ) işlevinin geri
dönüş tipi, her zaman int dir. Programın ana görevi burada tanımlanır.
C ve C++ serbest yapılı dillerdir. Derleyici birkaç istisna dışında,
satırbaşlarını ve boşlukları gözardı ettiğinden
deyim sonlanmaları mutlaka belirlenmek zorundadır. Deyimler,
noktalı virgül ile sonlandırılırlar.
C yorumları /* ile başlar */ ile biter. C++ da C tarzı yorum işaretlerine
ek olarak //işaretlerini yorum başlangıcı olarak alır, satırbaşında
sonlandırır. Biz tek satırlık yorumlarda // işaretlerini kullanacağız.
"Merhaba dunya"
Ve geldik ilk C++ programımıza;
// :B02:Merhaba.cpp
// C++ ile merhaba demek
#include <iostream>
//akis bildirimi
using namespace std;
int main( ){
cout<<"merhaba dunya ! iste ben"
<<4<<"bugun!"<<endl;
}///:~
cout nesnesi << işleçleri yardımıyla çok sayıda değişkeni işlemler. Bu
değişkenleri soldan sağa doğru yazar.
Özel iostream işlevi endl ise satır çıkışı ve yeni satır başı görevini
üstlenmiştir. iostream çok sayıdaki değişkenin
kelimeler ve cümleler halinde kullanılmasında kolaylık sağlar.
C dilinde çift (") tırnak arasına alınmış metinler string adıyla anılır.
C++ da ise çok güçlü bir string sınıfı metin işlemlemelerinde
kullanılır. Çift tırnak arasındaki metinler için karakter dizisi
diyenlerde vardır.
Derleyici karakter dizileri için bellekte yer açar ve karakterlerin ASCII
eşdeğerlerini burada saklar. Derleyici karakter dizisinin sonlanmasını
bellekte ekstra bir değerle (0 değeriyle) belirtir. Bu işlem otomatik
olarak yapılır.
Karakter dizisine çıktı dizilerini kullanarak özel karakterler
koyabilirsiniz. Bunlar ters bölü (\) işaretini takip eden özel
kodlardır.Örneğin; \n yeni satır başı demektir. Kendi C elkitabınız da
çıktı dizi ögeleri verilmiştir. Ötekilere
örnek; \t (tab), \\(backlash), ve \b(geriye bir adım)
Bilmemiz gereken önemli bir konuda; bir deyimin birkaç satır
sürebileceği ve sonlanmasının sadece noktalı virgülle olacağıdır.
Karakter dizisinde değişkenler ve sabitler yukarıdaki cout deyiminde
karışık olarak bulunabilirler. Bunun sebebi << işleci cout deyimiyle
kullanıldığında bindirime uğrar (yani aynı işleç birden fazla görev
üstlenir). Bu nedenle cout deyimine, kolaylıkla farklı değişkenler
gönderebilirsiniz.
Derleyiciyi çalıştırma
Kullandığımız derleyicinin GNUC derleyicisi olduğunu kabul ederek,
yukarıdaki merhaba.cpp programının derleme işlemi, aşağıdaki ifade
ile gerçeklenir;
gcc -o Merhaba Merhaba.cpp
Bu ifade merhaba .cpp programını derleyip çalıştırılabilir, merhaba
dosyasını oluşturur. Bunlar linux ortamında terminale yazılır.
merhaba programını çalıştırmak için terminale ;
./Merhaba
merhaba dunya ! iste ben 4 bugun
Öteki derleyicilerin sözdizimi kuralları için ilgili derleyicinin
kılavuzlarına bakılmalıdır.
iostream hakkında ayrıntılar
Yukarıda iostream sınıfı hakkında ilk ana bilgiler verildi. Çıktı
biçimlendirmesi yapan iostream sınıfı sayıları da
onluk, sekizlik, ve onaltılık (desimal,oktal,ve heksadesimal)
düzenlerde biçimleyebilir. Aşağıdaki örnek başka çeşit
bir iostream sınıf kullanımıdır;
// :B02:Stream.cpp
// stream ayrintilari
#include <iostream>
using namespace std;
int main( ) {
//bicimleri durum degistiricilerle belirleme:
cout<< "onluk olarak bir sayi:"
<<dec<<12<<endl;
cout<<"sekizlik olarak:"<<oct<< 12<<endl;
cout<<"onaltilik olarak:"<<hex<<12<<endl;
cout<<"kayan-noktali bir sayi:"
<<2.2728<<endl;
cout<<"yazilamaz char(escape):"
<<char(27)<<endl;
}///:~
Örnekte iostream sınıfı durum değiştiricileri kullanarak çıkışa onluk,
sekizlik ve onaltılık tabana göre sayılar gönderiyor. Durum
değiştiriciler (manipulators) çıkışa yazı yazdırmaz, sadece yazılacak
olanın durumunu belirler.
Kayan noktalı sayılar derleyici tarafından otomatik olarak biçimlenir.
Bunlara ilaveten iostream nesnesine, char'a
(char tek karakter saklayabilen veri tipidir.) yerleştirerek herhangi bir
karakter de gönderilebilir. Bu yerleştirme işlev çağrısına benzer; char(
) ayraçlar arasında karakterin ASCII değeri bulunur. Yukarıda ki
örnekte char(27) de 27 escape in ASCII değeridir. Böylece char(27),
iostream nesnesine "escape" gönderir.
Karakter dizisinin birleşmesi
C önişlemleyicisinin önemli bir özelliği de karakter dizilerini
birleştirebilmesidir. Bu özellik kitabımızdaki birçok örnekte
kullanılacaktır. Çift tırnak içinde bulunan iki yanyana karakter dizisini
aralarında noktalama bulunmamak
koşuluyla, derleyici bu dizileri tek bir karakter dizisi gibi
kopyalayabilir. Kodlamalarda yer kısıtlaması olduğunda oldukça
yararlı bir uygulamadır.
//:B02:Yanyana.cpp
//Karakter dizisi birlesmesi
#include <iostream>
using namespace std;
int main( ) {
cout<<"Bu cok uzun bir cumle olup tek satira"
"sigmaz fakat satir parcalara hatasiz"
"ayrilabilir \nparcalar arasinda noktalama"
"bulunmaz.\n";
}///:~
(Türkçe kelimelerdeki gariplik, ingilizce yazılmış olan derleyicilerde
sorun ortaya çıkmaması için,
ingilizce yazım kurallarına uymaktan kaynaklanmaktadır.)
Yukarıdaki örnekte her satırda noktalı virgül olmamasından dolayı
hata var sanılabilir. Ama hata yoktur, zira C ve C++ serbest yazım
özellikli dillerdir. Noktalı virgül, deyimin sonlandığını gösteren
işarettir. Bir deyimin uzunluğu bir satırdan fazla olabilir. Yani deyim
uzunluğu için satır sınırı yoktur.
Girdilerin okunması
iostream sınıfları giren verileri de okuyabilmektedir. Standart giriş
için kullanılan nesne cin (console input) dir.
Bu
nesne verileri normalde konsoldan kabul eder ama, başka kaynaklara
da yönlendirilebilir. Yönlendirme örneği ilerde bu bölümde
verilecektir.
cin le kullanılan iostream işleci >> dir. Bu işleç değişkeniyle aynı tip
giriş bekler. Örnek aşağıda verilmiştir;
//:B02:Sayicev.cpp
//on tabanliyi sekiz ve onaltiliklara cevirir
#include <iostream>
using namespace std;
int main( ){
int sayi;
cout<<"bir onluksayi gir :";
cin>>sayi;
cout<<"sekizlik degeri.-0"
<<oct<<sayi<<endl;
cout<<"onaltilik degeri -0X"
<<hex<<sayi<<endl;
}///:~
Bu program kullanıcının girdiği sayıyı oktal ve heksadesimale çevirir.
Diğer programları çağırmak
Standart çıkışa yazan ve standart girişten okuyan bir programın bilinen
kullanımı ya UNIX kabuğunda ya da
DOS dosyasında olur. C ve C++ programının içinden standart C
system( ) işlevini kullanarak herhangi bir program
çağırılabilir. Bunu gerçeklemek için <cstdlib> başlık dosyası
bildiriminde bulunulur.
//:B02:CagMerhaba.cpp
//Baska programi cagirma
#include <cstdlib>
//system( ) bildirimi
using namespace std;
int main( ){
system("Merhaba");
}///:~
system( ) işlevinde değişken yerine kullanılan karakter dizisi,
programı işletim sisteminde çalıştırmak için komut satırına yazılanla
aynıdır. system'in ayraçları arasına sizin ürettiğiniz karakter dizilerini
de yerleştirebilirsiniz. Bu, örneğimizde olduğu gibi statik olmayabilir.
Komut ilgili programı çalıştırır ve kendi programına geri döner.
Yukarıdaki örnek C kütüphane işlevlerinin, C++ da ne kadar
kolaylıkla kullanılabildiğini göstermektedir. Sadece başlık dosyasını
ekle ve ilgili işlevi çağır,hepsi bu kadar. Eğer C dilinden C++ diline
yukarı doğru geçiş yapıyorsanız, C dilinin C++ diline bu uyumu, yeni
dili öğrenmenizde çok büyük yarar sağlar.
String'lere giriş
Karakter dizileri oldukça kullanışlıdır, ama sınırları vardır. Bundan
dolayı, sadece bellekte bulunan bir grup karakterin üzerinde değişik
bir takım şeyler yapmak isterseniz, bütün küçük ayrıntıları
yönetmelisiniz.
Örneğin; çift tırnak arasında bulunan karakter dizisinin uzunluğu
derleme sırasında sabitlenir. Eğer bir karakter diziniz varsa ve siz buna
bazı karakterler eklemek isterseniz, gerçekleyeceğiniz birçok unsuru
anlamak ihtiyacı doğar. (dinamik bellek yönetimi, karakter dizisi
kopyalama, karakter dizilerini birbirine ekleme gibi). Aslında, bizim
de amacımız budur.
Standart C++da string sınıfı, karakter dizilerinde alt düzeyli işlemler
yapabilmek için tasarımlanmıştır. Zamanında C programcılarının
istekleri de bu doğrultudaydı. C dilinin başlangıcında bu işlemlerin
hepsi hem zaman kaybı hem de hatalara yol açıyorlardı. String sınıfı
hayatı kolaylaştırması bakımından çok önemlidir.
O nedenle burada bir giriş yapılacak, daha sonra ikinci ciltte konuya
bir bölüm ayrılacaktır. Anlatılanlar ilerde sürekli kullanılacaktır. O
yüzden iyice kavranmalıdır.
string'leri kullanabilmek için C++ da başlık dosyasına <string>
eklenmelidir. string sınıfı std isim alanındadır,
o nedenle using yönergesi de kullanılmalıdır. İşleç bindirimi
yüzünden string kullanımındaki sözdizimi, tamamen sezgiye
dayalıdır:
//:B02:MerhabaStrings.cpp
//Standart C++ string sınıfının esasları
#include <string>
#include <iostream>
using namespace std;
int main( ){
string s1,s2
;
//Bos stringler
string s3="merhaba dunya !" ;//Yazılı string
string s4("Ben") ;
//Bir yazılı string daha
s2="bugun"
;
//Deger atanmış string
s1=s3+" "+s4 ;
//Birlestirilmis stringler
s1+="8"
;
cout<<s1+s2+"!"<<endl ;
}///:~
//stringe yapıştırma
s1 ve s2 stringleri başlangıçta boş olarak tanıtılmıştır. s3 ve s4
stringlerin de ise karakter dizileri ile başlatılmıştır. Başlatma işlemleri
farklı fakat eşdeğerdir. String nesnelerini başka string nesneleri ile de
başlatabilirsiniz. String nesnelerine atama işlemi( = )işleci ile yapılır.
Bu işlecin sağ tarafındaki değer ilgili string'in yeni değeridir. Atama
işleminden önceki değer ise otomatik olarak kullanılmıştır. String'leri
bir string'te birleştirmek için (+) işleci kullanılır. Bir string'e başka
bir string'i ya da karakter dizisini eklemek istiyorsanız (+=) işleci
kullanılır. Sonuç olarak iostream, stringlerin işlemlerini bildiği için,
cout'a string doğrudan yazdırmak için yollanır. (ya da string üreten
bir ifade de ( s1+s2+"!") olabilir.)
Dosyaları okuma ve yazma
C dilinde dosyaların okunup işlemlere tabi tutulması, dil hakkında
altyapı bilgisi gerektirmektedir. C++ dilinde ise
iostream kütüphanesi dosya işlemlerinin basit şekilde yapılabilmesini
sağlar. Bu işlevsellik C diline göre daha erken gerçeklenmiştir.
Dosyaları okumak ve yazmak için <fstream> eklemek zorunludur. Bu
otomatik olarak <iostream> i
eklemesine rağmen cin ve cout vs.. kullanıyorsanız, genellikle
<iostream> i açıkça eklerken tedbirli olmalıdır.
Okumak için bir dosya açılıyorsa ifstream nesnesi oluşturulur, bu cin
yerine geçer. Yazmak için bir dosya açılıyorsa ofstream nesnesi
oluşturulur, bu da cout gibi davranır. Bir kez dosya açıldığında onu
okuyabilir ve ona yazabilirsiniz ya da öteki iostream nesnelerine
yaptığınız benzer işlemleri gerçeklersiniz. Herşey genel olarak bu
kadar basittir.
iostream kütüphanesinin en yararlı işlevlerinden bir tanesi getline( )
işlevidir. getline( ) işlevi bir satırı (yeni bir satır başına kadar) string
nesnesine okumaya olanak sağlar. getline( ) işlevindeki ilk değişken,
okuduğunuzdan gelen ifstream nesnesidir, ikinci değişken ise string
nesnesidir. İşlev çağrısı sonlandığında string nesnesinde satır bulunur.
Aşağıda bir dosyanın içeriğini başka bir dosyaya kopyalayan örnek
verilmiştir.
//:B02:Scopy.cpp
//Her seferin de bir satir olmak uzere bir dosyayinin diger dosyaya
kopyalanmasi
#include <string>
#include <fstream>
using namespace std;
int main( ){
ifstream in("Cogal.cpp");
//okumak icin dosya acimi
ofstream out("Cogal2.cpp");
//yazmak icin dosya acimi
string s;
while(getline(in,s))
//yeni satir karakterini, gozardi et
out<<s<<"\n" ;
//...geriye eklemeli
}///:~
Dosyalar ifstream ve ofstream nesneleri tarafından açılır ve
oluşturulan dosya isimleri de yukarıda görülmektedir.
Dikkatle bakarsanız, ilk kez while döngüsüne rastlıyorsunuz. while
ayrıntıları, daha sonra ki bölümde anlatılacak. Biz şimdilik while
anahtar kelimesinin, takip eden ayraçlar arasındaki alt deyimin
doğruluğunu denetlediğini söyleyemekle yetinelim. Bu ayraçların
dışında, zincirli ayraçlar arasında da çok sayıda deyim olabilir.
Yukarıdaki örnekte (getline(in,s)) deyimi doğru olduğu sürece, while
tarafından denetlenen deyimler işlemlenmeye devam eder. Eğer
(getline(in,s)) deyimi yanlış ise while tarafından denetlenmeyen
sıradaki ilk deyim işlemlenir. Yani yukarıda ki while döngüsü, giriş
dosyasındaki her satırı okur, ve daha sonra bu satırı çıkış dosyasına
gönderir.
getline( ) işlevi satırdaki karakterleri yeni satırbaşı karakteri görene
kadar okur. (satır bitişi ile ilgili karakter değiştirilebilir fakat bu konu
daha sonra iostream lerin ayrıntılı işleneceği ileriki bölümlerde ele
alınacak.)
Bununla birlikte yeni satır kavramı gözardı edilir,ve onun saklama
işlemi de string nesnesinde olmaz. Kopyalanmış dosyanın, kaynak
dosya gibi görünebilmesi için, satırbaşı için ("\n") işaretini
gösterildiği gibi yazmak zorundayız.
İkinci ilginç örnekte, bütün dosyayı bir string nesnesine
kopyalamaktır.
//:B02:FillString.cpp
//Butun dosyayi tek stringe okuma
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
int main( ){
ifstream in("FillString.cpp");
string s,line ;
while(getline(in,line))
s+=line+ "\n" ;
cout<<s ;
}///:~
Dinamik bir saklama yeteneği olan string'e ne kadar karakter
yüklendiğine bakılmamamlıdır. Saklama büyüklüğü, sizin ona
aktardığınız miktar kadardır.
Bütün dosyayı tek bir string'e yerleştirmenin sağladığı yararlardan
biri de; string sınıfının sağladığı araştırma ve işlemleme yeteneklerini
bu sayede kullanabilmektir. Fakat bunun da kendine göre kısıtlamaları
vardır. Örnek olarak genellikle bir dosya ayrı satırlar olarak işleme
tabi olur, dev bir satır gibi değil. Eğer satır numarası belli satırı
dosyaya eklemek isterseniz, her satırı ayrı string satır nesnelerine
sahip olmalısınız. Bunu başarabilmek için başka bir yaklaşıma
ihtiyacımız var.
vector'lere giriş
Stringleri kullanırken, ne kadar belleğe ihtiyaç olduğu bilinmesi
gerekmeden, string nesnelerini dolduruyorduk.
Satırları dosyadan bir string nesnesine okurken, kaç tane string
nesnesine ihtiyacımız olduğunu ancak okuma işlemi bittikten sonra
öğrenebiliyoruz. Bu bir sorundur. Sorunu çözmek için, otomatik
olarak eklenen string nesneleri kadar genişleme sağlayan, özel
tutuculara gereksinim vardır.
Gerçekten, neden kendimizi string nesnelerini saklarken kısıtlıyoruz?.
Bu bizi sorunun kendisine götürür. Program yazarken, bazı şeylerin ne
kadar olduğunu bilmemek sorunlara neden olabilir. İşte kap
(container) nesnesi,
sanki eğer her çeşit nesneyi tutarsa daha yararlı olacak gibi duruyor.
Allahtan standart C++ kütüphanesi hazır kap sınıflarına sahip.
Standart kap sınıfları, standart C++ nın, gerçek güç kaynaklarından
biridir.
Standart C++kütüphanesinin kapları(containers) ve algoritmaları
(algorithms) ile STL (standart template library)arasında küçük bir
karışıklık vardır. STL diye bilinen standart kalıp kütüphanesini Alex
Stepanov(o sırada HewlettPackard'da çalışıyordu) 1994 sonbaharında California San Diego'daki
C++ standartlar komitesine sundu.Özellikle, HP anılan kütüphaneyi
halka açınca, ismi yığıt oldu. Bu arada komite kütüphaneyi birçok
değişiklikle standart C++
kütüphanesine eklemledi, STL'nin gelişimi Silicon Graphics'te devam
etti.(bkz:www.sgi.com/Technology/STL).
SGI'nin STL'si birçok hassas noktada standart C++ kütüphanesinden
ayrılmaktadır. Böylece yaygın bir yanlış kanı olmasına rağmen, C++
standartı STL'yi içermez. Standart C++ kütüphanesindeki kap ve
algoritmalar ile SGI nin
STL leri aynı kökten (genelliklede aynı adlarla) geldikleri için küçük
bir karışıklık olabilmektedir. Biz bu kitapta STL ifadesini karışıklığa
meydan vermemek için kullanmayacağız. Standart C++ kütüphanesi
ya da standart kütüphane kapları veya benzeri birşey diyeceğiz.
Standart C++kütüphanesi kap ve algoritmalarının uygulamaları ileri
bilgiler gerektirse de, konuyu etraflıca ikinci ciltte inceleyeceğiz,
kütüphane kendisi hakkında fazla birşey bilmeye gerek olmadan da
çok güçlü kullanılabilir. O kadar kullanışlıdır ki; en temel standart kap
olan şimdi tanıtacağımız vector, bütün kitap boyunca kullanılacaktır.
vector'un temel unsurlarını kullanarak ne kadar çok şey
yapabileceğinizi göreceksiniz ve hiçbir soruna neden olmayacak.
(zaten NYP nin amacı da bu). İkinci ciltte standart kütüphane
kaplarına baktığınızda, bu ve diğer kaplar hakkında, çok daha fazlasını
öğreneceğiniz için, ilk kitabın baş tarafında vector'ü kullanan
programlar yazmak, affedilebilir, zaten deneyimli bir C++
programcısının yazdıklarına pek benzemeyecek. Yine de birçok durum
için kullanım çok uygundur.
vector sınıfı bir kalıptır, böylece; birçok tipe çok iyi şekilde
uygulanabilir. Yani şekillerin, kedilerin, kelimelerin vs.nin vector'ünü
oluşturabiliriz. Temel olarak kalıpla, "herhangi birşeyin sınıfını"
oluşturabilirsiniz. Derleyiciye söylenen; ne ile çalışılacağıdır (burada
vector'ün tuttuğu ). Açılı ayraç işaretlerinin içine istenen tipin adını
yerleştiririz.(<başlık dosyası yerleşim biçimi gibi>). Örnek olarak
string'in vector gösterimi; vector<string> dir.
Bu işlemi gerçekleştirdikten sonra vector sadece string nesnelerini
tutar, eğer bunun içine başka birşey yerleştirmeye kalkarsanız
derleyici hata iletisi verir.
Mademki vector, kap etkinliğini gerçekliyor, o zaman kabın içine
ögeleri koymanın, ve konulan ögeleri oradan alabilmenin, mutlaka bir
yolu olmalıdır. Yeni bir ögeyi vector sonuna eklemek için;
push_back( ) adlı üye işlev kullanılır. (üye işlevi belli bir nesneye
çağırmak için "." işareti kullanıldığını hatırlayın!). Burada üye işlev
için "put" yerine, bu kadar uzun adın seçilmiş olma nedeni; çok sayıda
kap ve üye işlevin olmasındandır. Başka
kaplarda, başka üye işlevlerle yeni ögelerin yerleşimi sağlanır.
Örneğin; insert( ) üye işlevi kabın ortasına öge yerleşimini sağlar ve
bunu vector kullanır, ama kullanımı ileri bilgi gerektirdiği için konuyu
ikinci cilde bırakıyoruz. Başka bir örnek üye işlev de push_front( )
dur (vector bunu kullanmaz), ögeleri başlangıca yerleştirir. Daha çok
sayıda üye işlev vector'de, ve daha çok sayıda kap, standart C++
kütüphanesinde bulunmaktadır. Küçük birkaç özelliği bilerek ne kadar
çok şey yapabileceğinizi görünce çok şaşıracaksınız.
Yukarıda, vector'e yeni ögeleri push_back( ) ile yerleştireceğinizi
öğrenmiştiniz, peki bu ögeleri geriye nasıl alacaksınız?. Çözüm zekice
ve güzeldir, işleç bindirimi vector'ü bir dizi (array) olarak görür. Dizi
her programlama dilinde sanal olarak bulunan bir veri tipidir.
Herhalde şimdiye kadar çok duymuşsunuzdur. Diziler topluluklar
olup, çok sayıda öge barındıran küme anlamını taşır. Dizinin ayırıcı
özellikleri; ögeleri aynı büyüklükte ve ögelerin biri ötekinin hemen
yanına konumlanmıştır. En önemliside; öge seçimi, dizin aracılığı ile
yapılır. Bunun anlamı "ben n nolu ögeyi istiyorum "denir ve ilgili öge
hemen üretilir, genellikle işlem çok hızlı olur. Programlama dillerinde
istisnalar olmasına rağmen dizinleme işlemi, köşeli ayraçlarla olur; bir
a dizisi varsa ve siz 5 öge üretmesini istiyorsanız a[4] şeklinde
gösterirsiniz. (dizinleme işlemi 0 dan başlar unutmayın!).
Yukarıdaki sağlam dizinleme gösterimi, işleç bindirimi yolu ile
vector'e eklemlenmiştir. Aynı iostream sınıflarında << ve >>
işleçleri gibi. Bindirimin nasılı sizin için şimdilik önemli değil (daha
sonra anlatılacak), aslında vector'ün [ ] gibi kullanımın da biraz
büyücülük var gibi (!). Aşağıdaki programda vector kullanılacak,
vector'ü eklemek için vector başlık dosyasını ( <vector> ) yerleştirin.
//:B02:FillVector.cpp
//string vectoruna butun dosyanın kopyalanmasi
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;
int main( ){
vector<string> v;
ifstream in("FillVector.cpp");
string line;
while(getline(in,line))
v.push_back(line);
//sona satir ekle
for(int i=0;i<v.size( );i++) //satir numaralarini ekle
cout<<i<<": "<<v[i]<<endl;
}///:~
Bu programın büyük kısmı daha önceki programa benzemektedir; bir
dosya açılıyor ve her satır string nesnesine bir kere de okunuyor,
string nesneleri vectore yerleştiriliyor, while döngüsü
tamamlandığında bütün dosya bellekte v dedir.
Programdaki bir sonraki deyim, for döngüsüdür. for döngüsü while
döngüsüne benzer, ayrıca ek denetim unsurları taşır. for kelimesinden
sonra yeralan ayraçlar arasında denetim açıklamaları yeralır, aynı
while da olduğu gibi. for'un denetim açıklamaları 3 kısımdan oluşur;
birinci kısım başlatır, ikinci kısım sınamadan çıkılıp çıkılmadığına
bakar, üçüncü yani son kısım da değişim adımlarını gösterir. Bu
program çok yaygın olarak kullanılan for döngüsünün çalışma
evrelerini göstermektedir; başlangıçta int i=0 ile i sayıcı ögesinin tipi
(integer) ve başlama değeri(=0) belirleniyor, ikinci bölümde (i<v.size
()) ile sınama bölümünde ise döngüde kalınıp kalınmayacağı
saptanıyor, burada i nin değeri vector v nin ögelerinin sayısından (bu
sayı size( ) üye işlevince üretilir) daha az olmalıdır(döngüde kalmak
için), üçüncü bölümde ise C ve C++ da kullanılan bir kısaltma
görülüyor (i++), bunun anlamı i yi al bir ekle ve kendi yerine yerleştir
(yani (i(t+1)=i(t)+1)demek) demektir. for döngüsünün toplam etkisi
i değişkenini sıfır değerinden alıp vector büyüklüğünün bir eksiğine
kadar işlemlemektir. Her i değeri için cout deyimi işlemlenir ve i
rakamlı satır inşa edilir (cout tarafından karakter dizisine çevrilir), iki
nokta üstüste ve bir boşluk, dosya satırı yazılır ve sonunda yeni
satırbaşı endl ile yapılır. Derleyip çalıştırdığınız zaman, satır
numaraları ve satırları görürsünüz.
>> işlecinin iostream'lerdeki gibi çalışmasından yararlanarak
yukarıdaki örneği, dosyayı satırlar yerine araları boşluklarla ayrılmış,
kelimelere bölebiliriz.
//:B02:GetWords.cpp
//Dosyayi kelimelerine bolmek
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
using namespace std;
int main( ){
vector<string> words;
ifstream in("GetWords.cpp");
string word;
while(in>>word)
words.push_back(word);
for(int i=0;i<words.size( );i++)
cout<<words[i]<<endl ;
}///:~
while(in>>word) açıklaması girdide her seferinde bir kelime okuma
demektir. Bu açıklama yanlış olduğu zaman dosyanın sonuna gelinmiş
demektir. Kelimeler boşluklarla sınırlandırılır, basit bir örnek olması
için bu yol tercih edildi. Kitabın ilerleyen bölümlerinde çok ayrıntılı
örnekler göreceksiniz, böylece girdileri istediğiniz gibi
bölümleyebilirsiniz.
vector'ün herhangi bir tiple kullanımının kolaylığını göstermek için
bir örnek daha verilmiştir; vector<int>
oluşturulacak.
//:B02:Intvector.cpp
//integer tutan vector oluşumu
#include <iostream>
#include <vector>
using namespace std;
int main( ) {
vector<int> v;
for(int i=0;i<10;i++)
v.push_back(i);
for(int i=0;i<v.size();i++)
cout<<v[i]<<", " ;
cout<<endl;
for(int i=0;i<v.size();i++)
v[i]=v[i]*10
for(int i=0;i<v.size();i++)
cout<<v[i]<<", " ;
cout<<endl ;
}///:~
Farklı bir tipi tutan vector oluşturmak için ilgili tipi, kalıp değişkeni
olarak, açılı ayraçların(< >) içine koyarsınız.
Kalıplar ve iyi tasarımlanmış kalıp kütüphaneleri bu kullanımı
kolaylaştırırlar.
Yukarıdaki örnek, vector'ün ikinci temel özelliğini de yansıtmaktadır;
aşağıdaki açıklama
v[i]=v[i]*10
;
bu vector etkinliklerinin, sadece içine birşey yerleştirme veya içinden
birşey alma ile sınırlı olmadığını gösterir. Böylece vector'ün herhangi
bir ögesine atama yapılıyor, köşeli ayraçların içindeki değerler tipi
belirliyor. Bunun anlamı vector nesneler toplululuğu ile çalışmak için
genel amaçlı esnek bir ortamdır. Önümüzdeki bölümlerde kullanımını
açık bir şekilde öğreneceksiniz.
Burada bir sınıfı da anmadan geçmeyelim; deque. Bu sınıfın bir çok
davranışı vector'e benzer fakat bir çok bakımdan da ayrılır. Ben
kişisel olarak deque kabını yerleşik tiplerin dışındaki struct ve class
için kullanılmasını öneririm. vector kabını ise yerleşik tipler için
tercih edebilirsiniz. Aslında gerek vector gerekse deque kaplarının
arayüzleri birbirlerine çok benzer. deque'deki pop_front( ) ve
push_front( ) işlevleri vector'de bulunmaz. (vector'ün capacity( ) ve
reserve( ) işlevleri de deque'de bulunmaz, ama bunlar o kadar önemli
değil bilakis bir zayıflıktır). Her iki kap sınıfı arasındaki temel fark;
içerde, belleği kullanma düzenindedir. deque belleği sayfa (veya
büyük parçalar) halinde kullanır. Her sayfada sabit sayıda kap ögesi
bulunur. Bu nedenle aslında adı çift uçlu kuyruktan (double-ended
queue) gelmesine rağmen iskambil destesi olarakta anılır. Çift uçlu
olması sayesinde kap ögeleri diziye her iki uçtan da (baş ve son)
yerleştirilebilir. vector ise bitişik bellek düzenini kullandığı için dizi
ögelerini sadece son kısıma koyabilir. deque'nin sayfalı bellek
düzeninin bir çok yararı vardır;
1- deque belli sürelerde kabın baş kısmında insert( ) ve erase( )
işlemlerini önerir. vector böyle bir şey yapmaz.
Standardın
yazdığına göre eğer her iki uçtan diziye öge eklemek veya silmek
isterseniz deque kullanmalısınız.
2- deque belleği vector'e göre daha verimli kullanır, özellikle sanal
belleği olmayan dizgelerde işletim dizgelerine daha uygun olanaklar
sağlar.
3- deque kullanımı daha kolaydır.
Şimdilik bu kadar yeter.
ÖZET
Bu bölümün amacı; biri sizin için nesne tanımlama işini yaparsa, NYP
nin kolaylığını göstermektir. Bu durumda; başlık dosyalarını
eklersiniz, nesneleri oluşturursunuz ve onlara ileti gönderirsiniz. İyi
tasarlanmış güçlü tipler kullanıyorsanız, çok fazla uğraşmadan iyi
netice veren güçlü programlar yazarsınız.
Kütüphane sınıflarını kullanarak, NYP nin kolaylığını gösterirken,
standart C++ kütüphanesindeki en temel ve kullanışlı tiplerle tanıştık;
iostream ailesi, string sınıfı ve vector kalıbı gibi. Kullanımlarının
basitliğini gördünüz
ve onlarla daha birçok şey başarabileceğinizi hayal etmelisiniz, aslında
yapabilecekleri daha çok şey var.
Kitabımızın başlarında bu araçların sınırlı işlevlerini kullansak da, C
gibi alt seviyeli dili öğrenme ilkelliğinden
hiçte azımsanmayacak ileri bir adım attık. Ve ayrıca boşuna zaman da
tüketmemiş olduk. Sonunda sahip olduğunuz nesnelerin yönetimini,
en alt seviyelere kadar sağlarsanız çok daha üretken olursunuz. NYP
nin bütün özelliği ayrıntıların üstünü boyayarak saklamaktır.
C++ dilinin yüksek seviyesine rağmen yine de C dilinin bazı temel
özelliklerinin bilinmesi lazımdır. C dilinin bilinmesi gereken
özellikleri bir sonraki bölümde anlatılacaktır.
Örnek program parçaları;
1//:B02:Ben_Kimim.cpp
//Kimlik
#include <iostream>
using namespace std ;
int main( ) {
cout<<"Adim:Selami Koclu \n"
"Bornova Izmir\n"
"Boy:1.83 ayak:44\n"
"e-mail:[email protected]";
}
--------------------------------------------------------2//:B02:Daire_alani.cpp
//Dairenin alanini hesaplama
#include <iostream>
using namespace std ;
int main( ){
float yari_cap,alan
;
float pi=3.1415 ;
cout<<"yari_cap gir :" ;
cin>>yari_cap ;
alan=pi*yari_cap*yari_cap ;
cout<<"alan_buyuklugu:"<<alan<<endl ;
}
----------------------------------------------------------3//:B02:Bosluk.cpp
//Metindeki bosluk sayisi
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
using namespace std ;
int main( ){
vector<string> words ;
ifstream in("Bosluk.cpp");
string word;
while(in>>word)
words.push_back(word);
int i;
i=words.size( );
cout<<"bosluk_sayisi: "<<i-1<<endl;
}
----------------------------------------------------------4//:B02:Aranan_kelime.cpp
// belli bir kelimenin sayısını bulma
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
using namespace std;
int main( ){
vector<string> words;
ifstream in("Aranan_kelime.cpp");
string kelime="ozel" ;
string word ;
int i=0;
while(in>>word){
words.push_back(word);
if(word==kelime)i++;}
cout<<"ozel kelimesinden:
"<<i<<"tane var\n"<<endl;
}
--------------------------------------------------------------5//:B02:Ters_metin.cpp
//Verilen metni tersten yazdırma
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
using namespace std;
int main(){
vector<string> words;
ifstream in("Dosya.cpp") ;
string word ;
while(getline(in,word))
words.push_back(word) ;
for(i=words.size(word);i>=0;i--)
cout<<words[i]<<endl ;
}
----------------------------------------------------------------6//:B02:Stringe_ekle
//vectorde yeralan bütün ögeleri ardışık olarak çıkıştan önce stringe
yazdır
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
vector<string> words;
ifstream in("Dosya.cpp") ;
string s,word ;
while(in>>word))
words.push_back(word) ;
s+=word ;
cout<<s ;
for(i=0;i<words.size(word);i++)
cout<<words[i]<<endl;
}
----------------------------------------------------------------------7//"Enter"e her basıştan sonra dosyadan bir satırı ekrana yazma
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
using namespace std ,
int main( ){
vector<string> v;
ifstream in("Dosya.cpp") ,
string line ;
while(getline(in,line)
v.push_back(line) ;
for(int i=0;i<v.size( );i++)
cin>>"enter" ;
cout<<v[i]<<endl;
}///:~
----------------------------------------------------------8//String özelliklerini gösteren program örnekleri;
#include <string>
using namespace std ;
int main( ){
string stringBir("Merhaba") ;
//ascii-z metni kullanim
string stringIki(stringBir) ;
//ikinci string nesnesi kullanimi
string stringUc ;
//ilk deger atamasi; " " icsel olarak
yapildi. Asla
//stringUc( ) islevi olarak
yazmayin!.
}
-----------------------------------------------------------#include <string>
using namespace std ;
int main( ){
string stringBir("Merhaba") ;
string stringIki ;
stringIki=stringBir ;
//stringIki'ye stringBir'i atamak
stringIki="Merhaba" ;
//stringIki'ye C-string'i atamak
}
--------------------------------------------------------------//string ASCII-Z çevrimi; bir önceki örnekte ascii-z string'i bir string
nesnesine
//içsel olarak dönüştürüldü. Bu işlemin tersi otomatik olarak
gerçeklenemez.
//string nesnesinin kendinde saklı olan C-string'ini elde etmek için;
char const* geri
//döndüren c_str( ) üye işlevi kullanılabilir.
#include <iostream>
#include <string>
using namespace std ;
int main( ){
string stringBir("Merhaba") ;
char const *c_String=stringBir.c_str( ) ;
cout<<cString<<endl ;
}
----------------------------------------------------------------#include <iostream>
#include <string>
using namespace std ;
int main( ){
string stringBir("Merhaba") ;
stringBir[6]='A' ;
//simdi "MerhabA"
if(stringBir[0]=='M')
stringBir[0]='m' ;
//simdi "merhabA"
//*stringBir='M' ;
//!hata. derlenemez. string göstergeç
alamaz
stringBir="Merhaba" ;
//at( ) üye islevini kullanalim
stringBir.at(6)=stringBir.at(0) ; //simdi "MerhabM"
if(stringBir.at(0)=='M')
stringBir.at(0)='A'
;
//simdi "AerhabM"
}
----------------------------------------------------------------//karsilastirmalar; iki string'in karsilastirmalari
#include <iostream>
#include <string>
using namespace std ;
int main( ){
string stringBir("Merhaba Dunya") ;
string stringIki ;
if(stringBir!=stringIki)
stringIki=stringBir ;
if(stringBir==stringIki)
stringIki="Baska birsey" ;
if(stringBir.compare(stringIki)>0)
cout<<"stringBir alfabede stringIkiden sonra gelir\n" ;
else if(stringBir.compare(stringIki)<0)
cout<<"stringBir alfabede stringIkiden önce gelir\n" ;
else
cout<<"her iki string aynıdır\n" ;
//secenek olarak;
if(stringBir<stringIki)
cout<<"stringBir alfabede stringIkiden önce gelir\n" ;
else if(stringBir>stringIki)
cout<<"stringBir alfabede stringIkiden sonra gelir\n;
else
cout<<"her iki string aynidir\n" ;
}
-------------------------------------------------------------------//selam.cpp
#include <iostream> // Stream bildirimler
using namespace std ;
int main( ){
cout<<"selam selami"<<
<<8<<"napan"<<endl ;
}
//selam2.cpp
#include <iostream>
using namespace std ;
int main( ){
cout<<"selam ben selami"<<endl ,
cout<<"iki kurdum var"<<endl ;
cout<<"nejat kim o"<<5<<",ve<<endl ;
cout<<"cafer12"<<endl ;
cout<<"(daha iyiyim!)"<<endl ;
}
selam ben selami
iki kurdum var:
nejat o kim 5 ve
cafer 12.
(daha iyiyim!)
---------------------------------------------------------------------------------------------------------------------------------------------------------
//alan.cpp
#include <iostream>
using namespace std;
int main() {
const float pi = 3.141592654;
cout << "yaricap gir: ";
float radius;
cin >> radius;
cout << "alan " << pi * radius * radius << endl;
}
/*
yaricap gir: 12
alan 452.389
*/ ///:~
-------------------------------------------------------------------------------
//kelimeler.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream f("kelimeler.cpp");
int nwords = 0;
string word;
while (f >> word)
++nwords;
cout << "kelime sayisi = " << nwords << endl;
}
/* Output:
kelime sayisi = 41
*/ ///:~
------------------------------------------------------------------------------------
//: kelimeler2.cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
int nwords = 0;
string word;
while (cin >> word) {
++nwords;
}
cout << "kelime sayisi = " << nwords << endl;
}
/*
kelime sayisi = 40
*/ ///:~
-------------------------------------------------------------------------------------------------------------------------------------------
//:KelimeSayisi.cpp
// ayni kelimenin yinelenme sayisi
#include <iostream>
#include <fstream>
#include <string>
int main(int argc, char* argv[]) {
using namespace std;
// surec komut satiri degiskenleri:
if (argc < 3) {
cerr << "kullanim: KelimeSayisi dosyasi\n";
return -1;
}
string word(argv[1]);
ifstream file(argv[2]);
// yinelenme sayisi:
long wcount = 0;
string token;
while (file >> token)
if (word == token)
++wcount;
// neticeyi yaz:
cout << '"' << kelime << "\" "
<< wcount << " defa belirdi\n";
} ///:~
"kelime" 3 defa belirdi
------------------------------------------------------------------------
//Fillvector.cpp
// dosyanin tamamini bir string vectoruna yerlestir
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
int main() {
vector<string> v;
ifstream in("Fillvector.cpp");
string line;
while(getline(in, line))
v.push_back(line);
// tersten yaz:
int nlines = v.size();
for(int i = 0; i < nlines; i++) {
int lineno = nlines-i-1;
cout << lineno << ": " << v[lineno] << endl;
}
} ///:~
------------------------------------------------------------------------------
//: S02:Fillvector2.cpp
// dosyayi string vectora kopyala
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
int main() {
vector<string> v;
ifstream in("Fillvector2.cpp");
string line;
while(getline(in, line))
v.push_back(line);
// satirlari tek bir stringe koy:
string lines;
for(int i = 0; i < v.size(); i++)
lines += v[i] + "\n";
cout << lines;
} ///:~
--------------------------------------------//FileView.cpp
// her sefer dosyadan bir satir goster
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ifstream in("FileView.cpp");
string line;
while(getline(in, line)) {
cout << line; // endl yok!
cin.get();
}
} ///:~
------------------------------------------------------------------------------------------
//FloatVector.cpp
// floatlardan vector olustur
#include <iostream>
#include <vector>
using namespace std;
int main() {
// vector doldur:
vector<float> v;
for (int i = 0; i < 25; ++i)
v.push_back(i + 0.5);
// goster
for (int i = 0; i < v.size(); ++i) {
if (i > 0)
cout << ' ';
cout << v[i];
}
cout << endl;
}
/* Output:
0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5 11.5
12.5 13.5 14.5 15.5 16.5 17.5 18.5 19.5 20.5 21.5
22.5 23.5 24.5
*/ ///:~
----------------------------------------------------------//FloatVector2.cpp
// vectorleri toplama
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<float> v1, v2;
for (int i = 0; i < 25; ++i) {
v1.push_back(i);
v2.push_back(25-i-1);
}
// toplam:
vector<float> v3;
for (int i = 0; i < v1.size(); ++i)
v3.push_back(v1[i] + v2[i]);
// goster:
for (int i = 0; i < v1.size(); ++i) {
cout << v1[i] << " + " << v2[i]
<< " = " << v3[i] << endl;
}
}
/* netice:
0 + 24 = 24
1 + 23 = 24
2 + 22 = 24
3 + 21 = 24
4 + 20 = 24
5 + 19 = 24
6 + 18 = 24
7 + 17 = 24
8 + 16 = 24
9 + 15 = 24
10 + 14 = 24
11 + 13 = 24
12 + 12 = 24
13 + 11 = 24
14 + 10 = 24
15 + 9 = 24
16 + 8 = 24
17 + 7 = 24
18 + 6 = 24
19 + 5 = 24
20 + 4 = 24
21 + 3 = 24
22 + 2 = 24
23 + 1 = 24
24 + 0 = 24
*/ ///:~
v3[i] = v1[i] + v2[i];
// hata!
// toplam:
vector<float> v3;
v3.resize(v1.size()); // on yerlesim alani
for (int i = 0; i < v1.size(); ++i)
v3[i] = v1[i] + v2[i];
---------------------------------------------------------------------------------------
//FloatVector3.cpp
// float vector kareleri
#include <iostream>
#include <vector>
using namespace std;
int main() {
// koy yazdir:
vector<float> v;
for (int i = 0; i < 25; ++i)
v.push_back(i);
for (int i = 0; i < v.size(); ++i) {
if (i > 0)
cout << ' ';
cout << v[i];
}
// kareleri yazdir:
for (int i = 0; i < v.size(); ++i)
v[i] = v[i] * v[i];
for (int i = 0; i < v.size(); ++i) {
if (i > 0)
cout << ' ';
cout << v[i];
}
} ///:~
Derleyiciniz uygunsa aşağıdaki ifadeyi kullanabilirsiniz.
#define for if(0); else for
3. BÖLÜM
C++ daki C
C++ dili C dilini esas alarak geliştirilmiş bir dildir. Aslında C diline nesnel
özellikler kazandırılmasından başka birşey değildir. Bununla berarber C
dili ile C++ dili tamamen farklı dillerdir. C dili yapısal bir dildir. C++ ise
nesne yönelimli bir dil. Dikkat edelim nesne tabanlı demedik nesne
yönelimli dil dedik. Nesne tabanlı bir dil tamamen statik özellikler taşır
ama nesne yönelimli dil de dinamik özelliklerde bulunur. Bununla beraber
C++, C dilinin birçok yazım kuralını aynı şekilde kullanır. O nedenle C
dilinin sözdizimi bilinmesi gerekir. Aynı, edebiyatı bilmek için alfabeyi
bilmek gibi.
C dili hakkında hiçbir bilginiz yoksa, bu bölüm tam size göre. Yok eğer, C
dili ile ilgili herşeyi biliyorum derseniz, bu bölümü atlayabilirsiniz. Tekrar
belirtelim C dili ile C++ dili aynı diller değillerdir ve C++ dilini bağımsız
bir dil olarak olarak düşünüp programları sadece C++ dili kurallarına göre
yazmak programları daha başarılı kılar. Fakat C dilinin daha eski olması ve
elde onunla ilgili kütüphanelerin fazlalığı, gerektiğinde bu kütüphanelerin
C++ derleyicisi ile kullanılabilmesi yüzünden C dili öğrenilmelidir.
Daha önce C dili ile hiç karşılaşmamış olanlara, C++ da kullanılan C dili
altyapısı bu bölümde ayrıntılarıyla anlatacağız. Kernighan ve Ritchie'nin
ilk baskısındaki C ile tanıştıysanız, Standart C de olduğu gibi, C++daki
değişiklikleri ve farkları da göreceksiniz. Standart C yi biliyorsanız, bu
bölümün fazlalıklarını atlayarak C++ özelliklerine bakmanız yeterlidir.
Hemen belirtelim burada, C++ nın bazı temel özelliklerine girişte
yapılacak. Bunlar ya C dekilere akraba, veya C dekilerin değiştirilmişidir.
C++ nın diğer ayrıntıları ilerleyen bölümlerde açıklanacaktır.
C yapıları şimdi hızlı şekilde incelenecek, C++ yapıları ise sadece
tanıtılacaktır. Anlatılan yapılar hakkında, önceden bildiğiniz dillerden
kaynaklanan bilginiz size yararlı olabilir.
İşlev oluşturma
Eski C (Standart öncesi C) de, işlevleri herhangi bir sayı, veya tipte
değişkenlerle kolaylıkla çağırabilirdiniz, derleyici
bundan rahatsız olmazdı. Programı çalıştırana kadar herşey çok iyi giderdi.
Çalıştırınca acayip sonuçlar alınırdı (kötü veya sistem çökerdi.). Niçin
böyle olduğu hususunda herhangi bir ipucu olmazdı. Değişkenlerden
kaynaklanan yardım eksiklikleri ve esrarengiz hatalar, standart öncesi C
dilini "yüksek seviyeli assembly dili" haline sokmuştu.
Standart C ve C++ dilleri işlev öntiplemesi özelliğine sahiptir. İşlev
öntiplemesi ile, işlev bildirimi ve tanımlamasında
değişken tiplerinin açıklanması şarttır. Bu açıklama işlemi "öntiplemedir".
İşlev çağrıldığı zaman derleyici öntiplemeyi, işleve uygun değişkenlerin
yerleşmesi, ve doğru geri dönüş tipini elde etmek amacıyla kullanır. İşlev
çağrılması esnasında, programcı hata yaparsa, derleyici bu sayede hatayı
derhal yakalar.
Bir önceki bölümde işlev öntiplemesini temel olarak öğrendiniz. C++da
işlev bildirimi, uygun öntipleme gerektirir.
Bir işlev öntiplemesinde; işlevdeki değişken listesinde, değişken tiplerinin
ve değişken belirteçlerinin bulunması
zorunludur. Değişkenlerin tipi ve sıralaması, bildirim tanım ve işlev
çağrımındakilere uyumlu olmalıdır. Şimdi bir bildirimde işlev öntipleme
örneği verelim;
int translate(float x, float y, float z);
Yukarıdaki örnekte işlevin bütün değişken tipleri float olduğu için işleve
(float x,y,z) şeklinde yazamazsınız.
Her değişkenin tipini ayrı ayrı belirtmek gereklidir. İşlev bildirimi
aşağıdaki şekilde de yazılabilir;
int translate(float, float, float);
Burada derleyici herhangi bir hata bildirmez, işlev çağrıldığı zaman sadece
tipleri sınar. Belirteçlerin eklenmesi kodları
okuyanlara kolaylık sağlar. İşlev tanımlarında değişkenlere atamalar
yapıldığı için isimlendirmeler gerekir.
int translate(float x, float y, float z){
x=y=z ;
/////
}
(Burada hemen bir konuyu açıklayalım; işlevleri irdeleyen bir çok yayında,
ayraç içinde yeralan değişkenlere argüman adı verilmektedir. Argüman
teriminin programcılıktaki anlamı işlev veya işleme aktarılan veri
demektir. O nedenle argüman yerine değişken terimini kullanmayı tercih
ettik. Bundan sonra işleve aktarılan her türlü veriye değişken diyeceğiz.)
Bu kural sadece C diline uygulanır. C++ dilinde işlev tanımında
değişkenler isim almayabilir. İsim vermediğiniz için
işlev gövdesinde de kullanmamanız doğaldır. İsimsiz değişkenler
programcıya, değişken listesinde önceden yer ayırmasına yardım eder.
İşlevi kullanan herhangi bir kişi, yine işlevi doğru değişkenlerle çağırmak
mecburiyetindedir. İşlevi oluşturan kişi, işlevi çağıran kodlarda değişiklik
yapmaksızın, daha sonra işlev değişkenini kullanabilir. Listedeki değişkeni
gözardı etme seçeneği, ismi sadece ayraç içinde unutarakta sağlanabilir. O
zaman işlevin her derlenişinde, kullanılmayan değerler için uyarı iletileri
alırsınız. Uyarı iletilerini ortadan kaldırmak için, kullanılmayan değişken
isimlerini silmek yeterlidir.
C ve C++ da değişken isim listelerinin bildiriminde, iki değişik yol daha
vardır. Boş bir değişken listeniz varsa bu
C++ da; func( ) olarak gösterilir ve anlamı; ey derleyici burada hiç
değişken yok demektir. Dikkat edilmesi
gereken nokta, sadece C++
da boş değişken listesi olabilir. Bu işlevin C dilindeki anlamı ise
bilinmeyen sayıda çok
değişken var demektir. Aslında bu anlam C dilinin en büyük açıklarından
biridir. Böyle bir yaklaşım C dilinde tip sınamasını engellemektedir. C ve
C++ dillerinin her ikisindede func(void) bildirimi aynı anlama gelir.
Anlamı da; boş değişken listesi demektir. void anahtar kelimesi bu
durumda "hiçbirşey" anlamındadır. (daha sonra göstergeçlerle
kullanıldığında, tipi yok anlamındadır.)
Öteki seçenekte; kaç tane değişken olduğunu bilmediğiniz de, veya sahip
olunan değişken tipleri bilinmediğinde ortaya çıkar. Bu durumda "esnek
değişken" listesi kullanılır. Bu belirsiz değişken listesi eksikliklerle
gösterilir(... ). Esnek
değişken listesine sahip bir işlevi tanımlamak, düzgün bir işleve göre
oldukça karmaşıktır. Belli sayıda değişkene sahip
bir işlev için, bazı nedenlerden işlev öntipleme hata sınamalarını kaldırmak
istediğinizde, esnek değişken listesi
kullanabilirsiniz. Bu nedenle esnek değişken listesi kullanımını, sadece C
dili ile sınırlamalısınız, bu tür listeleri C++
da kullanmamalısınız.(ilerde C++ da başka seçenekler olduğu görülecek).
Burada anlatılan esnek değişken listeleri, kullandığınız C dili kılavuzunun
kütüphane bölümünde, daha iyi anlatılmış olabilir.
İşlev geri dönüş değerleri
Bir C++ dili işlev öntiplemesi, işlevin geri dönüş tipini mutlaka
belirtmelidir. (C dilinde belirtilmediğinde, geri dönüş tipi olarak int
alınır.). Geri dönüş tipi, işlev isminden önce yazılır. Herhangi bir değer
döndürülmeyecekse
void anahtar kelimesi kullanılır. Bu durumda da işlevden bir değer
döndürülmeye çalışılırsa hata üretir. Aşağıda işlev öntipleme örnekleri
göreceksiniz;
int f1(void)
//geri dönüşü int, değişkenleri yok
int f2( )
//geri dönüşü int, değişkenleri yok, C++
geçerli C de değil
float f3(float, int, char, double) //geri dönüşü float
void f4(void)
yok
//hiçbirşey geri döndürmez ve değişkenleri
Bir değeri bir işlevden geri döndürmek için, return deyimi kullanılır.
return deyimi işlevden çıkarak işlevin çağrıldığı
yerin hemen sonrasına, geri dönen değeri yerleştirir. return deyimi
değişkene sahipse, o değişken, işlevin geri dönüş değeri olur. Bir işlev hep
aynı tipte geri dönüş ifade ederse, o zaman her return deyimi, o tipte geri
dönüş değeri
gönderir. İşlev tanımlarında birden fazla return deyimi kullanabilirsiniz.
//:B03:Return.cpp
//"return" kullanımı
#include <iostream>
using namespace std;
char cfunc(int i){
if(i==0)
return 'a';
if(i==1)
return 'g';
if(i==5)
return 'z';
return 'c';
}
int main( ){
cout<<"ondalik sayi yaz: ";
int sayi ;
cin>>sayi;
cout<<cfunc(sayi)<<endl;
}///:~
Burada cfunc(int i) deki ilk if deyiminde, sayı sıfıra eşitse, true olduğu için
işlevden return 'a' değeri ile çıkılır.
Burada işlev tanımı main( ) den önce verildiği için, işlev bildirimi
gerekmemiştir. Böylece derleyici daha baştan işlev hakkında bütün
bilgilere sahip olmuştur.
C işlev kütüphanesinin kullanımı
C++ ile programlama yaparken, C işlev kütüphanesinde bulunan bütün
işlevleri kullanabilirsiniz. Kendi işlevlerinizi tanımlamadan evvel, işlev
kütüphanelerini ayrıntılı incelemek, size uygun çözüme rastlama
olanaklarını arttırdığı gibi, program yazımını kolaylaştırır. Aslında
kütüphaneler sorununuza tam çözüm sağlamasa bile, size program
oluşturmanızda çok değerli bilgiler sunar.
Burada bir uyarımızı iletelim; çok sayıda derleyici hayatı kolaylaştırıcı
standart C de bulunmayan işlevlere sahiptirler.
Eğer uygulamalarınızı başka bir ortama taşımayı hiçbir zaman
düşünmüyorsanız (kim düşünmeyebilir?.), o zaman
bu anılan işlevleri kullanmanızda sakınca yoktur. Hem kolayca
programınızı yazmış olur, hem de derleyicinizin size sağladığı
olanaklardan istifade etmiş olursunuz. Eğer uygulamalarınızın, taşınabilir
yani değişik ortamalara aktarılabilir olmasını istiyorsanız, sadece standart
C kütüphane işlevlerini kullanmak zorundasınız. Belli bir ortama özgü
etkinlikler yapmak istediğiniz de, ilgili kodları belli bir yerde toplamak,
başka bir ortama taşınıldığında kodların kolaylıkla değiştirilmesini sağlar.
C++ da ortama özgü etkinlikler sınıf yapısı içinde saklanarak, en uygun
şekilde yerine getirilir.
Kütüphane işlevini kullanmanın aşamaları şöyledir; programlama
kaynağınızda işlevi bulma. (birçok programlama kaynağı işlevleri alfabetik
olduğu gibi, kategorik olarak ta sınıflar). İşlev ifadesi kodları da
göstermelidir. Kodların bulunduğu kısımın başında, en azından bir tane
#include satırı vardır. Bu satır işlev öntipini taşıyan başlık dosyasını
gösterir. Bu #include satırını dosyanıza aynen kopyalamak, işlevin doğru
bildirimi için yeterlidir. Sözdizimine uygun olarak işlevi, şimdi aynı
şekilde çağırabilirsiniz. Eğer yanlış yaparsanız, derleyici başlıktaki işlev
öntipiyle sizin çağrınızı karşılaştırarak hatayı bulur ve hatanız hakkında
sizi bilgilendirir. Zaten varsayım olarak, ilişimci standart kütüphaneyi
araştırarak, ihtiyacınız olan herşeyi, yani başlık dosyasını eklemeyi ve işlev
çağrımını kendisi yapar.
Kütüphaneci ile birlikte kütüphane oluşturma
Kendi işlevlerinizi de bir kütüphane içinde toplayabilirsiniz. Programlama
paketlerinin çoğunluğu hedef modül gruplarını yöneten bir kütüphaneci ile
birlikte gelmektedir. Her kütüphanecinin kendi komutları vardır, fakat
genel yaklaşım şöyledir; bir kütüphane oluşturmak isterseniz,
kütüphanenizdeki bütün işlevler için işlev öntiplerini içeren bir başlık
dosyası oluşturun. Sonra bu başlık dosyasını, önişlemleyicinin arama
sahasında bir yere yerleştirin. Burası, ya yerel dizindir (böylece #include
"header" ile bulunur), ya da include dizinidir(#include <header> ile
bulunur). Şimdi
bütün hedef modülleri alın ve tamamlanmış kütüphane için onları
isimleriyle kütüphaneciye bırakın.(kütüphanecilerin çoğunluğu ortak
uzantı talebinde bulunmaktadırlar .lib ya da .a gibi). Artık tamamlanmış
kütüphanenizi ilişimcinin bulabileceği öteki kütüphanelerin yanına
yerleştirebilirsiniz. Kütüphanenizi kullanırken komut satırına eklemeniz
gereken bazı şeyler olabilir. Bu sayede ilişimci, çağrılan işlevler için
kütüphanenizi arayacağını bilir. O nedenle elinizdeki sistemin kılavuzuna
bakarak yapacaklarınızı belirlemelisiniz. Zira bunlar sistemden sisteme
değişmektedir.
Çalışma denetimi
Bu bölüm de C++ da çalışma akışını denetleyen deyimler anlatılacaktır. C
veya C++ kodlarını yazıp okumadan evvel,
bu deyimleri mutlaka öğrenmelisiniz.
C++ dili, C dilinin çalışma akış deyimlerinin hepsini kullanır. Bunlar ifelse, while, do while, for deyimleridir. Ayrıca seçim yapma deyimi switch
ve pek tanınmayan goto deyimleri de C++ da kullanılır. Biz goto deyimini
kullanmaktan sakınacağız.
Doğru ve yanlış (true,false)
Bütün koşullu deyimler, akış yönünü saptamak için koşul açıklamasının
doğruluğunu veya yanlışlığını sınarlar.
Koşul açıklamasına örnek olarak A==B yi verebiliriz. Koşul işleci olarak
== kullanılmış olup, A değişkeninin B değişkenine özdeşliğine
bakmaktadır. Bu açıklamadan Boole cebirine göre doğru (true), veya
yanlış (false) üretilir. Bu deyimler yani doğru ve yanlış sadece C++ de
bulunur, C dilinde bulunmaz. Bir C açıklamasında true deyimi sadece sıfır
olmayan anlamını taşır. Öbür koşullu işleçler; <, >, >=, <= dir. Koşullu
deyimler bu bölümün ilerleyen kısımlarında ayrıntılarıyla anlatılacaktır.
if-else
if-else iki şekilde kullanılır; birisi else ile birlikte, öbürü ise else olmadan.
İki şekilde aşağıda gösterilmiştir;
if(açıklama)
deyim
ya da
if(açıklama)
deyim
else
deyim
Açıklamada true veya false belirlenir. Deyimden kastedilen; ya noktalı
virgülle sonlanan basit bir deyim, ya da
zincirli ayraçlarla çevrilmiş basit deyimlerden oluşan bileşik bir deyimdir.
Kitabımız da, deyim terimiyle karşılaştığınızda kastedilen; her zaman ya
basit ya da bileşik bir deyimdir. Aslında deyim yerinde, başka bir if
deyimi de bulunabilir bunda herhangi bir sakınca yoktur. Örnek;
//:B03:Ifthen.cpp
//if ve if-else kullanımı
#include <iostream>
using namespace std;
int main( ){
int i ;
cout<<"bir sayi girin ve "Enter'e " basin"<<endl;
cin>>i ;
if(i>5)
cout<<"5 ten büyük"<<endl ;
else
if(i<5)
cout<<"5 ten küçük"<<endl ;
else
cout<<"5 e eşit"<<endl ;
cout<<"bir sayi girin ve "Enter'e" basin"<<endl ;
cin>>i ;
if(i<10)
if(i>5)
cout<<"5<i<10"<<endl ;
else
cout<<"i=5"<<endl ;
else
cout<<"i>=10"<<endl ;
}///:~
Akış denetimi deyimlerinin gövdelerini düzenlemek yukarıdaki örnekte de
görüldüğü gibi oldukça kolaydır. Deyimlerin nerede başlayıp nerede
bittiğini okuyucu anlayabilir.
while
while, do while ve for deyimleri, döngü denetimleridir. Bunlarda denetim
açıklaması yanlış olana kadar, deyim yinelenir.
while döngüsü aşağıdaki gibidir;
while(açıklama)
deyim
Döngünün başlangıcında açıklama bir defa hesaplanır,daha sonra deyimin
her yinelenmesinden önce, tekrar açıklamaya bakılır. Örneğimizde
yineleme, gizli sayıya veya ctrl-C ye basılana kadar devam edecektir.
//:B03:Tahmin.cpp
//while tanıtımı için sayı tahmini
#include <iostream>
using nameplace std ;
int main( ){
int secret=15;
int guess=0;
while(secret!=guess){
cout<<"sayiyi tahmin et= "<<endl;
cin>>guess;
}
cout<<"tahmin ettin aferin !"<<endl;
}//:~
while daki ayraçtaki koşul açıklaması, örnekte olduğu gibi basit sınamayla
sınırlı değildir. İstediğiniz karmaşıklıkta ve uzunlukta olabilir. Bu
açıklamadan doğru veya yanlış üretilir. Hatta bazen döngünün gövdeye
sahip olmadığı, kodlar görebilirsiniz, sadece noktalı virgülün oluşturduğu
gövdedir bu.
while(/*burada birsürü şey yazılır*/)
;
Böyle durumlarda programcı koşul açıklamasını, sadece sınamayı
gerçeklemek için değil, aynı zamanda iş yapmak içinde kullanmaktadır.
do-while
do-while yapısı ;
do
deyim
while(açıklama);
do-while, while dan farklıdır. Çünkü do-while deyimi-açıklama yanlış
olsa bile- en az birkez işlemlenir. while da ise,
açıklama koşulu ilk seferde yanlış ise, deyim hiçbir zaman işlemlenmez.
Tahmin.cpp programında do-while kullanacak olursak guess değişkeni
için başlangıçtaki atama gereksiz olur.
//:B03:Tahmin2.cpp
//do-while kullanan tahmin programı
#include <iostream>
using namespace std ;
int main(){
int secret=15;
int guess ; //başlatma değeri ataması yok
do{
cout<<"sayiyi tahmin et= "<<endl ;
cin>>guess ;
//başlangıç değeri ataması burada yapılıyor
}while(guess!=secret);
cout<<"bravo buldunuz tebrikler.."<<endl;
}///:~
Programcıların bazıları çeşitli sebeplerden do-while yerine while deyimini
tercih etmektedirler.
for
for döngüsünde ilk yinelemeden önce başlangıç değeri ataması yapılır. Her
yinelemeden sonra, koşul sınaması işlemi yerine getirilir. Aslında yapılan
bir çeşit adımlama işlemidir. for döngü yapısı aşağıda verilmiştir;
for(başlatma; koşul; adım)
Başlatma, koşul ve adım açıklamalarından herhangi biri boş olabilir.
Başlatma kodu en başta birkez işlemlenir.
Koşul her yinelemeden önce birkez sınanır (başta false olursa deyim hiçbir
zaman işlemlenmez). Her döngü sonunda
adım, açıklamaya uygun adımı atar.
for döngüleri "sayma" görevlerinde yaygın olarak kullanılır.
//:B03:Charlist.cpp
//for kullanım gösterisi
#include <iostream>
using namespace std;
int main( ){
for(int i=0; i<128; i=i+1)
if(i!=26)
//ANSI ekran silme karakteri
cout<<"değeri :"<<i
<<"karakter : "
<<char(i)
<<endl;
}///:~
Yukarıdaki örnekte dikkat edilmesi gereken, i nin tipinin, kullanıldığı yer
olan if ayraclarının içinde tanıtılmasıdır.
Bilindiği gibi gelenksel dillerde (C de dahil) tip belirleme zincirli ayraç ({)
ilk açıldıktan hemen sonra yapılmaktaydı.
Değişkenlerin tamamı bir bütün halinde bildiriliyordu. Konu bölümün
ilerleyen kısımlarında ele alınacak.
break ve continue anahtar kelimeleri
Herhangi bir (do-while,while,for) döngü yapısının içinde, döngü akış
denetimi, break veya continue ile yapılabilir.
break deyimi döngünün geri kalan deyimlerini işlemlemeden döngüden
çıkarır. continue deyimi ise bulunulan
yineleme işlemini durdurur ve yeni bir yineleme için döngünün başına
gider. Aşağıdaki program break ve continue
örneği için, basit menü sistemi oluşturur.
//:B03:Menu.cpp
//Basit mnü programı gösterisi
#include <iostream>
using namespace std;
int main( ){
char c;
while(true){
cout<<" MAIN MENU :"<<endl;
cout<<"l:left,r:right,q:quit --->";
cin>>c ;
if(c=='q')
break;
if(c=='l'){
cout<<"LEFT MENU :"<<endl;
cout<<" a ya da b yi sec " ;
cin>>c ;
if(c=='a'){
cout<<"'a' yı sectiniz"<<endl;
continue ;
}
if(c=='b'){
cout<<" 'b' yi sectiniz"<<endl;
continue ;
}
else{
cout<<"herhangi birisini secmediniz"<<endl;
continue;
}
}
if(c=='r'){
cout<<"RIGHT MENU :"<<endl;
cout<<" 'c' veya 'd' den birini secin ";
cin>>c;
if(c=='c'){
cout<<" 'c' yi sectiniz :"<<endl;
continue;
}
if(c=='d'){
cout<<" 'd' yi sectiniz :"<<endl ;
continue ;
}
else{
cout<<" 'c' veya 'd' den herhangi birini secmediniz :"
<<endl ;
continue ;
}
}
cout<<"l, r veya q dan birini yazmalısınız "<<endl;
}
cout<<"menuden cıkılıyor "<<endl ,
}///:~
Ana menü de 'q' seçilirse öbür işlemlere geçilmeden çıkış yapılır. Başka
durumlarda program belirsiz şekilde çalışmaya devam eder. Alt menü
seçimlerinde continue anahtar kelimesine rastlandığında while
döngüsünün başına gidilir.
while(true) deyiminin anlamı; bu döngüyü sonsuza dek yap demektir.
break deyimi, kullanıcı 'q' ya bastığı zaman bu sonsuz döngüden
çıkılmasını sağlar.
switch
switch deyimi kendisi ile bitişik açıklamanın değerine göre kodlar arasında
seçim yapar. Yapısı aşağıdadır;
switch(seçici){
case seçilen-değeri1: deyim ; break ;
case seçilen-değeri2: deyim ; break ;
case seçilen-değeri3: deyim ; break ;
(.........)
default: deyim ;
}
Burada seçici, değer üreten bir açıklamadır. switch deyiminin işlevi,
seçilen değerle seçicinin değerini karşılaştırmaktır
Eşitlik olunca eşitliğin bulunduğu yerdeki deyimler işlemlenir. Eşitlik
olmadığında default taki deyimler işlemlenir.
Yukarıdaki örnekte her case in bir break deyimiyle sonlandığı
görülmektedir. Buradaki break deyimi, işlemi switch gövdesinin sonuna
gönderir.Yani switch i kapatan zincirli ayraca gider. break aslında bir
seçenektir,şart değildir.
Eğer break deyimi olmazsa takip eden satırdaki deyim işlemlenir. Yani bir
sonraki case deyimine sıra gelir . Bir
break deyimine rastlayana kadar, case deyimleri işlemlenir. Bu tür
davranış genellikle tercih edilmemesine rağmen
deneyimli programcılar tarafından kullanılabilir.
switch deyimi çoktan seçim yapabilmenin en uygun yoludur (yani çok
sayıdaki seçenekten uygun olanı seçerek akışı sağlamak). Fakat switch in
yanında derleme sırasında bir değere sahip olması gereken seçiciye gerek
vardır. Eğer bir string nesnesini örnek olarak seçicide kullanmak
isterseniz, string nesnesi seçicide çalışmaz. Ama string seçicisi,
çok sayıda string koşullu if deyiminin ilgili string lerini karşılaştırma da
kullanılabilir. switch, Menü örneği için oldukça güzel çözüm üretir;
//:B03:Menu.cpp
//switch deyimi ile menü çözümü
#include <iostream>
using namespace std ;
int main( ){
bool quit=false ;
while(quit==false){
cout<<"a,b,c ya da q yu cıkıs icin secin :";
char response ;
cin>>response ;
switch(response){
case 'a' :cout<<" 'a' yı sectiniz"<<endl;
break ;
case 'b' :cout<<" 'b' yi sectiniz"<<endl;
break ;
case 'c' :cout<<" 'c' yi sectiniz"<<endl;
break ;
case 'q' :cout<<" cikis menusu"<<endl;
quit=true;
break;
default :cout<<"lutfen 'a','b','c' veya 'q' dan birini kullanin !
"
<<endl;
}
}
}///:~
Boolean'ın kısaltması olan bool ifadesi sadece C++ da bulunur, burada
quit bayrağının değerini tanımlamaktadır.
Bunlar sadece true ya da false değerlerini alırlar. 'q' nun seçilmesi, quit
bayrağını true yapar. Gelecek sefer, seçicide quit=false olacağından while
gövdesi tekrar işlemlenmez.
goto'nun doğru ve yanlış kullanımları
C dilinden çıkarılmış olan goto deyimi C++ tarafından desteklenmektedir.
Aslında zayıf programlama biçimi oluşturduğu için uzun süredir
terkedilmiş sayılabilir. Eğer goto deyimini kullandığınız programlarınız
varsa,
bu deyimin yerine daha uygun yaklaşımları tercih etmeniz salık verilir.
Fakat nadiren de olsa, bazen başka seçenek olmadığında goto deyimi
kullanılabilir. Tabii dikkatli olmak koşuluyla. Aşağıdaki örnek o nedenle
dikkatle incelenmeli;
//:B03:GotoKeyword.cpp
//bilinmeyen goto anahtar kelimesi örneğinin C++ da gösterimi
#include <iostream>
using namespace std;
int main( ){
long val=0;
for(int i=1;i<1000;i++){
for(int j=1;j<100;j+=10){
val=i*j;
if(val>47000)
goto bottom;
}
}
bottom:
cout<<val<<endl;
}//:~
//dıştaki 'for'a gönderir.
Yukarıdaki örnekte başka çözüm seçeneği; boolean değişkenini dış 'for'
döngüsünde kullanıp sınamak, ve iç döngüden çıkmaktır. Eğer çok sayıda
if ve while kullanırsanız zayıf program yapısı oluşturursunuz.
Özyineleme (kendini çağırma)
İşlevin kendini kendisine çağırma olan özyineleme ilginç ve oldukça
yararlı bir programlama tekniğidir. Tabii hepsi bu kadar değil, işlev
bellekten çıkana kadar saklanmalı ve özyinelemeli çağrıdan, alttan çıkma
yolu bulmalısınız. Aşağıdaki
örnekte bu çıkış işlemi cat değeri 'Z'yi aştığında gerçekleniyor.
//:B03:CatsInHats.cpp
//özyinelemenin tanıtımı
#include <iostream>
using namespace std;
void removeHat(char cat){
for(char c='A';c<cat;c++)
cout<<" ";
if(cat<='Z'){
cout<<"cat"<<cat<<endl;
removeHat(cat+1) ;
//özyinelemeli çağrı
}else
cout<<"vaybe!!!"<<endl;
}
int main( ){
removeHat('A') ;
}///:~
removeHat( ) işlevinde cat değişkeni 'Z' den daha küçük olmalıdır. Böylece
removeHat( ) işlevi removeHat( ) içinden
çağrılarak özyineleme gerçeklenir. removeHat( ) işlevinin her çağrılışında,
cat değeri 1(bir) artar. Böylece değişken artışı sağlanır.
Özyineleme belli özellikteki karmaşık sorunları çözümlemekte kullanılır.
Zira bu tür çözümleri sorunun genişliğine bakmaksızın yapmak gerekir.
Özyineleme işlev çağırmayı sorun bitene kadar devam ettirir.
İşleçlere giriş
İşleçler özel tip işlevler olarak düşünülmelidir (C++ da işleç
bindirimlerinin bu yolla gerçeklendiğini göreceksiniz.). Bir işleç bir veya
daha fazla değişken alır ve yeni bir değer üretir. Değişkenler klasik
işlevlerdeki gibi olmayıp, farklı biçimlerdedir, fakat etkileri aynıdır.
Önceki programlama deneyimlerinizden yararlanrak işleçleri kolayca
kullanabilirsiniz. Zira bütün programlama dillerinde toplama (+), çıkarma
ve eksi işareti (-), çarpma (*), bölme (/) ve atama (=) işleçleri aynıdır.
Anlamları bütün programlama dillerinde aynıdır. İşleçlerin tamamı bu
bölümün ilerleyen kısımlarında gösterilecektir.
Öncelik
İşleç önceliği; bir açıklamada birden fazla işleç olduğunda, hesaplamada
uygulanacak sıradır. C ve C++
hesaplama sırası için özel kurallar uygularlar. Hatırlanması en kolay olan;
çarpma ve bölme, toplama ve çıkarmadan
önce yapılır. Bundan başka, bir açıklama size yeteri kadar saydam
gelmiyorsa, olasıdır ki onu başkaları da okuyamayacaktır, o nedenle
hesaplama sırasını, ayrıntılarıyla belirtmek için ayraçlar kullanılır. Örnek
olarak;
A=X+Y-2/2+Z ;
açıklaması ayraçlara alındığında oldukça farklı anlam taşır;
A=X+(Y-2)/(2+Z) ;
(örnekleri X=1,Y=2,Z=3 deneyerek karşılaştırın.)
Kendiliğinden artış ve azalış
C ve dolayısı ile C++ çok sayıda kısaltma işaretine sahiptir. Kısaltmalar
bazen kodun okunmasını zorlaştırsalar da,
kod yazımını oldukça kolaylaştırırlar. Herhalde C dilinin tasarımcıları,
gözlerinizle geniş bir alanı tarayıp kod arayacağınıza, kısa kodların daha
kolay olacağını düşündüler.
Kısaltmaların en iyilerinden biri, kendiliğinden artma ve azalmayı
sağlayanlardır. Bunlar bir döngünün kaç kere çalışacağını denetleyen
döngüdeki değişkenleri değiştirirler. Kendiliğinden azalma işleci (--) dır.
Anlamı bir birim azal demektir. Kendiliğinden artma işleci (++)dır. Anlamı
bir birim art demektir. Örnek olarak A bir integer ise; (++A) ile
(A=A+1) özdeştir. Kendiliğinden artma ve azalma işleçleri, sonuç olarak
değişkenin değerini üretirler. İşleç değişkenden önce gelirse (yani ++A)
önce işlem gerçeklenir, sonra sonuç üretilir. İşleç değişkenden sonra
gelirse (yani A++) önce o anki değer üretilir, daha sonra işlem gerçeklenir.
Örnek;
///:B03:AutoIncrement.cpp
///Kendiliğinden artış ve azalış işleç kullanımları gösterilmiştir.
#include <iostream>
using namespace std ;
int main( ){
int i=0 ;
int j=0 ;
cout<<++i<<endl
cout<<j++<<endl
cout<<--i<<endl
cout<<j--<<endl
}///:~
;
;
;
;
//kendiliğinden artım önce,sonra sonuç
//sonuç önce ,kendiliğinden artım sonra
//kendiliğinden azalış önce,sonuç sonra
//sonuç önce,kendiliğinden azalış sonra
Başka uygulamalı örnek verelim;
B=3 ;
A=++B ; //A=4 ve B=3
C=3 ;
D=C++ ; //D=3 ve C=4
Şimdi C++ nın ne anlama geldiğini herhalde anlamışsınızdır, C den bir
adım ilerde demektir.
Veri tiplerine giriş
Veri tipleri, yazdığınız programlarda belleğin kullanım şeklini belirler. Bir
veri tipi belirttiğinizde, derleyiciye söylediğiniz; onun bellek parçasının
nasıl oluşturulacağı ve ayrıca o belleğin nasıl kullanılacağıdır.
Veri tipleri yerleşik veya soyut olabilir. Yerleşik veri tipi, derleyicinin
dışarıdan herhangi bir açıklama ihtiyacı olmadan
anlayabildiği veri tipleridir. Kullanıcı, yerleşik veri tipini doğrudan
derleyiciye sunabilir. C ve C++ da ki yerleşik veri tipleri hemen hemen
birbirlerinin aynıdır. Kullanıcı tanımlı yani soyut veri tipini, siz veya başka
bir programcı, sınıf olarak oluşturur. Bunlar yaygın olarak soyut veri tipleri
olarak adlandırılırlar. Derleyiciler baştan yerleşik veri tiplerini nasıl
kullanacaklarını bilirler. Soyut veri tiplerini ise, başlık dosyalarındaki
bildirimleri okuyarak nasıl kullanacaklarını öğrenirler (daha fazla bilgiyi
ilerleyen kısımlarda göreceksiniz).
Temel yerleşik veri tipleri
Yerleşik veri tipleri için, standart C dili (C++ da ondan devraldı) özellikle,
bize bu veri tiplerinin herbirinin kaç bit
tutacakları konusunda bilgi vermez. Bu bilginin yerine, yerleşik veri
tiplerinin en küçük ve en büyük tutabilecekleri değerleri verir. Makine iki
tabanlı sayılara göre işlem yaptığından, tutulabilecek en büyük değere
denk gelen, en az bit sayısı, doğrudan çevrilerek elde edilebilir. Makinenin
kullandığı sayılar BCD denilen ikili kodlanmış onluklar halinde
ise, o zaman her veri tipi için tutulabilecek en büyük değere karşılık gelen,
makinedeki kullanılan bellek miktarı farklı
olacaktır. Değişik veri tiplerinin sakladığı en büyük ve en küçük değerler,
limits.h ve float.h sistem başlık dosyalarında
tanımlanır (C++ da #include <climits> ve #include <cfloat> ile
tanımlanır).
C ve C++, ikili tabana dayalı makinelerde 4 (dört) temel yerleşik veri
tipine sahiptirler. char veri tipinin en az 8 (sekiz) bit yani 1 (bir) byte
saklama sığası vardır. Fakat daha fazla da olabilir. int in ise en az 2 (iki)
byte saklama sığası vardır.
float ve double veri tipleri kayan noktalı sayıları saklarlar. Genellikle
IEEE kayan noktalı sayılar biçimindedirler.
float tek hassasiyetli kayan noktalı sayıları, double ise çift hassasiyetli
kayan noktalı sayıları gösterirler.
Daha önce anlatıldığı gibi, değişkenleri, programda kullanılmadan evvel
herhangi bir yerde tanımlayabilir,
ve hatta, hem tanımlayabilir hem de değer atayarak başlatabilirsiniz.
Aşağıda örnekte 4 (dört) temel yerleşik veri tipini (int, float, char,
double) kullanarak değişken tanımlamayı göreceksiniz.
///:B03:Basic.cpp
///C ve C++ da bulunan 4 temel veri tipinin
///tanımlanması
int main( ){
//ilk değer atamadan tanım
char protein ;
int carbonhydrates ;
float fiber ;
double fat ;
//başlatma ve tanım aynı anda
char pizza='A', pop='Z' ;
int dongdings=100, twinkles=150,
heehos=200
;
float chocolate=3.14159
;
//eksponensiyal gösterim
double fudge_ripple=6e-4 ;
}///:~
Programın ilk bölümünde 4 temel yerleşik veri tipi değişkenleri, değer
ataması yapmadan tanımlanmaktadır. İkinci bölümde ise aynı anda, hem
değişkenlerin veri tipleri tanımlanıyor, hem de başlatma değerleri atanıyor
(Aslında tanımın olduğu yerde değer ataması yapmak en iyisidir).
Eksponensiyal sayının değeri; 6e-4=6*(10)**4 tür.
bool, true, &false
bool standart C++ ye eklemlenmeden önce, herkes Bool biçimi neticeler
üretmek için, değişik teknikler kullanıyorlardı. Bunlar çoğu kez
uygulamanın değişik ortamlara taşınmasında sorun yaratıyordu, ayrıca pek
farkedilemeyen hatalara da yol açıyorlardı. Standart C++ da bool veri
tipleri yerleşik sabitler olan iki durumla açıklanırlar; true (1-(bir)e karşılık
gelir) ve false(0-(sıfır)a karşılık gelir). bool, true ve false un her üçü de
anahtar kelimelerdir. Ek olarak başka dil ögeleri de aşağıda gösterilmiştir.
ÖĞE
bool İLE KULLANIMI
&&,||,! -------------- bool değişkenlerini alır ve bool sonuçları üretir
<,>,>=,<=,}
==,!=
} --------- bool sonuçları üretirler
if,for,
}
while,do }----------- koşullu açıklamalar bool değerlerine çevrilir
?:
------------ ilk işlenen bool değerine çevrilir
int in bir bayrağı (flag) gösterdiği çok sayıda kod vardır, derleyici o
zaman kendi içinde, int i bool a çevirir (sıfır olmayan değerler true ,sıfır
olan değerler de false üretirler). İdeal olanda, derleyici durumu
düzeltmeniz için, öneri olarak uyarıda bulunur.
Zayıf programlama biçiminde geçen (++) nın bir bayrağı true yapmak için
kullanımına hala izin verilse de, aslında tavsiye edilmez. Bunun anlamı
gelecek bir dönemde de yasaklanabilir demektir. Derleyici içinde bool'dan
int'e tip çevrimi yapmak, (belki sıfır ve bir olan normal bool değerlerinden
başka ) ve sonra yine tekrar geriye çevirmek sorun oluşturmaktadır.
Ayrıca göstergeçler gerekli olduğunda kendiliğinden bool'a dönüştürülür.
Özelleştiriciler
Özelleştiriciler yerleşik temel veri tiplerinin anlamlarını değiştirirler ve
onları çok daha büyük küme yaparlar.
Dört temel özelleştirici vardır; long, short, signed ve unsigned
long ve short veri tipinin tutabileceği en büyük ve en küçük değerleri
değiştirir. Sade bir int en az short büyüklüğünde olmak zorundadır. Bu
veri tipinin boyut sırası; short int, int, long int şeklindedir. Bütün
büyüklükler makul bir şekilde en büyük/en küçük değer gerekirliğini
yerine geitrdiği sürece hep aynıdır. Örnek olarak; 64 bitlik bir makinede
bütün veri tipleri 64 bit olmalıdır.
Kayan noktalı sayılarda sıralama ise; float, double ve long double dır.
"long float" geçerli değildir. short kayan noktalı sayı yoktur. signed ve
unsigned özelleştiricileri derleyiciye işaret bit'ininin kullanımını belirtir.
unsigned bir sayı işarete sahip değildir, bu nedenle fazladan bir bit'i
vardır. Bu yüzden, işaret (signed) sahibi pozitif sayılara göre iki kat büyük
sayı saklayabilir. signed varsayılan olup sadece char için gereklidir. char
signed'e varsayılan olabilir de olmayabilir de. signed char olarak
özelleştirirseniz, işaret bit'ini zorla kullanmış olursunuz.
Aşağıdaki örnek, veri tiplerinin büyüklüğünü byte olarak vermektedir.
Burada sizeof işleci kullanılmıştır (ileride
sizeof işleci ile ilgili bilgi verilecektir).
//:B03:Specify.cpp
//özelleştirici kullanımın tanıtımı
#include <iostream>
using namespace std;
int main( ){
char c;
signed char cu;
int i;
unsigned int iu;
short int is;
short iis;
unsigned short int isu;
unsigned short iisu;
long int il;
long iil;
unsigned long int ilu;
unsigned long iilu;
float f;
double d;
long double ld;
cout
<<"\n char= "<<sizeof(c)
<<"\n signed char= "<<sizeof(cu)
<<"\n int= "<<sizeof(i)
<<"\n unsigned int= "<<sizeof(iu)
<<"\n short int= "<<sizeof(is)
<<"\n unsigned short= "<<sizeof(isu)
<<"\n long=
"<<sizeof(il)
<<"\n unsigned long= "<<sizeof(ilu)
<<"\n float= "<<sizeof(f)
<<"\n double= "<<sizeof(d)
<<"\n long double= "<<sizeof(ld)
<<endl ;
}///:~
Yukarıdaki programı çalıştırdığınızda bir makineden/işletim
sisteminden/derleyiciyiden ötekine farklı sonuçlar alındığını göreceksiniz.
Tipler de tutarlı görünen sadece standartlardaki en büyük ve en küçük
değerlerin tutulmasıdır.
int tipini short ya da long ile değişikliğe uğrattığınız da int istenirse
yazılır.
Göstergeçlere giriş
Bir programı çalıştırdığınız zaman, program önce bilgisayarın belleğine
(genelde diskten) yüklenir. Böylece programın bütün ögeleri belleğin bir
yerine yerleşir. Bellek genellikle bellek birim dizileri olarak
tertiplenmişlerdir. Yaygın olarak bellek birimleri 8-bite karşılık gelen bayt
(byte) olarak adlandırılmaktadır. Aslında bellek birimlerinin boyutu
makinenin mimarisine bağlı olup, kelime uzunluğu (word size) olarakta
isimlendirilir. Her birim birbirinden adresleri sayesinde ayrılır. Bunu şöyle
açıklayabiliriz; kelime uzunluğu 1 bayt olsun, bu baytları kullanan makine
adresleri, 0 (sıfır) dan başlayıp bilgisayarın sahip olduğu bellek miktarına
kadar çıkar.
Program çalışırken bellekte bulunduğu için, programın her ögesinin bir
adresi olur. Aşağıdaki basit programla işe başlayalım;
////:B03:YourPets1.cpp
#include <iostream>
using namespace std ;
int dog,cat,fish,bird ;
void(int pet){
cout<<"pet id number"<<pet<<endl ;
}
int main( ){
int i, j, k ;
}///:~
Programdaki ögelerin herbiri, program çalıştırıldığı zaman bellekte bir yere
sahiptir. Hatta işlev bile bellekte yeralır.
Sizin tanımladığınız ögenin tipi, bellekte kaplanacak alanı da saptar. C ve
C++ da bulunan bir işleç ögenin adresini bize anlatır. Bu işleç '&' işlecidir.
Yapacağınız bütün iş; öge adının başına '&' işaretini koymaktır. Ve bu da
ilgili ögenin adresini üretir. YourPets1.cpp, ögelerinin herbirinin adresini
yazdıracak şekilde değiştirilebilir. Aşağıdaki gibi;
//:B03:YourPets2.cpp
#include <iostream>
using namespace std ;
int cat,dog,bird,fish ;
void f(int pet){
cout<<"pet id number :
"<<pet<<endl ;
}
int main( ){
int i,j,k ;
cout<<"f( ): "<<long &f<<endl ;
cout<<"cat: "<<long &cat<<endl ;
cout<<"dog: "<<long &bird<<endl ;
cout<<"fish: "<<long &fish<<endl ;
cout<<"i : "<<long &i<<endl ;
cout<<"j : "<<long &j<<endl ;
cout<<"k : "<<long &k<<endl ;
}///:~
burada long bir dökümdür, yani anlamı int'i normal int olarak değil, long
olarak işlemle demektir. Döküm ana kullanım yolu değildir eğer yukarıda
long olmasaydı çıktıda görülen adresler heksadesimal biçiminde olurdu.
long'un kullanılması, çıktıyı daha okunur hale getirir.
Bu programın çıktısı bilgisayardaki işletim sistemine ve öteki etmenlere
bağlıdır. Fakat yine de size ilginç görüntüler verebilir. Benim
bilgisayarımda ki ilk çalıştırma da aşağıdaki sonuçları verdi;
f( ):4198736
cat:4323632
dog:4323636
bird:4323640
fish:4323644
i:6684160
j:6684156
k:6684152
Burada main( ) işlevinin içinde ve dışında tanımlanan değişkenlerin nasıl
farklı bellek alanlarına yerleştiğini görüyorsunuz. Bunun sebebini de dili
öğrenme süreci içinde anlayacaksınız. f( ) işlevinin kodları bellekteki
veriden ayrı yerde, kendi bölgesindedir.
Göze çarpan başka bir ilginç konuda, birbiri ardınca tanımlanan
değişkenlerin bellekte bitişik bulunmasıdır.
Veri tiplerinin belirlediği bayt kadar birbirlerinden ayrıktırlar.
Örneğimizde kullanılan tek veri tipi int'dir ve cat, dog'tan 4 bayt
uzaklıktadır, bird'te, dog'tan ve fish'ten 4 bayt ötededir. Böylece bu
makinede long int 4 bayt uzunluğunda demektir. Başka ilginç bir
deneyimde, belleğin haritalanması ve bellek adresleriyle neler
yapabileceklerinizdir. Yapılabilecek en önemli şey, ilerde kullanmak üzere
belleğe başka bir değişkeni saklamaktır.
C ve C++ göstergeç denilen adres bilgisini tutan özel tip bir değişkene
sahiptir.
Göstergeçi tanımlayan işleç, çarpma işleminin işleciyle(*) aynıdır.
Derleyici hangisinin çarpma, hangisinin göstergeç işleci olduğunu, işlecin
bulunduğu yerden belirler. Bu konunun ayrıntısı ilerde anlatılacak.
Bir göstergeç tanımladığınızda onun gösterdiği değişkenin tipini mutlaka
belirtmek zorundasınız. Tip ismi ile başladıktan sonra hemen tipi tanıtan
ismi yazmazsınız, önce "bekle bir göstergeç var" anlamında yıldız işaretini
basarsınız, sonra tipi tanıtırsınız. Yani yıldız işareti tip ile tanıtıcı arasında
bulunur. Bir göstergeç aşağıdaki gibi görünür;
int* ip ; //ip integer tipinde bir değişkenin adresidir, yani ip adresin
kendisidir.
Tiple * işaretinin yanyanalığı okumada kolaylık sağlasada, sanki tek bir
int* veri tipi gibi yanlış anlamalara da neden olabilmekte. int veya başka
temel veri tipleri ile kullandığınız;
int a, b, c ;
yaklaşımını aslında göstergeçler ile kullanabilirmisiniz?
int* ipa, ipb, ipc ;
C sözdizimi kurallarında (C++ ya da aktarılan) bu tür açıklamalara yer
yoktur. Burada sadece ipa göstergeçtir, ötekiler; ipb ve ipc normal int
değişkenlerdir. Hepsini göstergeç olarak belirtmek için her satırda bir
tanım yapmak en doğrusudur. Yani;
int* ipa;
int* ipb;
int* ipc;
Bu uygulama bütün karmaşayı çözmüş olur. C++ dilinde değişkenlerin
tanımlanmaları sırasında değer atamaları
yapılmasına izin verildiği için, aşağıdaki uygulama çok daha iyidir;
int a=67 ;
int* ipa=&a;
Böylece hem a hem de ipa ya ilk değer ataması yapılmış olur. ipa, a
değişkeninin adresini tutar.
Bir göstergeçe ilk değer atadığınız zaman onunla yapabileceğiniz en basit
şey; gösterdiği adresteki değeri
değiştirmektir. Bir değişkene göstergeçle erişebilmek için aynı işleçi
kullanarız, aşağıya bakınız;
* ipa=200;
değeridir.
//ipa, a değişkeninin adresidir, * ipa ise o adresteki a
artık a değişkeninin değeri 67 değil 200 oldu.
Göstergeçlerin temel özellikleri; tek adres tutabilir, orijinal değişkeni
değiştirmek için o adresi kullanabilir. Fakat soru hala ortada; niçin bir
değişken vekil kullanılarak, başka bir değişkeni değiştirmek istiyorsunuz?
Göstergeçlere başlarken, buna iki geniş kategoride yanıt veririz;
1.- İşlevin içinden "dışarıdaki nesneleri" değiştirmek için. Bu
göstergeçlerin en temel kullanımıdır. Birazdan anlatılacak.
2.- Kitabın ilerleyen bölümlerinde anlatılacak olan, zekice program
teknikleri geliştirmek için.
Dışarıdaki nesneleri değiştirme
Normalde bir işleve bir değişken yerleştirdiğinizde, işlevin içinde o
değişkenin bir kopyası oluşur. Buna değeriyle
aktarım denir. Aşağıdaki örnekte değerle aktarımın etkisini görebilirsiniz.
//:B03:PassByValue.cpp
#include<iostream>
using namespace std ;
void f(int a){
cout<<"a=
a=5 ;
cout<<"a=
}
int main( ){
int x=41 ;
cout<<"x=
f(x) ;
cout<<"x=
}///:~
"<<a<<endl ;
"<<a<<endl ;
"<<x<<endl ;
"<<x<<endl ;
f( ) işlevinin içindeki a yörel değişken olup sadece f( ) işlevi çağrıldığı
zaman vardır. İşlev çağrıldığı zaman a nın değeri aktarılan değişkenlerle
belirlenir. main( ) işlevinin içinde değişken x tir, ve değeri 41 dir. Bu
değer f( ) çağrıldığı zaman
a'ya kopyalanır. Program çalıştırıldığında aşağıdaki sonuçlar alınır;
x=41
a=41
a=5
x=41
Başlangıçta x değerinin 41 olması doğaldır. f( ) işlevi çağrıldığında, a
değişkenini işlev çağrı süresince tutmak için geçici bir bellek alanı
oluşturulur ve başlangıç değeri olarak a'ya x'in değeri kopyalanır, bu
durum çıktıda görülmektedir. Tabii a'nın değerini değiştirebilirsiniz ve
değişikliği gösterebilirsiniz. f( ) çağrısı tamamlandığı zaman
a için yaratılan geçici bellek alanı ortadan kalkar. Daha sonra a ve x
arasında sadece, x'in değerinin a'ya kopyalanması
ile ortaya çıkan ilişki kalır. f( ) in içinde iken, x değişkeni dış nesnedir, ve
değişen yerel değişken dış nesneyi etkilemez.
Zira onlar bellekte iki ayrı yerde bulunurlar. Ama dış nesneyi değiştirmek
isterseniz ne yaparsınız? Yine burada göstergeçleri kullanabiliriz. Bir
anlamda, göstergeç ikinci değişkenin takma adı olur. Eğer bir göstergeçi
bir işleve normal değerinin yerine aktarırsak, gerçekte dış nesneye takma
ad koymuş oluruz, böylece işleve dış nesneyi değiştirme yeteneği
kazandırırız. Aşağıdaki örnekte olduğu gibi;
//:B03:PassAddress.cpp
#include<iostream>
using namespace std ;
void f(int* p){
cout<<"p= "<<p<<endl ;
cout<<"*p= "<<*p<<endl ;
*p=5 ;
cout<<"p= "<<p<<endl ;
}
int main( ){
int x=41 ;
cout<<"x= "<<x<<endl ;
cout<<"&x= "<<&x<<endl ;
f(&x) ;
cout<<"x= <<x<<endl ;
}///:~
f( ) işlevi değişken olarak bir göstergeçi almaktadır, ve atamalarda
göstergeçi kullanır, bu da x dış nesnesinin değişmesine yolaçar. Bu
örneğin çıktısı aşağıdadır;
x=41
&x=0065FE00
p=0065FE00
*p=41
p=0065FE00
x=5
Dikkat edilmesi gereken nokta p'nin değeri x'in adresidir. p göstergeçi x'i
göstermektedir. *p'nin değeri 5 yapıldığı
zaman x'in değerininde 5 olduğu görülecektir.
Böylece işleve göstergeç yerleştirmenin dış nesneleri değiştirebildiği
görülmektedir. İlerde göstergeçlerin çok sayıda kullanım biçimlerini
öğreneceksiniz, fakat şimdi anlatılmış olanlar, en yaygın olarak
kullanılanlardır.
C++ dayançlarına (referanslarına) giriş
Göstergeçler C ve C++ da kabaca aynı şekilde kullanılırlar, ama C++ da
adresin işleve aktarılması için başka bir yol daha vardır. C++ buluşu
olmayan öteki bazı dillerde de kullanılan bu yeni yol, dayançla aktarımdır.
İlk bakıldığında dayançlar gereksiz görünebilir, ve program yazmak için
dayançlara ihtiyaç duyulmayabileceğini sanabilirsiniz. İlerleyen
bölümlerde göreceğiniz bazı çok önemli durumlar haricinde, genellikle bu
doğrudur. Zaten ilerleyen bölümlerde dayançlar hakkında daha ayrıntılı
bilgi edineceksiniz, ama dayançların temel uygulaması yukarıda anlatılan
göstergeçlerle aynıdır. Değişken adresini işleve dayançla aktarabilirsiniz.
Dayançlarla göstergeçler arasındaki fark; dayançlı işlevi çağırma,
göstergeçli çağırmadan daha temiz sözdizimi ihtiva etmesidir (sözdizimi
farkı, bazı durumlarda dayançların tercih sebebidir). PassAddress.cpp
programında dayanç kullanılırsa, fark main( )'deki işlev çağrılarında
görünür.
//:B03:PassReference.cpp
#include <iostream>
using namespace std ;
void f(int& r){
cout<<"r= "<<r<<endl ;
cout<<"&r= "<<&r<<endl ;
r=5 ;
cout<<"r= "<<r<<endl ;
}
int main( ){
int x=41 ;
cout<<"x= "<<x<<endl ;
cout<<"&x= "<<&x<<endl ;
f(x) ;
//sanki değerle atama var gibi görünüyor
fakat referansla atama var.
cout<<"x= "<<x<<endl ;
}///:~
Bu programın çıktısı :
x=41
&x=0065FE00
r=41
&r=0065FE00
r=5
x=5
tir. Burada &r, r değişkeninin adresidir. Çağırılan değişkene adres
aktarılıyor, değerin kopyası yaratılmıyor.
Böylece burada da, referansla aktarımda tıpkı göstergeçle aktarımda
olduğu gibi işlevin dışarıdaki nesneyi değiştirebileceğini gördük (Daha
sonra referansların adres aktarımını gizlediği gerçeğini ilerleyen
bölümlerde göreceksiniz). Bu basit girişten sonra dışarıdaki nesneleri
değiştirmek için göstergeçlerin yaptığını, referanslarında
farklı sözdizimi ile yapabileceğini kabul edebilirsiniz.
Göstergeç ve dayançların değiştirdikleri
Buraya kadar int, char, float, double temel veri tiplerini, ve signed,
unsigned, long, short gibi bu veri tipleriyle kullanılan özelleştiricileri
gördünüz. Şimdi temel veri tiplerine ve özelleştiricilere, doğru
yerleştirilmiş göstergeç ve dayançlarla olasılıkları üçe katlayacağız.
//:B03:AllDefinitions.cpp
//Temel veri tiplerinin,
//özelleştiricilerin, göstergeçlerin, referansların bileşimleri
#include <iostream>
using namespace std ;
void f1(char c, int i, float f, double d) ;
void f2(short int si, long int li, long double ld) ;
void f3(unsigned char ui, unsigned int ui,
unsigned short int usi, unsigned long int uli) ;
void f4(char* cp, int* ip, float* fp, double* dp) ;
void f5(short int* sip, long int* lip,
long double* ldp) ;
void f6(unsigned char* ucp, unsigned int* uip,
unsigned short int* usip,unsigned long int* ulip) ;
void f7(char& cr, int& ir, float& fr, double& dr) ;
void f8(short int& sir, long int& lir,
long double& ldr) ;
void f9(unsigned char& ucr, unsigned int& uir,
unsigned short int& usir, unsigned long int& ulir) ;
int main( ){ }///:~
Dayançlar ve göstergeçler işlevlerin içine ve dışına nesnelerin
aktarılmasını da sağlarlar. Bu konuyu da ilerleyen kısımlarda göreceksiniz.
Göstergeçlerle beraber çalışan başka bir veri tipi daha vardır; void.
Göstergeç tipini void olarak tanımlarsanız
(void*) o zaman oradaki verinin tipini hiçbir zaman bilemezsiniz (eğer
int* derseniz, burada sadece int tipini taşıyan değişken adresi var
demektir). Örnek;
//:B03:VoidPointer.cpp
int main( ){
void* vp ;
char c
;
int i
;
float f ;
double d ;
//herhangi bir tipin adresi void göstergeçine aktarılabilir.
vp=&c ;
vp=&i ;
vp=&f ;
vp=&d ;
}///:~
void* e bu görevlendirme yapıldığı zaman verinin tipi ile ilgili bilgi
kaybolur. Bunun anlamı göstergeçi kullanmadan evvel mutlaka dökümle
tipi yerleştirmelisiniz. Örnek :
//:B03:CastFromVoidPointer.cpp
int main( ){
int i=99 ;
void* vp=&i;
//void* vp ye aktarım yapılamaz.* vp=3 derleme
hatası verir,yeniden düzenlenmeli
*((int*)vp)=3 ;
//döküm yapıldı
}///:~
buradaki (int*)vp dökümü, void*'i alır ve derleyiciye onu int* olarak
işlemlemesini söyler, böylece başarılı bir
şekilde dayançlama yapılmış olur. Farkettiğiniz gibi bu tarz gösterim çirkin
durmaktadır, fakat programda hata
delikleri oluşturmamak için tercih edilmelidir. Aslında bu yaklaşım bir tipe
başka bir tip gibi davranma yeteneği
kazandırıyor.Yukarıdaki örnekte int i int olarak kullanmak için vp yi int*
e döküp kalıplıyoruz. Burada bir şey daha belirtmeliyiz; döküm işleminin
char* ya da double* a yapılıp yapılamayacağı hakkında hiçbir şey
söylenemez,zira
int ile char ya da double'ın bellekte kapladığı alan farklıdır, o nedenle
böyle bir işlem yapılsaydı büyük ihtimalle
program çökerdi. Genellikle çok özel durumların dışında void
göstergeçlerin kullanılmasından kaçınılması tavsiye edilir. Zaten nadiren
kullanılır.
Hiçbir zaman void dayancı olmaz. Bunun nedeni ilerleyen bölümlerde
anlatılacak.
Çalışma alanı (görüntü)
Çalışma alanı kuralları bir değişkenin oluşturulduğu, geçerli olduğu ve
ortadan kalktığı(yani çalışma alanı dışına
çıktığı ) yerleri belirtir. Bir değişkenin çalışma alanı, tanımlandığı
noktadan başlar ve en yakın kapanan zincirli ayraca kadar sürer. Kısaca
çalışma alanı; değişkenin içinde bulunduğu en yakın açılan ve kapanan
zincirli ayraçlardır. Biz bir çok yerde çalışma alanı ile görüntü terimlerini
eşedeğer olarak kullanacağız. Yani çalışma alanı ve görüntü aynı anlamı
taşımaktadır. Örnek olarak A nesnesinin çalışma alanı ile A nesnesinin
görüntüsü aynı anlamdadır.
İncelemek için aşağıdaki örneğe bakınız;
//:B03:Scope.cpp
//Değişkenlerin çalışma alanları
int main( ){
int scp1 ;
//scp1 buradan itibaren faaliyete geçer
{
//scp1 hala faaliyette
//....
int scp2 ; //buradan itibaren çalışır
{
//scp1 ve scp2 hala görünüyor ve faaliyetteler
//..
int scp3 ;
//scp3 te buradan itibaren faaliyette ayrıca scp1 ve scp2 hala
faaliyette
//...
}//scp3 artık faaliyette değil
//scp3 artık yok fakat scp1 ve scp2 faaliyetteler.
//..
}//scp2 de artık faaliyette değil sadece scp1 faaliyette
//artık scp2 ve scp3 yoklar sadece scp1 faaliyette
//..
}//scp1 de artık faaliyet dışı
///:~
Yukarıdaki örnek değişkenlerin görünür olmaya ne zaman başladıkları ve
ne zaman faaliyet dışı kaldıklarını göstermektedir. Bir değişken sadece
çalışma alanı içinde kullanılabilir. Çalışma alanları içiçe olabilir yani biri
diğerine yuvalanabilir. Bu durum içiçe eşlenik zincirli ayraçlarla
gerçeklenir. Yukarıdaki örnekte bu olay gözlemlenmektedir.
Yuvalanmanın anlamı; bulunulan çalışma alanından başka alanın
değişkenine erişim demektir.
Yukarıdaki örnekte scp1 bütün çalışma alanlarına erişmektedir, scp3 ise
sadece kendi en iç çalışma alanında
faaliyet gösterebilir.
Değişkenleri kullanırken tanımlama
Kitabın en baş kısmında C ve C++ temelde aynı olsalarda birçok bakımdan
da farklılık taşırlar demiştik. Bu farklılıklardan birisi de değişken
tanımlarının yapıldığı yerlerdir. Yani değişken tanımlama zamanları. Her
iki dilde de
değişkenler kullanılmadan önce mutlaka tanımlanmak zorundadır. Fakat C
dili tanımlamanın, çalışma alanının
hemen başında yapılmasını zorunlu tutar. Böylece derleyici bu değişkenler
için bir bütün halinde yer ayırır.
C kodlarını okurken ilk gördüğününüz; çalışma alanının başındaki
değişken tanımlamalarıdır. Bloğun en başında bütün değişkenlerin
bildirimi, dilin uygulanması ile ilgili ayrıntılarla ilgili olduğu için,
programcı özel bir yol takip ederek
bu işlemi gerçekler. İnsanların çoğu kodları yazmadan evvel kullanacakları
değişkenleri bilmezler, sonradan
bloğun başına dönüp yeni değişkeni yerleştirirler. Bu programcıyı zor
durumda bırakır ve hata kaynağıdır. Değişken tanımlamaları genellikle
okuyucuya çok fazla anlam ifade etmez, ama kullanıldıkları yerden uzakta
bulunan değişken tanımlamaları kafa karıştırırlar.
C++ (C değil) değişkenlerin çalışma alanının herhangi bir yerinde
tanımlanmalarına, kullanılmadan önce olmak koşuluyla izin verir. Buna ek
olarak, tanımlama esnasında değişkene ilk değer ataması yaparak
başlatabilirsiniz.. Böylece belli özellikteki hatalardan korunursunuz.
Değişkenlerin böyle tanımlanması kod yazımını kolaylaştırdığı gibi,
çalışma alanının içinde ileri geri gidip gelmelerden kaynaklanan hatalardan
da sakınılmış olur.
Tanımlandığı yerde kullanılan değişkenler sayesinde kodların anlaşılması
kolaylaşır. Aynı anda hem tanımlama
hemde ilk değerin atanması işlemi özellikle önemlidir. Başlangıç değerinin
anlamını değişkenin kullanımı yolu ile
görebilirsiniz.
Değişkenleri ayrıca for ve while döngülerinin denetim açıklamaları içinde,
koşullu if ve seçici switch deyimlerinin
içinde tanımlayabilirsiniz. Aşağıdaki örnek, kullanım anındaki tanımları
gösteriyor;
//:B03:OnTheFly.cpp
#include <iostream>
using namespace std ;
int main( ){
//..
{
//Yeni çalışma alan başlangıcı
int q=0 ;
//C burada bütün tanımlamaların bildirilmesini ister
//....
//Kullanımları tanımla
for(int i=0;i<100;i++){
q++ ;
//dışarıdaki çalışma alanından geliyor
//çalışma alanının sonunda tanımlama
p=12 ;
}
p=1 ;
//farklı p değeri
}//q ve p için çalışma alanı dışı burada başlıyor
cout<<"karakterleri yaz "<<endl ;
while(char c=cin.get( )!='q'){
cout<<c<<"degil mi ? "<<endl ;
if(char x=c=='a'||c=='b')
cout<<"a ya da b yazdiniz "<<endl ;
else
cout<<"yazdıginiz "<<x<<endl ;
}
cout<<"A,B,C yazın "<<endl ;
switch(int i=cin.get( )){
case 'A' :cout<<"Snap "<<endl ;break;
case 'B' :cout<<"Crackle "<<endl;break;
case 'C' :cout<<"Pop "<<endl ;break;
default :cout<<"A,B, ya da C değil<<endl;
}
}///:~
En içteki çalışma alanında, alan sona ermeden hemen önce p tanımlanıyor.
Gerçi herhangi bir kullanımı olmuyor ama,
bir değişkenin herhangi bir yerde tanımlanabileceğini gösteriyor. Dış
çalışma ortamında p ye atanan değerde aynı şekilde
kullanılmıyor.
for döngüsünün içinde tanımlanan i, gerektiği zaman değişken
tanımlanmasına ve değer atanmasına güzel bir örnektir.
Bu işlemi sadece C++ da yapabilirsiniz. i değişkeninin çalışma alanı for
döngüsündeki denetleme açıklamasının çalışma alanı kadardır. Bu
döngünün içinde i kendine atanan değerlerle dolanıp durur, ta ki sınır
değere kadar. Burada i bir sayıcı işlevi görmektedir aslında. Buradaki for
deyiminin kullanış biçimi, C++ da yaygın olarak kullanılır; i de bu
döngüde tercih edilen en yaygın değişkendir, zaten başka bir harf aramaya
da gerek yok.
Yukarıdaki örnekte while, if ve switch deyimlerinde de değişkenler
tanımlanmışlardır, fakat bu tür tanımlamalar for dakilere göre pek yaygın
değildir. Herhalde yazım zorluklarından olsa gerek. Örneğin herhangi bir
ayracınız olamaz
Yani söyleyemezsiniz ki
while((char c=cin.get( ) )!='q')
Burada çok masum gözüken yeni ayraç ilaveleri yararlı da sanılabilir.
Onları kullanamamanızdan dolayı beklediğiniz sonuçları alamazsınız.
Aslında sorun '!=' işlecinin '=' işlecine göre daha öncelikli olmasından
kaynaklanmaktadır.
Böylece char c, bool dan char'a çevrilmiş olarak sonlanır. Çıkışa
yazdırıldığında, gülümseyen ifadeler elde edilir.
Genel olarak if, while ve switch deyimlerinde ki, değişken tanımlamaları
tamamlayıcı olmak amacı ile yapıldığı düşünülmelidir. Fakat sadece for
döngüsünde bu tür değişken tanımlamaları yapılmalıdır. Zaten genelde de
hep böyle
tercih edilir.
Saklama yeri belirleme
Bir değişken (veya nesne) oluştururken; değişkenin ömrünü, o değişkenin
bellekteki yerleşimini, ve değişkenin derleyici tarafından nasıl
işlemleneceğini bizzat belirleyebilirsiniz. Saklama yeri belirteçleri auto,
extern, static, register ve mutable dır.
Global değişkenler
Bu tür değişkenler tüm işlev gövdelerinin dışında tanımlanmışlardır, ve
programın bütün parçalarında kullanılabilirler.
Global değişkenler çalışma alanlarından etkilenmezler, ve her zaman
programın her yerinde kullanılmaya hazırdırlar
(yani global değişkenlerin yaşamı, program bitene kadar sürer). Bir dosya
da bulunan bir global değişken extern anahtar kelimesi kullanılarak ikinci
bir dosyada bildirilirse, ikinci dosya için bu veri artık kullanılabilir. İşte
global değişkenleri gösteren bir örnek;
//:B03:Global.cpp
//{L} Global2
//Global değişkenlerin tanıtımı
#include <iostream>
using namespace std ;
int globe ;
void func( );
int main( ){
globe=12 ;
func( ) ; //globe değişir.
cout<<globe<<endl ;
}///:~
Şimdi de globe'a, extern olarak erişen dosya yazalım;
//:B03:Global2.cpp{O}
//Dış global değişkenlere erişim
extern int globe ;
//ilişimci referansları çözümler
void func( ){
globe=40 ;
}///:~
globe değişkeni için bellekteki yer Global.cpp deki tanımlamada belirlenir.
Aynı değişkene Global2.cpp deki koddanda
erişilebilir.Global.cpp ve Global2.cpp ayrı ayrı derlendiği için derleyicinin
mutlaka
extern int globe;
ile bilgilendirilmesi gerekir. Çünkü derleyici, globe değişkeninin yerini bu
bildirimle saptar.
Program çalıştırıldığı zaman func( ) işlevinin çağrımı globe değişkeninin
tek global durumunu etkiler.
Global.cpp de görülen;
//{L}Global2
ifadesi tarafımızdan açıklama amacıyla eklenmiştir. Anlamı sonuç
programı elde etmek için Global.cpp ile Global2.cpp nin ilişimleneceğini
açıklamaktadır. Global2.cpp de bir ifade görünmektedir; {O}, bu kısa ekin
anlamı Global2.cpp'nin
tek başına çalıştırılabilir program olarak derlenemeyeceğini, diğer
çalıştırılabilir program gerektiğini açıklıyor. Yani diğer programla
ilişimlenmesi lazım. Bu konu ayrık derleme yapılırken herşeyin doğru
işlemlenmesi için kullanılan makefile yazımında ayrıntılarıyla
anlatılacaktır.
Yörel değişkenler (auto)
Yörel değişkenler bir çalışma alanı içinde yeralırlar. Onlar belli bir işleve
"yörel"dirler. Çalışma alanına girildiğinde otomatik olarak geldiği için ve
çalışma alanı kapanırken otomatik olarak ortadan kaybolduğu için
çoğunlukla da otomatik değişken olarak adlandırılır. auto anahtar kelimesi
bunu bildirir. Fakat varsayım olarak auto olan yörel değişkenlerde auto
anahtar kelimesini yazmak gereksizdir.
Yazmaç (register) değişkenleri
Yazmaç değişkeni, bir yörel değişken tipidir. register anahtar kelimesi
derleyiciye mümkün olan en kısa sürede
bu değişkene eriş demektedir. Erişim hızının artırması uygulamaya bağlı
bir konudur, ama adından anlaşılacağı üzere
hızı artırmak, değişkeni register'e yerleştirerek gerçekleştirilir. Değişkenin
registere yerleşip yerleşmeyeceği
garanti değildir, dolayısı ile hızın artması da garanti değildir. Sadece
derleyiciye bir ipucu verilmektedir.
register değişkenlerinin kullanımıyla ilgili bazı kısıtlamalar vardır. Bu
değişkenin adresini alamaz veya hesaplayamazsınız. Böyle değişkenler
sadece bir bloğun içinde bildirilebilir (global ya da static register
değişkenleri
olamaz). Bir register değişkeni, bir işlevin değişkeni olarak
bildirebilirsiniz (yani değişken listesinin içine yerleştirebilirsiniz).
Genelde derleyici, herhalde işini sizden daha iyi yapacağı için, derleyicinin
iyileştiricisini ikinci seçenek olarak çalıştırmamanız daha iyi olur. Aslında
en iyisi register anahtar kelimesinden sakınmaktır.
static
static anahtar kelimesi yerine göre farklı anlamlar taşır. Normal de, bir
işleve göre yörel tanımlanmış değişken,
işlevin çalışma alanının sonunda ortadan kalkar. İşlev tekrar çağrıldığı
zaman bellekte yeni yer oluşturulur ve
değerler yeniden başlatılır. Bir programın tamamı boyunca aynı değerin
kullanılmasını istersek, işlevin yörel değişkeni static olarak tanımlanabilir
ve kendisine o durağan ilk değer atanabilir. İlk değer atama, işlevin ilk
çağrıldığı
anda yerine getirilir ve veri değerini işlev çağrıları arasında da kaybetmez.
Bu yolla işlev, bazı veri parçalarını
işlev çağrıları arasında "hatırlar". Böylece static ile yörel değişken
dışarıdan global görünür.
Şimdi neden böyle durumlarda global değişkenler kullanılmıyor diye
düşüneceksiniz. static değişkenin güzelliği
işlevin çalışma alanının dışında kullanılamamasıdır. Böylece dikkatsizce
değiştirilemez. Bu da hataları yörelleştirir.
Şimdi de static değişkenlerin kullanıldığı bir örneği aşağıda verelim;
//:B03:Static.cpp
//İşlev de static değişken kullanımı
#include <iostream>
using namespace std ;
void func( ){
static int i=0 ;
cout<<"i= "<<++i<<endl ;
}
int main( ){
for(int x=0; x<100; x++)
func( ) ;
}//:~
Döngü içinde func( ) işlevi, her çağrılışında farklı bir değer yazar. Eğer
static anahtar kelimesi yazılmamış olsaydı
çıkışta her zaman '1' çıkardı.
static'in ikinci anlamı "belli bir çalışma alanının dışında temin
edilememezlik" olup, birinci ile ilgilidir. static, bir işleve veya değişkene
uygulandığı zaman diğer bütün işlevlerin dışındadır. Bunun anlamı; "bu
isim bu dosyanın dışında kullanılamaz". İşlev veya değişken adı bu
dosyaya özel olur; yani, dosya çalışma alanındadır veya başka deyişle
dosya görüntüsündedir. Tanıtım için, aşağıdaki iki dosya derlenip
ilişimlendiğinde, ilişimci hatası ortaya çıkar.
//:B03:FileStatic.cpp
//Dosya çalışma alanının tanıtımı
//Bu dosyayı FileStatic2.cpp ile derleme ve ilişimleme ilişimci hatasına
yolaçar
//Dosya çalişma sadece bu dosya için geçerli
static int fs ;
int main( ){
fs=1 ;
}///:~
Aşağıdaki dosyada fs extern olarak belirtilmesine rağmen ilişimci
FileStatic.cpp dosyasında static olarak tanımlandığından fs i bulamaz ve
hata verir.
//:B03:FileStatic2.cpp {O}
// fs e referans edilme işlemi
extern int fs;
int main( ){
fs=100 ;
}///:~
static belirteci ayrıca sınıf içinde de kullanılır. Bu konu sınıf oluşturma
bilgileri anlatıldıktan sonra incelenecektir.
extern
Buraya kadar olan kısımlarda extern anahtar kelimesinin kullanımı ile
ilgili biraz bilgi edindiniz. Bu kelime derleyiciye, derlenen dosyada henüz
rastlanmasa bile bir değişken veya işlevin varlığını bildiriyor. Bu değişken
ya da işlev başka bir dosyada, veya bulunulan dosyada, program içinde
ilerde tanımlanabilir demektir. Aşağıda bunun bir örneği verilmiştir.
//:B03:Forward.cpp
//İleride işlev ya da veri bildirimleri
#include <iostream>
using namespace std ;
//Bu gerçekte dışarıda değil, derleyiciye söylenen herhangi
//bir yerde olabileceğidir
extern int i;
extern void func( ) ;
int main( ){
i=0 ;
func( ) ;
}
int i ; //veri tanımlaması
void func ( ){
i++ ;
cout<<i ;
}///~
Derleyici extern int i bildirimine rastladığı zaman, i için bir tanım
herhangi bir yerde, global değişken olarak bulunmaktadır. Derleyici i nin
tanımlamasına rastladığında ise artık başka bir bildirim bulunmaz, zira
derleyici bilirki, bu tanım daha önceden dosyada bildirilen i ye aittir. Eğer i
yi static olarak tanımlasaydınız, derleyiciye söylediğiniz;
extern aracılığı ile i yi global tanımla demektir. static olmasından ötürü,
dosya çalışma alanına sahip olacağından, globallikle dosya çalışma
alanının çatışmasından, derleyici hata üretecektir. Yani hem extern ham
static birarada kullanılamaz.
mutable
Sadece C++ da kullanılır, içeren nesne const olsa bile değiştirilebilecek
veri üyesini gösterir. Yani bir nesnenin bir üyesinin sabitliğini
baskınlaştırır. Ayrıca bu veri üyesi static olmamalıdır. mutable üyeler
çoğunlukla fizik olarak değişse bile mantık olarak değişmeyen nesnelere
sahip sınıf uygulamalarında kullanılır. mutable bir üye const üye işlev
tarafından değiştirilebilir. Bir örnek uygulama verelim;
class susint{
public:
koint( ) : deg_yakls(false){....}
double to_double( ) const {
if(!deg_yakls){
yakls=as_double( ) ;
deg_yakls=true ;
}
return yakls ;
}
.....
private:
double as_double( ) const ;
mutable double yakls ;
mutable bool deg_yakls ;
};
İlişim
C ve C++ programlarının davranışlarını kavrayabilmek için ilişim
konusunu bilmek gerekir. Çalışan bir programda
belirteç; derlenmiş işlev gövdesi veya değişkeni tutan bellek alanı ile ifade
edilir. İlişim, saklama
yerini ilişimcinin gördüğü şekilde tarif eder. İki türlü ilişim vardır; iç
ilişim, dış ilişim.
İç ilişimde belirteci gösteren saklama yeri, sadece derlenmekte olan dosya
için oluşturulur. Öbür dosyalar aynı belirteç ismini iç ilişimle veya global
değişken için kullanabilirler. İlişimci için herhangi bir çatışma olmaz.-Her
belirteç için ayrı saklama alanı oluştururlur. İç ilişim C ve C++ da static
anahtar kelimesi ile belirtilir.
Dış ilişim de; saklama yerinin bir parçası, derlenmekte olan bütün dosyalar
için, belirteci göstermek amacıyla oluşturulur. Saklama yeri bir kez
oluşturulur, ve ilişimci öteki bütün referans alanlarını, bu saklama yerine
göre çözmek zorundadır. Global değişken ve işlevlerin adları dış ilişime
sahiptirler. Bunlara diğer dosyalardan erişim, isimlerine extern anahtar
kelimesini yerleştirerek olur. Tüm işlevlerin dışında tanımlanan
değişkenler (C++ da const hariç) ve işlev tanımları dış ilişim unsurları
olarak varsayılır. Bunları özellikle static anahtar kelimesini kullanarak iç
ilişime zorlayabilirsiniz. Bir belirtecin açık biçimde dış ilişimde olduğunu
göstermek için, onunla extern anahtar kelimesini birlikte kullanmalısınız.
C de ise, bir değişken ya da işlevin adını extern kelimesi ile birlikte
tanımlamak gerekli değildir. C++ da ise bazen const için gerekir.
İşlev çağırılırken, otomatik (auto) değişkenler sadece geçici olarak yığıtta
bulunur. İlişimci otomatik değişkenler hakkında bilgi sahibi olmadığı için
bunlar da ilişim yoktur.
constant'lar
Standart öncesi C dönemlerin de sabit oluşturmak istediğinizde
önişlemleyiciyi kullanmıştınız.
#define PI 3.14159
Böylece PI olan her yere 3.14159 değeri önişlemleyici tarafından
yerleştirilir. (Aslında hala C ve C++ da aynı yolu
kullanabilirsiniz.)
Önişlemleyiciyi sabitleri oluşturmada kullandığınızda, sabitlerin denetimi,
derleyicinin çalışma alanının dışına çıkar.
O zaman PI adı üzerinde herhangi bir tip denetimi yapılamaz ve PI nin
adresini alamazsınız. (böylece PI'ye göstergeç veya referans
aktaramazsınız) PI kullanıcı tanımlı bir tip değişkeni olamıyor. PI'nin
anlamı tanımlandığı
noktadan dosyanın sonuna kadar sürer; bu durumlarda önişlemleyici
çalışma alanını dikkate almaz.
C++ ilk defa isimlendirilmiş sabit fikrini tanıttı ki, bu da aynı değişken
gibidir, tek farkı değerinin değiştirilemez oluşudur. Başındaki const ibaresi
derleyiciye o adın sabit olduğunu belirtir. Kullanıcı tanımlı veya yerleşik,
herhangi
bir veri tipi, const olarak tanıtılabilir. Eğer bir unsur const olarak
tanımlanır ve daha sonra bu değiştirilirse, derleyici hata iletisi üretir.
Sabit bir tipi şöyle tanıtmalısınız;
const int x=10 ;
Standart C ve C++ da ad verilmiş const, bir göstergeç veya referans olarak
değişken listesinde kullanılabilir.
(yani bunun anlamı bu const değişkenin adresini alabilirsiniz). Bir const
öge normal değişkenler gibi çalışma
alanına sahiptir. Ve const'ın adını programın geri kalanını hiç etkilemeden
bir işlevin içinde saklayabilirsiniz.
const ifadesi C++ dan alınarak standart C ye, gerçi tamamen farklı
anlamda da olsa, eklenmişti. C de derleyici const
ifadeyi peşine "beni değiştirme açıklaması eklenmiş" bir değişken olarak
işlemler. C de bir const tanımladığınız da,
derleyici bellekte onun için bir yer oluşturur. İki farklı dosyada aynı isimle
birden fazla const ifade oluşturursanız (ya da bir başlık dosyasına
koyarsanız), ilişimci çatışmalardan dolayı hata iletisi üretir. C deki const
ifadesinin planlanan kullanımı ile C++ daki const ifadesinin planan
kullanımı birbirinden farklıdır. (kısacası C++ daki daha iyidir.)
Sabit değerler
C++ da const ifadeler mutlaka bir değerle başlatılmak zorundadır. (Bu C
de doğru değil). Yerleşik tipler de sabitler onluk, sekizlik, onaltılık
tabanlara göre ya da kayan noktalı sayılar ya da karakterler olarak ifade
edilir. (maalesef ikili taban önemsenmemişti)
Herhangi bir bilgi verilmediğinde derleyici sayıyı onluk tabana göre yani
desimal olarak alır. O zaman 48.0 veya 1110101gibi sayılar derleyici
tarafından desimal olarak algılanır. Sıfır ile başlayan sabit sayı, oktal yani
sekiz tabanına göre algılanır. Sekiz tabanlı sayılarda rakamlar (0-7)
arasındadır. Derleyici bu tabanı kullandığında öteki rakamlar yazılırsa hata
üretir. Geçerli bir oktal sayı örneği; (017) dir, onluk tabana göre bu 15
demektir.
Bir sayı 0x ile başlarsa bu sayı heksadesimal yani onaltılık tabana göre
yazılmış demektir. Onaltılık tabanda bulunan rakamlar (0-9) ve (a-f) veya
(A-F) harfleridir. Doğru bir heksadesimal sayı; 0x1FE dir, bunun
desimaldeki karşılığı
510 dur.
Kayan noktalı sayılarda, ondalık noktası veya eksponensiyel üsler olabilir.
Burada nokta ve e bir seçenek olarak bulunur. Kayan nokta tipli bir
değişkene sabit bir değer atadığınız zaman, derleyici bu sabit değeri alır ve
onu kayan noktalı sayıya çevirir (bu içerdeki tip çevriminin bir çeşididir).
Fakat yinede kayan noktalı değişkenler için noktalı
ya da e'li sayılar kullanmak daha iyi bir fikirdir. Bazı eski derleyicilerin
böyle ipuçlarına ihtiyacı var.
Geçerli kayan noktalı sayı örnekleri; 1e4, 1.00001, 46.0,0.0 ve -1.159e-88.
Kayan noktalı sayılarda tipin etkisini
arttırmak için sayı sonuna ek yapabilirsiniz; f veya F float'ı, l veya L bir
long double'ı bildirir. (1.003f, vs)
Karakter sabitleri tek tırnak içine alınmış karakterlerdir. Yani 'A', 'O',' '
gibi. Burada 'O' (ASCII 96 ) ile 0 değeri arasında büyük fark vardır. Dikkat
edilmelidir. Özel karakterler ters bölü işareti ile birlikte gösterilirler: '\n'
yeni satır, '\t' tab, '\\' ters bölü, '\r' elde işareti, '\"' çift tırnak, '\'' tek tırnak,
v.s. Ayrıca char sabitlerini sekizli tabanda da gösterebilirsiniz; '\17', veya
onaltılı tabanda da '\xff' gibi.
Uçuculuk (Volatile)
Bilindiği gibi const ifadesi derleyiciye "bu hiçbir zaman değişmez"
demektedir. (derleyicinin ek iyileştirme yapmasına
izin verir). volatile niteleyicisi derleyiciye "bu ne zaman değişecek hiçbir
zaman bilemezsin" der, ve değişken
kararlılığına dayalı iyileştirmeyi derleyicinin gerçeklemesini engeller.
volatile'i kodunuzun denetlediği alanın dışından gelen değerler
okunduğunda kullanın, iletişim donanım kaydedicilerindeki bilgiler gibi.
volatile değişken her zaman değerin talep edildiği herhangi bir anda
okunur, hatta hemen bir satır önce okunmuş olsa bile.
"Kod denetiminin dışında" olan saklamanın bu özel hali, koşut denetim
akışlı (multithreaded) programlarda rastlanır.
Başka bir denetim akışı veya süreç tarafından değiştirilen özel bir bayrağı
izliyorsanız, o bayrağın volatile olması
gerekir, böylece derleyici bayrağı birden fazla okuyarak iyileştirme
yapılacağı önkabulünde bulunmaz.
Derleyici iyileştirme yapmadığı zaman volatile ifadesinin bir etkisi olmaz.
Ama iyileştirme yapmaya başladığınız zaman kritik hataları engeller.
(derleyici azalan sayıda okumayı aramaya başladığı zaman.)
const ve volatile anahtar kelimeleri ilerleyen bölümde daha da
aydınlatılacaktır.
İşleçler ve kullanımları
Bu bölüm C ve C++ daki bütün işleçleri kapsamaktadır.
İşleçler işlenenlerinden değer üretirler. Bu değer işlenenleri değiştirmez.
Atama, artma ve azalma
işleçlerinin kullanıldığı durumlar hariç. İşlenenin değişimine "yan etki"
denir. İşleçlerin en yaygın kullanımı yan etki üreten yani işlenenlerinin
değiştiği işlemlerdir. Yalnız yan etkisiz işleçlerde üretilen değerlerin de
kullanıldığını unutmayın.
Atama (=)
Atama işlemi (=) işleci ile yapılır. Bu işlecin anlamı; sağ tarafta ki değeri
(sağ değer) al sol(sol değer) tarafa kopyala demektir. Sağ değer bir sabit,
bir değişken veya değer üreten bir açıklama olabilir. Ama sol değer farklı,
adı konmuş bir değişken olmalıdır (yani veriyi saklamak için fiziksel
bellek alanı olmalıdır). Örnek olarak bir değişkene bir sabit değer
atanabilir (A=5 ;) gibi. Ama sabit değere değişken atanamaz, yani (5=A;)
olamaz. Burada hemen bir şeyi önemle vurgulayalım ilk değer atama ile
(yani başlatma), atama aynı işlemler değildirler. Başlatma programcılıkta
çok özel ve önemli anlamı bulunan bir işlemdir. Başlatma yapılmamış bir
nesne veya öge programın çalışmasını engeller, ama ataması unutulmuş
öge eski değerle işlem yapıp yanlış sonuç üretir. Gerçi C++ da ilk değer
ataması yapılmamış fakat bildirimde bulunulmuş değişkenler için sıfır
değeri başlatma değeri olarak alınıyorsa da siz her zaman eşeğinizi
sağlama bağlayıp kendi işinizi kendiniz görün.
Aritmetik işleçler
Temel aritmetik işleçler programlama dillerinin çoğunda aynıdır; toplama
(+), çıkarma(-), çarpma(*) ve bölme(/) ve modulus (%) (%;bu işleç int
sayıların bölümünden kalanı üretir). int bölme, noktadan sonrakileri
tıraşlar.
Yani yakınlaştırma yapmaz. Modulus işleci kayan noktalı sayılarla
kullanılmaz.
C ve C++da hem işlem hem atamaları bulunduran kısaltılmış gösterimli
uygulamalar vardır. Bunun gösterimi; işlem işlecini takip eden eşitlik işleci
ile sağlanır, ve bu dilde kullanılırken, bütün işleçlerle tutarlıdır. Örnek
olarak x değişkenine 5 ekleyerek aynı değişkene atama yapmak için,
(x+=5) şeklinde yazılır.
Aşağıdaki örnek aritmetik işleçlerin kullanımını göstermektedir;
//:B03:Mathops.cpp
//Matematik işleçler
#include <iostream>
using namespace std ;
//string ve değeri gösteren macro
#define PRINT(STR,VAR) \
cout<<"STR=
"<<VAR<<endl ;
int main( ){
int i,j,k ;
float u,v,w ; //double uygulanır
cout<<"int girin ; "<<endl ;
cin>>j ;
cout<<"baska bir int girin; "<<endl ;
cin>>k ;
PRINT("j",j); PRINT("k",k);
i=j+k ;
PRINT("j+k",i);
i=j-k;
PRINT("j-k",i);
i=k/j;
PRINT("k/j",i);
i=j*k;
PRINT("j*k",i);
i=k%j;
PRINT("k%j",i);
//aşağıdaki işlem sadece integerlere uygulanır
j%=k;
PRINT("j%=k",i);
cout<<"kayan noktali sayi girin; "<<endl;
cin>>v ;
cout<<"baska bir kayan noktali sayi girin; "<<endl ;
cin>>w ;
PRINT("v",v) ;PRINT("w",w) ;
u=v+w ;
PRINT("v+w",u) ;
u=v-w ;
PRINT("v-w",u) ;
u=v*w ;
PRINT("v*w",u) ;
u=v/w ;
PRINT("v*w",u) ;
//Aşağıdaki işlemler sadece int, char ve double'lar için geçerlidir
PRINT("u",u) ;PRINT("v",v) ;
u+=v ;
PRINT("u+=v",u);
u-=v ;
PRINT("u-=v",u) ;
u*=v ;
PRINT("u*=v",u) ;
u/=v ;
PRINT("u/=v",u) ;
}///~
Atamaların sağ taraf değerleri çok daha karmaşık olabilir.
Önişlemleyici makro'larına giriş
Yukarıdaki PRINT( ) makro'sunun kullanımı, sizi fazla kod yazmaktan
korur (yazım hatalarından da korur).
Geleneksel olarak önişlemleyici makro'ları göze çarpsın diye büyük
harflerle yazılır. Zira daha sonra göreceğiniz
üzere ne kadar tehlikeli olabilecekleri anlaşılacaktır. (Ayrıca ne kadar
yararlı olabilecekleri de)
macro adının ardındaki ayraçlanmış listedeki değişkenler, takip eden
kapalı ayraçların arasındaki kodlara yerleştirilir.
macro'nun çağrıldığı herhangi bir yerde önişlemleyici PRINT adını
kaldırır ve kodu yerine yerleştirir. Böylece derleyici macro adını
kullanarak hata üretemez ve değişkenler üzerinde tip denetlemesinde
bulunmaz. (Bu bölümün sonunda
bakım macroları yazarken kazançlı olur)
İlişki işleçleri
İlişki işleçleri, işlenenlerin değerleri arasında bağlantı kurarlar. İlişki doğru
ise bool tipindeki true'yu, ilişki yanlışsa false'ı üretirler. İlişki işleçleri
arasında; büyüktür (>), küçüktür(<), eşittir(=), eşittir veya büyüktür(>=),
eşittir
veya küçüktür(<=), özdeştir(==), özdeş değildir(!=) bulunur. Bunlar, C ve
C++ daki yerleşik veri tiplerinin hepsinde kullanılır. C++ da ise kullanıcı
tanımlı veri tiplerinde bu işleçlere özel anlamlar verilebilir (bu yeni
anlamlandırmayı
kullanıcı mutlaka tanımlamak zorundadır). Bu konu ilerleyen bölümlerde
işleç bindirimlerinde anlatılacaktır.
Mantık işleçleri
Mantık işleçleri ve (&&) ile veya(||) olup değişkenler arasındaki mantık
ilişkilerine göre true veya false üretirler. Biliyorsunuz ki C ve C++ da bir
deyimin değeri sıfırdan farklı ise true'dur, sıfıra eşit ise false'dır.
bool yazdırırsanız, true için '1', false için '0' görürsünüz.
Aşağıdaki örnekte hem ilişki hem mantık işleçleri göreceksiniz;
//:B03:Boolean.cpp
//İlişkili ve mantık işleçleri
#include <iostream>
using namespace std ;
int main( ){
int i,j ;
cout<<"integer sayi gir: " ;
cin>>i ;
cout<<"
başka bir integer sayi gir " ;
cin>>j ;
cout<<"i>j tur"<<(i>j)<<endl ;
cout<<"i<j tur "<<(i<j)<<endl ;
cout<<"i>=j tir"<<(i>=j)<<endl ;
cout<<"i<=j tir"<<(i<=j)<<endl ;
cout<<"i!=j dir"<<(i!=j)<<endl ;
cout<<"i==j tir"<<(i==j)<<endl ;
cout<<"i&&j "<<(i&&j)<<endl ;
cout<<"i||j"<<(i||j)<<endl ;
cout<<"(i<10)&&(j<10) dir"<<(i<10)&&(j<10)<<endl ;
}///~
Yukarıdaki örnekte int değişken tipini float veya double ile
değiştirebilirsiniz. Dikkat etmeniz gereken bir nokta da kayan noktalı sayı
ile sıfır nokta sayı ile (0.222 gibi) karşılaştırmadır; bir sayının öbüründen
az oranda farklılığı eşitliği ortadan kaldırır. Bir kayan noktalı sayının
sıfırdan çok az farklılığı da onun mantık değerinin true olmasını sağlar.
Bit işleçleri
Bit işleçleri sayıların her bir biti üzerinde, ayrı ayrı işlemler yapmaya izin
verirler. Bit işleçleri sadece int, char ve long tipleri üzerinde çalışırlar ve
değişkenin ilgili biti üzerinde Bool cebir işlemlerini uygulayarak sonucu
üretirler.
Ve (and) bit (&) işleci her iki giriş değeri bir olduğunda bir sonucunu
üretir, başka herhangi bir durum için sıfır değeri
üretir. Veya (or) bit (|) işleci girişlerin herhangi biri veya her ikisi de bir
olduğunda, bir sonucunu üretir, her ikisi de sıfır olduğunda sıfır sonucunu
üretir. Dışarlıklı ya da (exclusive or) bit (^) işleci girişlerden herhangi biri
bir (1) olduğunda (her ikisi aynı anda bir değil) bir sonucunu üretir, öteki
başka durumlarda sıfır sonucu üretilir. Değil bit(~) işleci (bir tamamlayıcı
işleci olarak ta adlandırılır) tek değişkenli bir işleçtir. Öbür bit işleçleri iki
değişken kullanırlar. Değil bit(~) işleci girişin tersini üretir, yani giriş bir
olduğunda sıfır, giriş sıfır olduğunda bir üretir. Bit işleçleri atama işleci ile
birleştirilebilirler;&=, |= ve ^= gibi, ve bunların hepsi geçerli işlemler
yaparlar. (değil(~) işleci, = ile birleşemez).
Kaydırıcı işleçler
Kaydırma işleçleri de bitler üzerinde işlem yaparlar. Sola kaydırma işleci
(<<) işlemlenenin bitlerini işleçten sonra yazılan sayıda sola kaydırır, ve
işlecin soluna üretir. Sağa kaydırma işleci(>>) işlemlenenin bitlerini
işleçten sonra yazılan sayıda sağa kaydırır, ve işlecin soluna üretir. Kayma
işlecinden sonra ki değer sol taraftaki işlemlenenin bit sayısından daha
büyükse sonuç tanımsızsa. Sol tarafta işlemlenenin işareti yoksa, sağ
kayma mantık kayması olur ve büyük basamaklara sıfır yerleştirilir. Sol
taraftaki işlemlenen işarete sahipse, sağa kayma mantık kayması olabilir
de olmayabilirde (yani,davranışı tanımsızdır).
Kayma işleçleri eşit işleçi ile beraber (>>=,<<=) birarada kullanılabilir. Sol
değer sağ değer tarafından kaydırılmış
sol değer ile yerdeğiştirilir.
Aşağıdaki örnek bitlerin kullanımını ihtiva eden bütün işleçleri
kullanmaktadır;
//:B03:printBinary.h
//ikili sayıları bayt içinde gösterin
void printBinary(const unsigned char val) ;
///:~
İşlevin uygulaması;
//:B03:printBinary.cpp {O}
#include <iostream>
void printBinary(const unsigned char val){
for(int i=7;i>=0;i--)
if(val &(1<<i))
std::cout<<"1" ;
else
std::cout<<"0" ;
}//:~
printBinary( ) işlevi bir bayt bilgiyi alır ve bit-bit gösterir.
Açıklama;
(1<<i)
yaptığı; ardışık bitleri bir bit sola kaydırır, yani ikili tabanda 00000001
sayısı kayma sonunda 00000010 olur. Bu bit
val ile bit ve (&) işlemine uğradığında sonuç sıfırdan farklıdır, bunu
anlamı; val yerinde bir vardır.
Sonuç olarak örnekte kullanılan işlev, bit işlem işleçlerini göstermektedir;
//:B03:Bitwise.cpp
//{L} printBinary
//Bit işleminin gösterimi
#include "printBinary"
#include <iostream>
using namespace std ;
//yazımı saklayan macro
#define PR(STR,EXPR) \
cout<<STR ; printBinary(EXPR) ; cout<<endl ;
int main( ){
unsigned int getval ;
unsigned char a,b ;
cout<<"0-255 arası a'yı girin : " ;
cin>>getval ; a=getval ;
PR("a ikili tabanda : ",a) ;
cout<<"0-255 arasındaki b'yi girin: " ;
cin>>getval ; b=getval ;
PR("b ikili taban da: ",b) ;
PR("a|b= ",a|b) ;
PR("a&B= ",a&b) ;
PR("a^b= ",a^b) ;
PR("~a= ",~a)
;
PR("~b= ",~b) ;
//İlginç bit düzeni
unsigned char 0x5A ;
PR("c ikili tabanda: ",c) ;
a|=c ;
PR("a|=c ; a= ",a) ;
b&=c ;
PR("b&=c;b= ",b) ;
b^=a ;
PR("b^=a ; b= ",b) ;
}///~
Bu program örneğinde de yine macro tanımı yapılmıştır. Seçtiğiniz
kelimeyi yazar, daha sonra açıklamanın ikili tabanda gösterimini sağlar, ve
son olarakta yeni bir satıra gönderilir.
Ana işlev olan main( ) de değişkenler işaretsiz (unsigned) dir, bunun
sebebi ikili taban da baytlarla çalışma yaparken işaretlerin gereksizliğidir.
Ayrıca cin>> deyiminde, getval için char yerine int kullanılmak
mecburiyeti var, zira cin>> deyimi aksi durumda ilk biti karakter olarak
işlemler. Bu sayede a ve b'ye atanan getval değeri çevrilerek bir bayt
yapılmış olur.(burada fazlalık tıraşlanır)
<< ve >> işleçleri bit kaydırması yaparlar. Bitler, bit sayısı kadar
kaydırıldığı zaman görülürki bütün bitler ortadan kaybolmuş. Bitleri
kaydırma işlemi sırasında dairesel döndürme işlemi yapmak mümkündür.
Bu sayede kayan bitler tekrar kullanılır. Döndürme işleminde kayan bitler
bir taraftan çıkarken, öbür taraftan sayıya eklenmektedir. Sanki sayı içinde
bir döngü oluşturuyolarmış gibi görünür. Hatta işlemcilerin çoğunda
makine düzeyinde döndürme komutu olmasına rağmen (bu konu için ilgili
işlemcinin assembly komutlarını inceleyebilirsiniz), C ve C++ da
döndürme için doğrudan destek yoktur. C dili tasarımcıları önkabul olarak
dil deyimlerini en aza indirmek için döndürme komutuna
gerek duymamışlar, onu yazmayı size bırakmışlardır. Aşağıda sola ve sağa
dairesel döndürme örneği verilmiştir ;
//:B03:Rotation.cpp {O}
//Sağ ve sola döndürmeyi gerçekler
unsigned char rol(unsigned char val){
int highbit ;
if(val &0x80)
//0x80 sadece en büyük bittir.
highbit=1 ;
else
highbit=0 ;
//sola kayma en küçük bit sıfır olur
val<<=1 ;
//enbüyük biti en arkaya yolla
val|=highbit ;
return val ;
}
unsigned char ror(unsigned char val){
int lowbit ;
if(val &1) //en küçük biti incele
lowbit=1 ;
else
lowbit=0 ;
val>>=1 ; //sağa bir bit kayma
//en küçük biti en başa yolla
val|=(lowbit<<7) ;
return val ;
}///:~
Bu işlevleri Bitwise.cpp ile kullanmayı deneyiniz. İşlevler kullanılmadan
önce Bitwise.cpp de ror( ) ve rol( ) un tanımları mutlaka derleyiciye
bildirilmelidir.
Bit düzenindeki işlevler doğrudan assembly dili deyimlerine çevrildikleri
için çok yararlıdırlar. Bazen tek bir C veya
C++ deyimi bir satırlık assembly koduna çevrilir.
Tek değişkenli işleçler
Bit düzenli (değil=not), tek değişken alan biricik işleç değildir. Ona yakın
bir işleçte mantık değili yani (!=) dir kendisi true değerini aldığında false
üretir. Tek değişkenli eksi(-) ve tek değişkenli artı (+) işleçleri ikili
düzendeki eksi ve artı gibidirler. Derleyicinin kullanımı şekillendirmesi
sizin yazım biçiminize bağlıdır. Örnek olarak;
x=-a ;
anlamı açıktır. Derleyici aşağıdakini de şekillendirir.
x=a*-b ;
fakat okuyanı şaşırtmamak için ayraç içine almak en iyisidir.
x=a*(-b) ;
gibi.
Tek değişkenli eksi işleçi, değerin negatifini üretir. Tek değişkenli artı tek
değişkenli eksinin simetrisi olup aslında değer üzerinde değişiklik yapmaz.
Azalma(--) ve çoğalma (++) işleçleri daha önceden anlatılmıştı. Bu işleçler
değişkeni bir birim azaltır veya çoğaltırlar.
Buradaki birim özellikle göstergeçlerle kullanıldığında değişik değerlere
karşılık gelebilir. Aslında bu işleçlerin birim değerini, kullanıldıkları veri
tipi belirler.
Ve tek değişkenli işleçlerin son örnekleri referans (& )-değişkenin adresini
verir-, referanstan kurtaran- göstergeç- yani adresi belli değeri veren (*) ile
(->) işleçleridir. Ayrıca C ve C++ daki döküm işleçleri ve yine C++ daki
new ile delete işleçleri tek değişkenli işleçlerdir. Döküm konusu bu
bölümde anlatıldı, new ile delete ileride anlatılacak.
Çok değişkenli işleç
Çok dallanmalı if-else üç işlemlenene sahiptir, fakat sık rastlanmaz.
Bilinen if-else ile pek benzemeyen çok değişkenli
if-else gerçek bir işleçtir. Zira bilinen if-else benzemeyen bir şekilde bir
değer üretir.
a=--b ? b: (b=-99) ;
Çok değişkenli if-else işlecine örnek yukarıda verilmiştir. Burada soru
işaretinden önceki açıklama true ise soru işaretinden sonraki değşkenin
değeri hesaplanır ve sonuç olarak işleç tarafından üretilir. Soru işaretinden
önceki
açıklama false ise o zaman iki nokta (:) dan sonraki değer işlemlenir ve
işlecin değeri olarak üretilir. Dikkat edilirse
burada koşullu işleçler kullanılabilmektedir.
Yukarıdaki azalan b değeri sıfır değilse a'ya b değeri atanır, yok eğer
azalan b değeri sıfıra eşitse, o zaman a=b=-99
olur. Yani bu durum sadece azalan b'nin sıfıra eşit olması ile olur.
Benzer deyim "a=" açıklamasını kullanmadanda yazılabilir;
--b ? b: (b=-99) ;
Buradaki ikinci B gereksiz olmaktadır,zira işlecin ürettiği değer
kullanılmamaktadır.? ile : arasına bir açıklama gerekir,burada basit bir
sabittir ve kodu bir miktar hızlandırır.
Virgül işleci
Virgül sadece fazla sayıdaki tanımlamaları ayırmak amacı ile kullanılmaz.
int i, j, k ;
//gibi
Ayrıca işlev değişken listesinde de kullanılır. Ve ek olarak açıklamaları
birbirinden ayırmak için de kullanılır. Bu durumda da sadece son
açıklamanın değeri üretilir. Virgülle ayrılmış açıklamaların geri
kalanlarında, sadece yan
etkilere göre hesaplamalar yapılır. Aşağıdaki örnek virgülle ayrılmış
değişken listesini arttırır ve sağ değer olarak sonuncusunu kullanır.
//:B03:VirgulIsleci.cpp
#include <iostream>
using namespace std ;
int main( ){
int a=0,b=1,c=2,d=3,e=4 ;
a=(b++,c++,d++,e++) ;
cout<<"a= "<<a<<endl ;
//burada ayraçlar kritik önemdedir, eğer ayraçlar olmazsa deyim
aşağıdaki gibi işlem yapar
(a=b++),c++,d++,e++ ;
cout<<"a= "<<a<<endl ;
}//:~
En iyisi yinede virgülü, ayırıcı olma dışında kullanmaktan sakınılmalı,
çünkü insanlar işleç olarak kullanırken şaşırıyorlar.
İşleçleri kullanırken düşülen yaygın hatalar
Yukarıdaki örnekler de görüldüğü üzere, işleçleri kullanırken düşülen
hatalardan biri; açıklamanın en küçük bitini hesaplarken, belirsizlik ortaya
çıkınca, ayraç olmadan sonucu elde ederken ortaya çıkar. (bunu irdelemek
için elinizdeki C dili kılavuzuna bakıp açıklama düzeni hesaplamalarını
inceleyin).
Başka bir yaygın hata da aşağıda görülüyor;
//:B03:Tuzak.cpp
//İşleç hataları
int main( ){
int a=1,b=1 ;
while(a=b){
//...
}
}///:~
Yukarıda b değeri sıfırdan farklı olduğu sürece while yanındaki ayraçlar
arasında true üretilir. Burada a'ya, b'nin değeri atanmıştır. Ayrıca b'nin
değeride eşit(=) işleci ile üretilir. Koşullu deyimde özdeşlik(==) işleci,
atama(=) işlecinin
yerine kullanılmalıdır.Bu konu birçok programcıyı ters köşeye yatırır.
Dikkat edilmeli ve (==) işleci kullanılmalıdır .
Benzer bir karışıklık mantık ve(and,&&), ya da(or,||) ile bit düzeni ve
(and, &), ya da(or,|) aralarında ortaya çıkar.
Aynı (=) ile (==) arasındaki karışıklık gibi. Zira iki karakter yerine tek
karakter basmak her zaman daha kolaydır. Hatırlamayı kolaylaştırmak için
"bitler daha küçük birimlerdir o nedenle daha az sayıda işlece gereksinim
duyarlar."diye düşünün.
Döküm işleçleri
Döküm, bilindiği gibi sanayide bir maden türünün ergitilip kalıba
dökülmesi işlemidir. Siz bu işlemde madeni yeni bir
biçime sokuyorsunuz. Programlamada da döküm kelimesi, sanayideki
kullanımını andırır. C ve C++ da da ; döküm (cast) kelimesinden
kastedilen "kalıba dökülen" demektir. Anlamlı kılmak gerekirse, derleyici
bir veri tipinden öteki veri tipine otomatik çevirme yapar. Örnek olarak; bir
integer değişken float (kayan noktalı) değişkene çevrilmek istenirse,
derleyici bu çevirme işi için ilgili işlevi çağırır ve int'i float'a çevirir.
Döküm, tip çevrimi işini aleni hale getirir. Veya normalde olmayacak şeyi
olması için zorlar.
//:B03:BasitDokum.cpp
int main( ){
int b=100 ;
unsigned long a=(unsigned long int )b ;
}//:~
Döküm çok güçlü bir işlemdir. Fakat bazı durumlarda başağrısı yapabilir,
zira örnekte olduğu gibi döküm
derleyiciyi, değişkenin kapladığı alandan fazla yer ayırmaya zorlar bu da
öteki değişkenlerin alanını çiğnemesine yolaçar. Böyle durumlar genellikle
döküm göstergeçlerinde ortaya çıkar. Yukarıdaki basit dökümlerde pek
rastlanmaz.
C++ da işlev çağırmayı takiben ilave döküm sözdizimine rastlanır. Bu
sözdizimi işlev çağrısı gibi, veri tiplerinin etrafı yerine, değişkenlerin
etrafına ayraç koyar.
//IslevCagirmaDokumu.cpp
int main( ){
float a=float (100) ;
//aşağıdaki ile aynı
float b=(float)100 ;
}///:~
Aslında yukarıda döküme hiç gerek yok. 100f yazabilirsiniz.
(derleyicinizin bu açıklamayı anladığını düşünüyoruz.)
Dökümler genelllikle sabitlerden ziyade değişkenler ile kullanılır.
C++ da açık dökümler
Döküm kullanılırken dikkatli olmalıdır çünkü siz derleyiciye "tip
denetimini unut, bu tipi başka bir tip olarak işlemle" diyorsunuz. Yani
aslında siz C++nın tip sisteminde bir delik oluşturuyorsunuz ve
derleyicinin, tiple yaptığınız yanlış işleri size bildirmesini engelliyorsunuz.
En kötüsü derleyici size içten güveniyor ve hataları yakalamak için
herhangi bir denetimde bulunmuyor. Bir kez döküm yapmaya
başladığınızda sorunların her çeşidinin yolunu açıyorsunuz. O nedenle çok
sayıda döküm kullanan programlara şüphe ile yaklaşmak lazımdır. Size
zorunluluk olduğu söylense bile dikkatli olun. Dökümler, çok özel
sorunların çözümüne özgü, yalıtılmış çözümlerde ve çok az
kullanılmalıdır.
Bunu anladığınız için, hatalı programlara rastladığınız da ilk eğiliminiz
genellikle dökümlerin suçlu olduğudur.
Fakat C biçimli dökümleri nasıl yerleştireceksiniz. Bilindiği gibi onlar
basitçe ayraçlar arasındaki tip adlarıdır, bunları aramaya başladığınızda,
onları kodun geri kalanından ayırmanın zorluğunu farkedeceksiniz.
Standart C++da, açık döküm yeni sözdizimi, eski C biçimli sözdiziminin
yerini almıştır (eski sözdizimi de kullanılabilir ama derleyici yazarları için
yeni olan tercih edilmelidir) Açık döküm sözdizimi öyle düzenlenmiştir ki,
siz onları kolaylıkla bulabilir, adlarını görebilirsiniz;
static_cast ----------------iyi-davranışlı ve uygun iyi-davranışlı döküm
için,döküm gerekmese bile, yapabileceğiniz
durumlarda kullanılır (otomatik tip çevrilmesi
gibi).
const_cast ----------------const ve/ ya da volatile döküm.
reinterpret_cast----------Tamamen farklı anlam için döküm. Burada
anahtar, güvenli şekilde kullanmak için orijinal
geri dökmeye ihtiyaç duymaktır. Dökülecek tip
ya sadece bit çevirmek ya da başka gizli
sebeplerden kullanılır. Bütün tip dökümlerinin
en tehlikelisidir.
dynamic_cast ------------güvenli aşağı tip döküm (Bu tip döküm, ilerleyen
bölümlerde ayrıntıları ile anlatılacak).
--------------------------------------------------------------------------------------------------------------------------------------İlk üç döküm bu bölümde anlatılacaktır. Son açık döküm ise, daha sonraki
bölümlerden birinde incelenecektir.
static_cast
İyi tanımlanmış çevirilerin tamamında kullanılır. Bunlar, derleyicinin izin
verdiği, döküm gerekmeyen veya daha az güvenli çevrimleride içeren, her
şeye rağmen iyi tanımlanmış çevrimlerdir. Çevrim tipleri static_cast ile
belirtilir, fakat
dilenirse döküm işlemi kullanılmayabilir, böylece çevrimler daraltılır (bilgi
kaybı), void* den zorlama çevrim, iç tip çevrimleri, ve sınıf
hiyerarşilerinde static gezinti yapılır (Bu konular kalıtım ve sınıf
işlendikten sonra anlatılacaktır)
//B03:static_cast.cpp
void func(int){ }
int main( ){
int i=0x7fff ;
float f ;
long l ;
//döküm olmaksızın çevrimler (1)
l=i ;
f=i ;
//Aşağıdakilerde çalışır
l=static_cast<long>(i) ;
f=static_cast<float>(i) ;
//çevrim daraltmaları (2)
i=l ; //digitler kaybolur
i=f ; //bilgi kaybolur
//bildiğini söyler,uyarıları kaldırır
i=static_cast<int>l ;
i=static_cast<int>f ;
char c=static_cast<char>(i) ;
//void* ten çevrime zorlama (3)
void* vp=&i ;
//eski yöntem tehlikeli çevrim yapar
float* fp=(float*)vp ;
//yeni yolda tehlikeli
fp=static_cast<float*>(vp) ;
//içteki tip çevrimleri derleyici tarafından gerçeklenir (4)
double d=0.0 ;
int x=d ;
//otomatik tip çevrim
x=static_cast<int>(d) ; //daha açık belli
func(d) ;
//otomatik tip çevrimi
func(static_cast<int>(d)) ; //daha açık belli
}///:~
(1) .kısımda C dilinde yaptığınız çevrimleri gördünüz. Bunlar dökümlü ve
döküm gerekmeden yapılan çevrimlerdir.
Sonraki tip int tipinin her zaman her değerini saklayabileceğinden, int den
long ya da float a geçiş sorun yaratmaz. Gereksiz olmasına rağmen
static_cast deyimi bu geçişleri belirli kılar.
Ters çevirmenin yolu (2) de gösterilmiştir. Burada veri kaybı olur, zira int
tipinin bellek uzunluğu long ya da float
kadar olmadığı için sayıları aynı büyüklükte tutamaz. Bu nedenle böyle
çevrimler daraltıcı çevrim olarak adlandırılır.
Derleyici bu işlemleri yerine getirirken uyarılarda bulunur. Uyarıları
kaldırmak için static_cast dökümünü kullanabilirsiniz.
C++ da void* ten döküm yapmaksızın atamaya izin verilmez (C ye
benzemez). Bu tehlikeli bir iş olup programcının ne yaptığını iyi bilmesi
gerekir. Hata yakalamaya kalktığınızda en azından static_cast in yeri, eski
standart döküme göre, daha kolay belirlenir.
Derleyici tarafından otomatik olarak yerine getirilen (4). kısımdaki
çevrimler, iç tip çevrimlerdir. Bunlar döküm gerekmeyen otomatik
çevrimlerdir, fakat static_cast i ne olduğunu belirlemek ve ilerde hata
ayıklamada kolaylık sağlaması için kullanabilirsiniz.
const_cast
const tan const olmayana veya volatile den volatile olmayana çevirmek
için kullanılır. Bu, const_cast ile çevrimine izin verilen tek çevrimdir.
Başka bir çevrim yapılırsa mutlaka ayrı bir açıklama kullanılmalıdır veya
derleme hatası
ortaya çıkar.
//:B03:const_cast.cpp
int main( ){
const int i=0 ;
int* j=(int*) &i ; //beğenilmeyen tarz
j=const_cast<int*>(&i) ; //tercih edilen tarz
//aynı anda ek döküm yapılamaz
//! long* l=const_cast<long*>(&i) ; //hata
volatile int k=0 ;
int* u=const_cast<int*>(&k) ;
}///:~
const nesnenin adresini aldığınızda, const a bir göstergeç üretirsiniz, bu
da const olmayan bir göstergeçe döküm yapmaksızın atanamaz. Eski tip
döküm bunu başarabilir, ama const_cast kullanmak daha uygundur.
volatile için de aynı yaklaşım geçerlidir.
reinterpret_cast
En az güvenli döküm yöntemidir. Bu nedenle en fazla hata üretir. Bu
döküm biçiminde, sanki nesne tamamen farklı
bir tipmiş gibi işlemlenecek bit düzeni uydurulur (genellikle karanlık bir
amaç için !). Bunlar C de çok ünlü alt düzeyli bit çevirmeleridir. Her
zaman bu durumlarda, başka bir işlem yapmadan önce sanal olarak ana tipe
reinterpret_cast gereksinimi (veya ana tiple işlemleme) olabilir.
//:B03:reinterpret_cast.cpp
#include <iostream>
using namespace std ;
const int sz=100 ;
struct X {int a[sz]; };
void print(X* x){
for(int i=0;i<sz;i++)
cout<<x->a[i]<<' ' ;
cout<<endl<<"--------------------"<<endl ;
}
int main( ){
Xx ;
print(&x) ;
int* xp=reinterpret_cast<int*>(&x) ;
for(int* i=xp;i<xp+sz;i++)
*i=0 ;
//bu noktadan sonra geri döküm yapılmazsa, xp X* olarak kullanılmaz
print(reinterpret_cast<X*>(xp)) ;
//bu örnekte hemen ayrıca esas tanımlayıcıyı kullanabilirsiniz.
print(&x) ;
}///:~
Yukarıdaki basit örnekte struct X, int diziye sahiptir. X x teki yığıtta birini
oluşturduğunuzda int lerin herbirinin
değeri kullanılmaz olur (struct ın içindekiler, print( ) işlevi kullanılarak
gösterilir). Bunlara ilk değerlerini atamak
için X in adresi alınır ve bir int göstergecine dökülür böylece dizi içinde
int leri sıfır yapmak için rahatlıkla dolaşılır.
Burada i nin üst değeri xp ye sz ekleyerek elde edilir ve derleyici sizin sz
göstergec yerlerinin xp den büyük olmasını istediğinizi bilir ve doğru
göstergeç hesaplamasını yapar.
reinterpret_cast kullanımındaki temel fikir; sahip olduğunuz o kadar
farklıdırki geriye döküm olmazsa esas amaç için kullanılamaz. Örnekte
yazma çağrısı için X* e geri döküm görülüyor. Ama hala tabii ki esas
tanımlayıcıyı da kullanabilirsiniz. int* olarak kullanışlı tek değişken xp
dir, gerçekte yapılan orijinal X in yeniden yorumlanmasıdır.
reinterpret_cast çoğunlukla tavsiye edilmez ve/veya aktarılamaz
programlardır. Yine de kullanmak zorunluluğu ortaya çıkarsa
kullanabilirsiniz.
sizeof işleci
sizeof işleci pek bilinmeyen bir gereksinimi yerine getiren tek başına bir
işleçtir. sizeof, verilerin bellekte kapladıkları
alan ile ilgili bilgi verir. Daha önce de açıklandığı gibi sizeof işleci belli bir
değişkenin kullandığı byte sayısını verir. Ayrıca veri tipinin büyüklüğü
hakkında da bilgi verir (değişken ismi gerekmez);
//:B03:sizeof.cpp
#include <iostream>
using namespace std ;
int main( ){
cout<<"sizeof(double) =
"<<sizeof(double) ;
cout<<"sizeof(char)= "<<sizeof(char) ;
}///:~
char tipinde (signed ,unsigned ya da basit hali) boyut (yani sizeof) her
zaman birdir. Ayrıca char tipinin boyutunun 1
olduğunu belirtmeye gerek yoktur. Öteki tipler içinde sizeof işleci boyutu
bayt (byte) olarak verir.
Burada sizeof un işlev değil işleç olduğunu özellikle belirtmeliyiz. sizeof u
bir tipe uyguladığınızda sizeof un yanında yukarıdaki örnekte olduğu gibi
ayraçlar bulunur. Ama sizeof u bir değişkene uyguladığınız zaman ayraçlar
gerekmez.
//:B03:sizeofOperator.cpp
int main( ){
int x ;
int i=sizeof x ;
}///:~
sizeof işleci kullanıcının tanımladığı veri tiplerinin de boyutunu verebilir.
Bu konu ilerleyen bölümlerde incelenecek.
asm anahtar kelimesi
C++ içinde donanımınız için assembly programı yazmak amacı ile
kullanılan çıkış mekanizmasıdır. asm ile assembly programlama ortamına
geçilmiş olur. Genellikle assembly içinden C++ değişkenlerine erişip
kullanabilir, işlemcinin özel buyruklarını kullanıp verim sağlayan
assembly sınırlarını belirlersiniz. Doğru sözdizimi için, derleyici
belgelerine bakarak assembly programı yazılmalıdır. Çünkü C/C++ da
assembly, derleyiciye bağımlıdır.
yazılı işleçler
Bunlar mantık ve bit işleçleridir. Klavyelerinde &, |, ^ gibi işaret
olmayanlar için, kullanacakları karakterlerin yerine geçen yani, aynı
anlamlı anahtar kelimelerdir. C++ ya eklenen bu anahtar kelimeler aşağıda
verilmiştir;
anahtar kelime
and
or
not
not_eq
bit_and
and_eq
bit_or
or_eq
xor
xor_eq
comp
anlamı
-----------------------&& (mantık ve)
----------------------- || (mantık ya da)
----------------------- ! (mantık değil)
----------------------- != (mantık eşit değil)
----------------------- & (bit ve)
----------------------- &= (bit ve atama)
----------------------- | (bit ya da)
----------------------- |= (bit ya da atama)
----------------------- ^ (bit dış ya da)
----------------------- ^= (bit dış ya da atama)
----------------------- ~ (bir tamamlayıcı)
Derleyiciniz standart C++ yi destekliyorsa yukarıdaki anahtar kelimeleri de
kullanabilirsiniz demektir.
Harç tiplerin oluşturulması
C ve C++ deki ana veri tipleri ve onların uzantıları esas olmalarına
rağmen, uygulamaların bazılarında çok basit kalırlar. Bu nedenle daha
özgün veri tipleri gerektiğinde, temel veri tiplerini kullanarak yenilerini
oluşturmak mümkündür. Bunların en önemlisi, C++ daki class a karşılık
gelebilecek olan struct'tır. Yeni özgün bir tip oluşturmanın en basit yolu
ise typedef kullanarak birisine takma ad koymaktır.
typedef ile yeniden adlandırma
Bu anahtar kelime yapacağından daha fazlasına söz verir. typedef tip
tanımlamasıdır, takma ad ise yeniden isimlendirmedir, söyleyebileceğimiz
takma adın daha doğru olduğudur. Ve aslında yapılan işlemi de en iyi
açıklayan kelimedir. Sözdizimi aşağıdaki gibidir;
typedef var-olan-tip-tanımı takma-ad ;
Programcıların çoğunluğu typedef i tipler biraz karmaşıklaştığı zaman,
fazla klavye tuşu kullanmamak için tercih ederler. Yaygın typedef lerden
biri aşağıda verilmiştir;
typedef unsigned long ulong ;
Artık bundan sonra ulong yazdığınız zaman derleyiciniz unsigned long
anlayacaktır. Bunu önişlemleyici işaretleri ile de başarabileceğinizi
düşünebilirsiniz, fakat derleyicinin tipleri algılayan tuş bilgileri önemlidir,
o nedenle typedef esastır. typedef in kullanışlı olduğu başka bir yerde
göstergeçlerdir. Daha önce gösterilen
int* x, y ;
Burada x adresinde int değişken bulunur ve y, int değişkendir. Yani x int*
e ait, y ise sadece int e aittir. * işareti sağ tarafa bağlar, sol tarafa değil.
typedef kullanalım ;
typedef int* IntPtr ;
IntPtr x,y ;
Şimdi x ve y int* olur. Temel veri tiplerini kullanırken typedef ten
sakınmak programınızı hem daha okunaklı hem de
daha az hatalı yapar. C programlama da typedef in struct ile birlikte
kullanımı özellikle çok önemlidir.
struct ile değişkenleri biraraya getirme
Yapılar struct anahtar kelimesi kullanılarak oluşturulur. C++ dilinde
struct aynı zamanda sınıfı da tanımlar. class ile struct arasındaki tek fark
struct'ta üyelerin varsayılan erişimi public'tir. class'ta varsayılan erişim
private'dir. struct'ta erişimi private yapmak için private yazılması
zorunludur.
yapı bildirim genel yazımı aşağıda verildi;
struct struct-adı : kalıtım-listesi{
//varsayılan public üyeler
protected:
//protected üyeler
private:
//private üyeler
}nesne-listesi
C dilinde struct'lara çok sayıda kısıtlama konmuştu; bunların ilki sadece
veri üyelerini içermesi, üye işlevlere izin verilmemesidir. C struct'ları
kalıtımı da desteklemez. Ayrıca C dilinde bütün üyelere erişim public
olup, protected ve private erişim bulunmaz.
struct bir grup değişkeni bir çatı altında toplamanın yoludur. Bir kez
struct oluşturunca geliştirilen bu yeni tip ile değişik kullanımlar
oluşturabilirsiniz. Örnek:
//:B03:SimpleStruct.cpp
struct Structure1{
char c;
int i;
float f;
double d;
};
int main( ){
struct Structure1 s1,s2 ;
s1.c='a' ;
s1.i=1 ;
s1.f=3.14 ;
s1.d=0.00033 ;
s2.c='a' ;
s2.i=1 ;
s2.f=3.14 ;
s2.d=0.00033 ;
}///:~
struct bildirimi mutlaka noktalı virgülle sonlanmalıdır. Yukarıda main( )
de Structure1 struct ında iki örnek (s1,s2) oluşturulmuştur. Bunların her
ikiside kendi ayrı c, i, f ve d lerine sahiptirler. Bu sayede s1 ve s2 ayrı
değişken toplulukları meydana getirirler.s1 ya da s2 nin ögelerinden birini
seçmek için daha önce C++ da bahsedilen '.' işareti
kullanılır.
Yukarıda göze çarpan birşey de Structure1 in kullanımında ki zorluktur
(sadece C de, geri dönüş olduğunda istenir,
C++ da değil). C dilinde değişkenleri tanımlamak için sadece Structure1
değil, struct da eklenmek zorundadır. Yani mutlaka struct Structure1
yazılmalıdır. İşte bu nokta, C dilinde typedef in özellikle kullanışlı olduğu
yerdir.
//:B03:SimpleStruct2.cpp
//struct kullanarak tip tanımlaması
typedef struct{
char c;
int i ;
float f ;
double d ;
}Structure2 ;
int main( ){
Structure2 s1,s2 ;
s1.c='a' ;
s1.i=1 ;
s1.f=3.14 ;
s1.d=0.0003 ;
s2.c='a' ;
s2.i=1 ;
s2.f=3.14 ;
s2.d=0.0003 ;
}///:~
Bu yolla typedef i kullanarak Structure2 i yerleşik veri tipi olarak
kullanırız. Aynı int, char ,float ya da double da olduğu gibi (Bu C de
böyle, C++ da typedef i kaldırırız). Burada dikkat edilmesi gereken;
sadece veri tipinin özelliğinin açıklandığı,davranışın ise, C++
nesnelerinde gösterilen gibi, belirtilmemesidir. struct deyimi typedef
nedeni ile ikinci sıraya düştü. Tanımlama sırasında struct kullanılması ise
struct ın kaynak olacağı durumlara birçok kez rastlanacak olmasındandır.
Böyle durumlarda struct ın adını struct adı olarak olarak yineleyebiliriz
ve typedef olarakta kullanabiliriz.
//:B03:SelfReferential.cpp
//struct ın kendini dayanak alan
typedef struct SelfReferential{
int i ;
SelfReferential* sr ;
//fırlatma başladımı?
}SelfReferential ;
int main( ){
SelfReferential sr1,sr2 ;
sr1.sr=&sr2 ;
sr2.sr=&sr1 ;
sr1.i=57 ;
sr2.i=1048 ;
}///:~
Yukarıdaki örneği bir süre incelersek,sr1 ve sr2 nin veri parçalarını
tutarken birbirini gösterdiğini farkederiz.
Gerçekte struct adı ile typedef adı aynı değildir, burada böyle
yapılmasının sebebi unsurları basitleştirmek içindir.
Göstergeçler ve struct'lar
Yukarıdaki örnekte bütün struct lar nesne olarak işlemlendi. Bu struct
nesnelerinin adresini aynı, bellekteki bir yer gibi
elde edebilirsiniz (yukarıdaki SelfReferential.cpp de görüldüğü gibi). Belli
bir struct nesnesinin ögesini seçebilmek için
daha önce de gördüğümüz '.' işaretini kullanırız. Bir struct nesnesini
gösteren göstergeçiniz varsa o zaman o nesnenin
ögesini seçebilmek için mutlaka, farklı bir işleç olan '-> ' işlecini
kullanmalısınız. İşte bir örnek ;
//:B03:SimpleStruct3.cpp
//struct ları göstergeçlerle kullanalım
typedef struct Structure3{
int i ;
char c ;
float f ;
double d ;
}Structure3 ;
int main( ){
Structure3 s1,s2 ;
Structure3* sp=&s1 ;
sp->i=1 ;
sp->c='a' ;
sp->f=3.14 ;
sp->d=0.0003 ;
sp=&s2 ;//başka bir struct nesnesini gösteriyor
sp->i=1 ;
sp->c='a' ,
sp->f=3.14 ;
sp->d=0.0003 ;
}///:~
Yukarıdaki örnekte başlangıçta göstergeç s1 struct ını gösteriyor ve s1
struct ının ögeleri '-> ' imleci ile saptanıyor.
(aynı işleci bu ögeleri okumak için de kullanabilirsiniz.). Daha sonra sp s2
struct ını gösteriyor. Buradada aynı işleç kullanılarak ögeler seçiliyor.
Göstergecin çok dinamik bir şekilde nesneleri işaretlemede
kullanılabileceğine dikkat
edilmelidir; bu özellik programlamada oldukça büyük esneklik sağlar.
Şimdilik struct lar hakkında bu kadar bilgi size yeter. Fakat bölümler
ilerlerken struct lar ile daha fazla haşır neşir olacak, deneyimleriniz de
artacak, ve daha sonra struct larla çalışırken oldukça rahatlık
hissedeceksiniz.
enum ile programlama
enum anahtar kelimesi ile veri adlarına sayı değerleri atanır. Böylece
kodlar daha kolay okunabilir. enum anahtar kelimesi ile 0, 1, 2, 3,..,vs gibi
ardışık değerler tanımlayıcılar listesine (C den) atanır. enum değişkenlerini
sadece int olarak bildirebilirsiniz. enum bildirimi struct bildirimine çok
benzer.
Bu tür veri tipleri özelliklerin belli bir sırada kullanılmasına yardımcı
olurlar.
//:B03:enum.cpp
//şekillerin dizisini belirlemek
enum ShapeType{
circle ;
square ;
rectangle ;
};
//struct gibi mutlaka noktalı virgülle sonlanmalı
int main( ){
ShapeType shape=circle ;
//etkinlikler başlıyor..
//şeklin ne olduğuna bakarak bazı şeyler yapalım
switch(shape){
case circle :/*daireler doldur */ break;
case square :/*kareler doldur */ break ;
case rectangle :/*dörtgenler doldur */ break;
}
}///:~
shape, ShapeType sayılaştırılmış veri tipinin bir değişkenidir, değeri,
sayılaştırma değeriyle karşılaştırılır. shape gerçek anlamda int olduğu için
değeri int olan herhangi bir değeri tutabilir (negatif de olabilir). Ve int
herhangi bir değeri, sayılaştırılmış veri tipleri ile karşılaştırabilirsiniz.
Yukarıdaki örnekte switch komutunun seçim de kullanılması sorunlu
programlamaya yolaçmaktadır, bunu özellikle
akılda tutmak gerekir. C++ bu tür sıralama için çok daha iyi bir kodlama
yoluna sahiptir. Bu ilerleyen konularda açıklanacak.
Derleyicinizin değişkenlere değer atamasından hoşlanmadıysaniz, bunu
aşağıdaki şekilde de yapabilirsiniz;
enum ShapeType {
circle=1,square=2,rectangle=5
};
Siz bazı değişkenlere değer atayıp bazılarına atamazsanız, derleyici değer
atanmayan değişkene bir sonraki int değeri
atar. Örnek:
enum zihin {parlak=45,zeki};
yukarıda derleyici, zeki değişkenine 46 değerini atar. Sayılaştırılmış veri
tipleri kullandığınızda, kodların ne kadar okunabilir olduğunu
görüyorsunuz. C de bazı şeyleri başarabilmek amacı ile yaptıklarımız için,
C++ da sınıflar olduğundan ileride de göreceğimiz gibi enum için de
uygulamalar azdır.
Sayılaştırmalarda (enum kullanımı) tip denetimi
C de sayılaştırma işlemi oldukça ilkeldir ve sadece, isimlere int değerler
atanır, ayrıca tip denetimi de yapılmaz. C++
da bildiğiniz ve beklediğiniz gibi tip fikri esas unsurdur ve sayılaştırılmış
veri tipleri için de, bu geçerlidir. İsim verilmiş sayılaştırma yaptığınız
zaman, aynen bir sınıfla yapılan gibi yeni bir tip oluşturuyorsunuz.
Sayılaştırma adı, çevirici için artık bundan sonra, ayrılmış özel bir ad olur.
Bunlara ek olarak C++ da sayılaştırılmış veri tiplerine, C ye göre çok sert
veri tipi denetimi uygulanır. Bunu özellikle adı verilen color
sayılaştırmasında farkedeceksiniz; C de a++ diyebilirsiniz,fakat buna C++
da izin verilmez. Bunun sebebi sayılaştırmadaki artmanın iki tip çevrimine
neden olmasıdır,bunlardan biri C++ da geçerli olup diğeri geçerli değildir.
Önce sayılaştırmanın değeri color dan int e içeriden döküm yapılır,daha
sonra değer bir artar ve daha sonra da int color a geri döküm olur. Bu
işleme C++ da izin verilmez bunun sebebi de color ile int in farklı tipler
olmasından kaynaklanmaktadır ve color int e eşit değildir. Bunu, renk
listesindeki blue nun arttığında, değerinin iki olma sebebi
anlamlı kılar.color da artım isterseniz o zaman onun sınıf (artma işlemi ile
beraber) olması lazımdır, yani enum değil.
Bunun esas nedeni sınıf çok daha güvenli kılınabilir. Kod yazmanın
herhangi bir anında enum tipe iç çevrim yapıldığı kabul edildiğin de
derleyici bunu doğal olarak tehlikeli etkinlik olarak gösterir.
Biraz sonra anlatılacak olan union lar C++ dakilere benzeyen, ek tip
denetimlerine sahiptir.
union
union bütün veri üyeleri aynı bellek alanını paylaşan bir yapıdır. C++
dilinde union hem üye işlevleri hem de verileri ihtiva eder. union'da bütün
üyeler varsayım olarak public erişime sahiptir. private üyeler oluşturmak
için mutlaka private anahtar kelimesini kullanmalısınız. union
bildiriminin genel biçimi aşağıdaki gibidir;
//:B03:union_deneme.cpp
//union açıklaması
union sınıf-adı{
//varsayım olarak public üyeler
private:
//private üyeler
}nesne-listesi
C dilinde union'da sadece veri üyeleri bulunur ve private erişim olmaz.
union üyeleri birbirini kaplar. Örnek;
//:B03:selo.cpp
//örnek
union selo{
char ch ;
int x;
}s;
|------2 bayt int--------|
------------|-------------|-1 bayt char-|--------------|
union üyelerin işlemlenmesi için değişkenlerde nokta işleci,
göstergeçlerde ise ok işleci kullanılır.
union'u C++ da kullanırken çok sayıda kısıtlama bulunur. Bir union
herhangi bir sınıftan türetilemez, ana sınıf olamaz, sanal (virtual) üye
işlevi olamaz, ve üyeleri static olarak bildirilemez. Dayançlı (reference)
üye kullanılamaz. Bir sınıf nesnesi üyesi olamaz.
union kullanımının C++ da çok özel bir biçimi bulunmaktadır; anonim
union . Bu union'nun adı bulunmaz, ve herhangi bir nesnesi bildirilmez.
Anonim union derleyiciye üye değişkenlerin aynı bellek alanını
paylaştığını söyler. Bu değişkenler dizgede, ne nokta ne de ok işleçleri
kullanılmadan doğrudan işlemlenirler. Örnek;
//B:03:anonymous_union.cpp
//anonim union
union{//anonim union
int a ; //a ve b aynı bellek
float b; //paylaşırlar
};
...
a=22 ;
//a'ya erişim
cout<<b ; //b'ye erişim
Burada a ve b aynı bellek yerini paylaşırlar. Görüldüğü gibi değişken
adları nokta işleci kullanılmadan işlemlendi. Genel olarak bütün union'lara
konan bütün kısıtlamalar anonim union'lar içinde geçerlidir. private ve
protected üyeleri bulunmaz. Son olarakta şunu belirtelim; isimalanı
(namespace) görüntüsüne sahip bir anonim union mutlaka static olarak
bildirilmelidir.
union ile bellek tasarrufu
Bazen programlarda aynı değişkenler farklı veri tipleri ile kullanılmaktadır.
Böyle bir durumda iki seçenek vardır. Ya
bütün olası tipleri içeren struct oluşturulabilir veya union
kullanabilirsiniz. Bir union bütün verileri tek bir tip bellek alanına koyar.
union'a yerleştirilecek en büyük boyutlu öge tipi hesaplanır ve buna göre
union un boyutu belirlenir. Bu işlem sayesinde union kullanarak, bellek
tasarrufu yapabiliriz. Çünkü her tip veriye ayrı ayrı yer ayrılmaz. En
büyüğe göre barınma alanı belirlendiği için, öteki tipler kendilerine sıra
geldiği zaman kolayca bellek alanına yerleşirler.
Herhangi bir anda bir değer union a yerleştirildiğinde, bu değer her zaman,
union un başındaki aynı yerden başlar, fakat
sadece gereken büyüklükte bellek kullanır. Böylece herhangi bir union
değişkenini tutabilen süper değişken meydana gelir. Bütün union
değişkenlerinin adresleri aynıdır (sınıf ya da struct ta adresler farklıdır),
yani tek bellek alanı var.
İşte şimdi basit bir union kullanımı. Bazı ögeleri çıkarmanın union un
büyüklüğü üzerindeki etkilerini araştıralım. Burada dikkat edilmesi
gereken; bir union da bir veri tipinin birden fazla örneğinin bildirilmesinin
anlamsızlığıdır.(böyle yapmayacaksınız farkılı isimler kullanacaksınız.)
//:B03:Union.cpp
//Union un büyüklüğü ve basit kullanımı
#include <iostream>
using namespace std ;
union Packed{//bildirim sınıftakinin benzeri
char i;
short j;
int k;
long l;
float f;
double d;
//union un boyutu double kadar olur zira en
//büyük ölçekli veri tipi yukarıda double dır.
}; //union da aynı struct gibi noktalı virgül ile biter
int main( ){
cout<<"sizeof(Packed)=
"<<sizeof(Packed)<<endl ;
Packed x;
x.i='c' ;
cout<<x.i<<endl ;
x.d=3.14 ;
cout<<x.d<<endl ;
}///:~
Derleyici seçilen union üyesine göre, uygun atamayı gerçekleştirir.
Atamayı siz gerçeklerseniz, derleyici union la yaptıklarınızı dikkate almaz.
Yukarıdaki örnekte x e kayan noktalı değer atayabilirsiniz.
x.f=3.3333 ;
ve daha sonra çıkışa bunu int miş gibi gönderirseniz;
cout<<x.i;
bunlar çöplük üretir.
diziler (arrays)
Diziler bileşik veri tipi örneklerindendir. Diziler birden fazla değişkeni
birarada tutarlar,değişkenin biri öbüründen
sonra gelir, aynı isim altında bulunurlar. Örnek;
int a[100];
bildirimde, 100 tane int değişkeni birbirinin üzerinde, yığıt olarak
tanımlanmıştır. Her değişken için farklı bir ad konmamış, bilakis bütün
değişkenler a adı altında ortak tanımlanmışlardır. Dizinin ögelerinden
birine erişebilmek için yine aynı kare ayraç sözdizimi kullanılır.
a[45]=88;
Burada mutlaka hatırlanması gereken, dizi de 100 tane int değişken
olmasına rağmen, seçmeye sıfırdan başlanacağıdır.
(bazen buna sıfır dizinleme denir). Böylece 100 tane değişkeni olan bir
dizinin ögelerini seçerken 0-99 arasında ki sayılar kullanılır.Aşağıdaki
örnekteki gibi;
//:B03:Arrays.cpp
include <iostream>
using namespace std;
int main( ){
int a[10];
for(int i=0;i<10;i++){
a[i]=i*10;
cout<<"a["<<i<<"]=
}
}///:~
"<<a[i]<<endl;
Dizi ögelerine erişim oldukça hızlıdır. Dizideki son ögeyi geçen bir öge
seçilirse-güvenlik ağı olmadığı için-başka değişkenlere atlanır. Derleme
sırasında mutlaka dizi boyutu belirlenmelidir; çalışma sırasında dizi
boyutunu değiştirmek isterseniz bu işlem yukarıdaki söz dizimi ile olmaz
(C de, dinamik olarak dizi boyutunu belirlemenin bir yolu vardır fakat
oldukça karmaşıktır). C++ da ise daha önceki bölümlerde tanıtılan vector,
nesne benzeri dizi oluşturur ve kendisini otomatik olarak boyutlandırır.
Derleme sırasında dizi boyutu bilinmediği zaman, bu yöntemi uygulamak
daha iyidir.
struct dahil her tipte dizi yapılabilir.
//:B03:StructArray.cpp
//struct dizisi
typedef struct{
int i,j,k ;
}ThreeDpoint ;
int main( ){
ThreeDpoint p[10];
for(int i=0;i<10;i++){
p[i]*i=i+1 ;
p[i]*j=i+2;
p[i]*k=i+3;
}
}///:~
Yukarıda struct tanımlayıcısı i ile, for döngüsünün i sinin birbirlerinden
bağımsız olduğuna dikkat edilmelidir.
Bir dizideki ögelerin birbirini takip etmesini izlemek için, ardışık ögelerin
adreslerini çıktıda yazdırabiliriz;
//:B03:ArrayAddresses.cpp
#include <iostream>
using namespace std;
int main( ){
int a[10];
cout<<"sizeof(int)= "<<sizeof(int)<<endl;
for(int i=0;i<10;i++)
cout<<"&a["<<i<<"]= "<<(long)&a[i]<<endl;
}///:~
Yukarıdaki programı çalıştırdığınız zaman, her bir öge bir öncekinden bir
int boyutu ötededir.Yani ögeler birbirinin
üzerine ardışık olarak yığılır.
Göstergeçler ve Diziler
Dizilerin tanımlayıcıları bilinen değişken tanımlayıcılarına benzemez. Dizi
tanımlayıcısı bir sol-değer değildir ve kendisine atama yapılamaz.
Gerçekte sadece kare ayraca yapılmış etiketlemedir. Dizi adını kare
ayraçsız yazdığınız
zaman dizinin başlangıç adresini elde edersiniz.
//:B03:ArrayIdentifier.cpp
include <iostream>
using namespace std;
int main( ){
int a[10];
cout<<"a= "<<a<<endl;
cout<<"&a[0]= "<<&a[0]<<endl;
}///:~
Yukarıdaki program çalıştırıldığı zaman birbirinin aynı olan iki adres elde
edilir. (Hex tabanlı yazım elde edilir,çünkü
long a döküm yapılmadı.)
Yalnız okunabilir bir göstergeçi dizi başlangıçı için kullanarak, dizi
tanımlayıcısını inceleyebiliriz. Dizi tanımlayıcılarını başka bir yeri
gösterecek şekilde değiştirememize rağmen, başka bir göstergeç oluşturup,
dizi içinde dolaştırmak amacı ile kullanabiliriz. Gerçekte kare ayraç
sözdizimi normal göstergeçlerle olduğu gibi çalışır;
//:B03:PointersAndBrackets.cpp
int main( ){
int a[10] ;
int* ip=a ;
for(int i=0;i<10;i++)
ip[i]=i*10 ;
}///:~
Dizi etiketlemenin başlangıç adresini üretmesi, işleve dizi aktarırken
oldukça önemlidir. Diziyi bir işlevin değişkeni olarak bildirirseniz, aslında
bildirdiğiniz bir göstergeçtir. Aşağıdaki func1( ) ve func2( ) işlevleri aynı
etkide değişken listesine sahiptirler.
//:B03:ArrayArguments.cpp
#include <iostream>
#include <string>
using namespace std;
void func1(int a[ ],int size){
for(int i=0;i<size;i++)
a[i]=i*i-i;
}
void func2(int* a,int size){
for(int i=0;i<size;i++)
a[i]=i*i-i;
}
void print(int a[ ],string name,int size){
for(int i=0;i<size;i++)
cout<<name<<"["<<i<<"]= "
<<a[i]<<endl;
}
int main( ){
int a[5],b[5];
//herhalde çöplük bunlar
print(a,"a",5);
print(b,"b",5);
//dizilerin ilk değerlerini ata
func1(a,5);
func1(b,5);
print(a,"a",5);
print(b,"b",5);
//diziler her zaman değiştirilir
func2(a,5);
func2(b,5);
print(a,"a",5);
print(b,"b",5);
}///:~
func1( ) ve func2( ) değişkenlerini farklı biçimde bildirselerde, işlev
içindeki kullanımları aynıdır. Yukarıdaki örneğin ortaya çıkardığı başka
özellikler de göze çarpar; diziler değer aracılığı ile aktarılamaz, yani, hiçbir
zaman işleve aktarabilmek için, otomatik olarak dizinin yörel kopyasını
elde edemezsiniz. Böylece bir diziyi değiştireceğiniz zaman
her zaman dış nesneyi değiştiriyorsunuz. Olağan değişkenlerde yapıldığı
gibi bir değer aktarımı beklenirse, bu biraz şaşırtıcı gelebilir. Burada göze
çarpan başka bir şey de; print( ) işlevinde dizi değişkenleri için, kare ayraç
kullanılıyor olmasıdır. Göstergeç biçimli ve kare ayraç biçimli uygulamalar
aynı etkiye sahip olmalarına rağmen, dizileri değişken olarak aktarırken,
kare ayraç kullanımı dizi değişkenlerinin okunmasını kolaylaştırır.(Tabii
size)
Ayrıca size değişkeni her durumda aktarılır. Sadece hemen dizi adresini
aktarmak yeterli bilgi değildir; mutlaka her zaman işlevdeki dizinin
büyüklüğünü bilmeniz gerekir, yoksa dizinin sonunda çıkış yapamazsınız.
Veri tiplerinin hepsi ile dizi oluşturulabilir, hatta göstergeç dizileri bile
olabilir. Gerçekte, programınızda komut satırına değişkenler koymak
istediğinizde, main( ) işlevi için, C ve C++ da değişken listesi vardır, ve
görünüşü de aşağıdaki gibidir;
int main(int argc,char* argv[ ]) //..
İlk değişken(arg c) dizi içindeki öge sayısıdır, dizi de ikinci (argv[ ])
değişkendir. İkinci değişken her zaman char* dizisidir. Bunun nedeni;
komut satırında aktarılan değişkenlerin karakter dizileri olmasıdır. (bir
dizinin sadece göstergeç ile aktarılabileceğini hatırlayın!). Komut satırında
boşluk ile sınırlandırılmış herbir karakterler öbeği için
ayrı bir dizi değişkeni oluşur. Aşağıdaki örnek komut satırı değişkenlerinin
herşeyini, dizi adım aşamaları biçiminde yazdırır.
//:B03:CommandLineArgs.cpp
#include <iostream>
using namespace std;
int main(int argc,char* argv[ ]){
cout<<"argc= "<<argc<<endl;
for(int i=0; i<argc; i++)
cout<<"argv["<<i<<"]= "
<<argv[i]<<endl;
}///:~
Yukarıda farkettiğiniz gibi argv[0] programın kendi adı yani ilk satırdır.
Bu sayede program, kendine kendisi hakkında bilgi verir. Ayrıca program
değişkenleri dizisine birden fazla şey ekler, yaygın hatalardan biride; argv
[1] yi
istediğiniz zaman, komut satırı değişkenlerini getirirken argv[0]nin sıkıca
tutuluyor olmasıdır.
main( ) işlevinde argc ve argv kullanmak zorunda değilsiniz. Bu tür
tanımlayıcılar genellikle alışkanlıktır.(kullanmadığınız zaman insanlar
şaşırabilir). Yukarıdaki gösterimden başka ikinci bir argv gösterimi daha
vardır;
int main(int argc,char** argv){//...
Her iki biçimde aynı anlama gelir. Biz kitabımız da "bu karakter
göstergeçlerinin dizisidir" anlamına geleni tercih edeceğiz.
Komut satırından elde edilen her şey karakter dizisidir; bir değişken başka
bir tip olarak işlemlenmek istenirse programda onun yeni tipe
çevrilmesinden programcı sorumludur. Standart C kütüphanesinde sayılara
çevirmeyi kolaylaştırıcı yardımcı işlevler vardır. Bunlar <cstdlib> başlığı
altında bildirilir. En basitleri arasında; atoi( ), atol( ) ve
atof( ) işlevleri bir ASCII karakter dizisini int, long ve double kayan
noktalı değerlere çevirirler. Şimdi atoi( ) işlevini kullanan bir örneği
inceleyelim (öbür iki işlev de aynı biçim de çağrılır);
//:B03:ArgsToInts.cpp
//Komut satırı değişkenlerini int e çevirme
#include <iostream>
#include <cstdlib>
using namespace std;
int main(int argc,char* argv[]){
for(int i=0;i<argc;i++)
cout<<atoi(argv[i])<<endl;
}///:~
Yukarıdaki programda komut satırına istediğiniz sayıda değişken
koyabilirsiniz. Yine for döngüsünde i 1 den başladığı için argv[0] yani
program adı atlanmıştır. Ve eğer ayrıca komut satırına kayan noktalı
ondalık sayı koyarsanız, atoi( ) işlevi sadece sayının ondalık noktasına
kadar olan kısmını alır. Komut satırına sayı olmayan unsurları koyarsanız o
zaman atoi( ) işlevinden sıfır(0) değeri döner.
Kayan noktalı biçemin (format) incelenmesi
Değişik veri tiplerinin iç yapılarını incelemek için, bu bölümün başlarında
geçen printBinary( ) işlevi oldukça kullanışlıdır. Bunların içinde en ilginç
olanı; C ve C++ da çok büyük ve çok küçük sayıları, sınırlı büyüklükteki
yerde saklayan, kayan noktalı biçemdir(format). Bütün ayrıntılar burada
gösterilemeyecek olsada, float ve double da bitler üç temel bölüme ayrılır;
üs, mantis ve işaret biti; böylece sayılar bilimsel biçimde gösterilir.
Aşağıdaki program değişik kayan noktalı sayıların iki tabanlı biçimlerini
yazdırır, bunları inceleyerek kullanılan derleyicinin kayan noktalı biçemi
hakkında bilgi edinilebilir (genellikle IEEE standartıdır ama siz yine de
kendi derleyicinizi izleyin).
//:B03:FloatingAsBinary.cpp
//{L}printBinary
//{T} 3.14159
#include "printLibrary"
#include <iostream>
#include <cstdlib>
using namespace std;
int main(int argc,char* argv[]){
if(argc!=2){
cout<<"Mutlaka bir sayi girin "<<endl;
exit(1);
}
double d=atof(argv[1]);
unsigned char* cp=reinterpret_cast<unsigned char*>(&d);
for(int i=sizeof(double);i>0;i-=2){
printBinary(cp[i-1]);
printBinary(cp[i]);
}
}///:~
Programımızda bulunan argc değişkeninin değerini denetleyerek, eğer tek
değişken varsa, ki burada ikidir, kendini emniyete alır.(eğer değişken
yoksa değeri birdir zira, program adı her zaman argv nin ilk ögesidir). Bu
geçersiz olduğu zaman bir ileti yazdırılır,ve programı sonlandırmak için de
Standart C kütüphanesinden exit( ) işlevi çağrılır.
Program, komut satırından öge yakalar, ve bunların karakterlerini atof( )
işlevini kullanarak, double a çevirir. Daha
sonra double veri tipi adreslerini kullanarak byte dizisi olarak düzenlenir,
ve sonra da unsigned char* tipine döküm yapılır. Verileri ekranda görmek
içinde her byte printBinary( ) işlevine aktarılır.
Yukarıdaki örnek, byte dizisini işaret biti en başta görünen yapıyla
görüntülemek için kurulmuştur. Belki sizin derleyiciniz bundan farklı
olabilir, bu nedenle siz düzenlemenizi kendi derleyicinize göre hazırlayın.
Bu arada bir şeyi daha belirtmeliyiz; kayan noktalı biçemleri anlamak çok
kolay değildir,örnek olarak üs ve mantis kısımları genellikle byte
sınırlarına yerleşmemiş, ve herbiri için belleğe sıkıca bağlı birkaç bit özel
olarak ayrılmıştır. Gerçekte neler olduğunu görmek için sayının her
parçasının bellek boyutunu bilmeye gerek vardır. (işaret biti her zaman bir
bit uzunluğundadır fakat üs ve mantis uzunlukları değişebilir). Son
olarakta her parçadaki bitleri ayrı ayrı yazdırmalısınız.
Göstergeç aritmetiği
Diziyi işaret eden göstergeçle yapılan herşey diziye sanki takma ad
koymak gibi bir şey olsaydı, dizi göstergeçleri fazla ilginç olmayacaktı.
Aslında dizi göstergeçleri bundan çok daha esnek özellikler taşımaktadır,
zira onlar başka bir yeri gösterecek şekilde değiştirilebilir. (yalnız dizi
tanımlayıcısı başka bir yeri gösterecek şekilde değiştirilemez, bunu
unutmayın!)
Göstergeç aritmetiği bazı aritmetik işleçlerin göstergeçlere uygulanmasıdır.
Göstergeç aritmetiği normal aritmetik işlemlerden ayrı bir konudur, bunun
sebebi de; göstergeçlerin doğru çalışmaları için gereken koşulları yerine
getirmektir. Örnek olarak (++) işlecini inceleyelim; göstergeçlerle en
yaygın kullanılan işleçlerden olan (++) işleci
"göstergeçe bir ekle " demektir. Bunun anlamı ise; göstergeçin "sonraki
değere" gitmesidir. İşte bir örnek program parçası;
//:B03:PointerIncrement.cpp
#include <iostream>
using namespace std;
int main( ){
int i[10];
double d[10];
int* ip=i;
double* dp=d;
cout<<"ip= "<<(long)ip<<endl;
ip++;
cout<<"ip= "<<(long)ip<<endl;
cout<<"dp= "<<(long)dp<<endl;
dp++;
cout<<"dp= "<<(long)dp<<endl;
}///:~
Program derlenip çalıştırıldığı zaman, derleyici tipine göre farklı neticeler
elde edilir. Burada asıl ilginç olan int* ile double* için aynı işlemin
yapılmasıdır. Bizim makinemizde göstergeç, int* için 4 byte, double* için
8 byte değişiklik gösterdi. Aslında rastlantı değil, bunlar benim
makinemde, int ve double veri tipleri için ayrılmış bellek uzunlukları.
Göstergeç aritmetiğinin ince noktası burasıdır; derleyici dizideki bir
sonraki ögeyi göstermesi için göstergeç değerini hesaplar. (göstergeç
aritmetiği sadece diziler için anlamlıdır). Bu işlemler struct dizileri için de
geçerlidir;
//:B03:PointerIncrement2.cpp
#include <iostream>
using namespace std;
typedef struct{
char c;
short s;
int i;
long l;
float f;
double d,
long double ld;
}Primitives;
int main( ){
Primitives p[10],
Primitives* pp=p;
cout<<"sizeof(Primitives)= "<<sizeof(Primitives)<<endl;
cout<<"pp= "<<(long)pp<<endl;
pp++;
cout<<"pp= "<<(long)pp<<endl;
}///:~
Programın çıktısı benim makinemde aşağıdaki gibidir;
sizeof(Primitives)=40
pp=6683764
pp=6683804
Böylece gördüğünüz gibi derleyici struct dizilerinin göstergeçleri için de
(union ve class lar için de aynen) doğru hesaplama yapar.
Göstergeç aritmetiği (--), (+) ve (-) işleçleri ile de çalışır. Fakat son iki
işleçte işlemler kısıtlıdır; iki göstergeçi
toplayamazsınız, göstergeçleri çıkarırsanız; sonuç, göstergeçler arasındaki
ögelerin sayısıdır. Bir int değer ile göstergeçleri, toplayıp çıkarabilirsiniz.
Şimdiki örnekte göstergeç aritmetiğinin inceliklerini görebiliriz.
//:B03:PointerArithmetic.cpp
#include <iostream>
using namespace std;
#define P(EX) cout<<#EX<<": "<<EX<<endl;
int main( ){
int a[10];
for(int i=0;i<10;i++)
a[i]=i;
//dizin değerlerini atama
int* ip=a;
p(*ip);
p(*++ip);
p(*(ip+5));
int* ip2=ip+5;
p(*ip2);
p(*(ip2-4));
p(*--ip2);
p(ip2-ip); //ögelerin sayısını verir.
}///:~
Program başka bir makro ile başlamaktadır; kelimeleştirme (stringizing)
denilen önişlemleyici özelliğini kullanan bu yapı (açıklamadan önce #
işareti kullanılır) herhangi bir açıklamayı alır, onu karakter dizisine çevirir.
Böylece yazdırılacak olan açıklamayı iki nokta üstüste ve onu da
açıklamanın değeri takip eder. Bunlar çok bilinen uygulamalardır. main( )
işlevinde üretilebilecek yararlı kısaltmalar vardır.
Göstergeçlerde önceden ve sonradan kullanılabilen gerek (--) ve gerekse
(++) işleçleri geçerli olsa da, biz yukarıdaki örnekte sadece bu işleçlerin
önceden işlem yapanlarını kullandık. Bunun nedeni yukarıdaki
açıklamada, göstergeçler yerleştirilmeden önce işlemlerin uygulanmasıdır.
Ve bu sayede işlemlerin yan etkilerini görmemiz mümkün olur.
Burada bir defa daha belirtelim; sadece int değerler göstergeçlere
eklenebilir ya da çıkarılabilir. Eğer iki göstergeç toplanmaya ya da
çıkarılmaya çalışılırsa derleyici buna izin vermez. Yukarıdaki programın
çıktısı aşağıda verilmiştir;
*ip:0
*++ip:1
*(ip+5):6
*ip2:6
*(ip2-4):2
*--ip2:5
Bütün durumlarda,göstergeç aritmetiği "doğru yeri" gösterecek biçimde
ayarlanmış, ögelerin büyüklüğünü esas alarak
gösteren göstergeçlerde sonuçlanır.
Başlangıçta göstergeç aritmetiği size çok karışık görünürse, şunu hemen
belirtelim; canınızı sıkmayın,yani fazla kafaya takmayın. Aslında
zamanınızın çoğunluğunu dizi oluşturma ve onu [ ]ile dizinlemek alır, ve
en ayrıntılı göstergeç aritmetik bilgilerini genellikle (++) ve (--) işleçleri
için gerekir. Göstergeç aritmetiği genellikle daha zekice ve karmaşık
programlara saklanır. Standart C++ kütüphanesindeki kılıfların(containers)
çoğunluğunda bu zekice ayrıntılar gizlenmiş olarak bulunurlar, o nedenle
sizin bunları dertlenmenize gerek yoktur.
Hata ayıklamanın ipuçları
İdeal bir ortamda, mükemmel bir hata ayıklayıcı vardır ve bu sayede
programın davranışları size saydamlaşır, böylece
hataları hızla ve kolayca belirleyebilirsiniz. Hata ayıklayıcıların
çoğunluğunda, kör noktalar bulunur. Bunlar, bazı kod parçalarının,
programda gömülü olarak bulunmasını gerektirir, böylece programda neler
olduğunu anlarsınız.
Burada bir şey daha ekleyelim, bazı program geliştirme ortamlarında
(tekleşik(embedded) dizgelerde) hata ayıklayıcı bulunmaz veya sınırlı geri
besleme bulunur (tek satırlık LED ekranı). Böyle durumlarda program
çalışması
hakkında bilgileri görmek ve incelemek için yollar bulmalısınız. Bu bölüm
bunları yapmak için size bazı teknikler önerecektir.
Hata ayıklama bayrakları
Programa hata ayıklamak için, özel bağlantılı kodlar yerleştirdiğiniz zaman
sorunlarla karşılaşırsınız. Çok fazla bilgi elde etmeye başladığınız zaman,
hataları yerlerinden ayırmak zorlaşır. Hata bulduğunuzu düşündüğünüz de
hata ayıklama kodunu söküp almaya başlamalısınız, sadece gerekince
tekrar geri koymalısınız. İki çeşit bayrakla bu sorunlar çözülür;
önişlemleyici hata ayıklama bayrakları ve çalışma zamanı hata ayıklama
bayrakları.
Önişlemleyici hata ayıklama bayrakları
Önişlemleyiciyi kullanarak #define ile bir ya da daha fazla hata ayıklama
bayrağı tanımlanır, bayrağı sınamak için
#ifdef deyimi kullanılır ve koşullara göre hata ayıklama kodlaması yapılır.
Hata ayıklama işleminizin tamamlandığını düşünüyorsanız basit bir şekilde
#undef ile bayrak ya da bayraklar ve kod otomatik olarak kaldırılır.
(böylece boyutu küçültür ve çalıştırılabilir dosyanızın çalışma zamanını
azaltırsınız.)
Projenizin inşasına başlamadan önce, isimler arasında tutarlılığı en iyi, hata
ayıklama bayraklarının adları hakkında karar vererek sağlarız.
Önişlemleyici bayraklarını değişkenlerden ayırmak için, hepsi büyük
harfle yazılır. En yaygın bayrak isimlerinden biri DEBUG tır (fakat
NDEBUG ı kullanmayın zira bu anahtar kelime C de ayrılmış özel
kelimedir.) Deyimlerin sıralaması aşağıdaki gibi olabilir;
#define DEBUG //Olasılıkla başlık dosyası içinde
//.....
#ifdef DEBUG //Bayrak tanımlandı ise görmek için bak
/*hata ayıklama kodu */
#endif //DEBUG
C ve C++ ortamlarının çoğunluğunda #define ve #undef bayraklarına,
derleyici komut satırında da izin verilir. Böylece
kodu yeniden derleyebilir ve hata ayıklama bilgisini tek bir komut ile
yerleştirirsiniz. (tercihen makefile ile, daha sonra kısaca anlatılacak).
Ayrıntılar için derleyicinizin belgelerine bakınız.
Çalışma zamanı hata ayıklama bayrakları
Bazı durumlarda hata ayıklama bayraklarını program çalışırken açmak ve
kapatmak daha uygundur, özellikle de
komut satırını kullanarak programa başlarken ayarlanırlar. Büyük
programlarda hata ayıklama kodlarını ekleyip yeniden derlemek çok
uğraştırıcıdır.
Dinamik olarak, hata ayıklama kodunu açmak ve kapatmak için bool
bayrakları oluşturulur.
//:B03:DynamicDebugFlags.cpp
#include <iostream>
#include <string>
using namespace std;
//hata ayıklama bayraklarının global olması gerekli değildir
bool debug=false;
int main(int argc,char* argv[ ]){
for(int i=0;i<argc;i++)
if(string(argv[i])=="--debug=on")
debug=true;
bool go=true;
while(go){
if(debug){
//hata ayıklama kodları
cout<<"hata ayıklayıcılar şimdi açıldı"<<endl;
}else {
cout<<"hata ayıklayıcı şimdi de kapalı"<<endl;
}
cout<<"hata ayıklayıcıyı [aç/kapa/çık]: " ;
string reply;
cin>>reply;
if(reply=="on") debug=true; //aç
if(reply=="off") debug=false; //kapat
if(reply=="quit") break; //'while' dışı
}
}///:~
Yukarıdaki program, sizin programdan çıkmak istediğinizi belirten exit
komutunu yazana kadar, hata ayıklama komutlarını açıp kapatmaya devam
eder. Gördüğünüz gibi kelimeler tam yazılmalıdır,sadece harf yeterli
değildir.
(dilerseniz ilgili kelimeleri kısaltma yoluna gidebilirsiniz). Hata
ayıklayıcıları başlangıçta çalıştırmak için
komut satırı değişkeni seçenek olarak kullanabilir. Bu değişken komut
satırının herhangi bir yerinde bulunabilir.
Zira main( ) deki başlatma kodu bütün değişkenlere bakar. Aşağıdaki
açıklama nedeni ile sınama oldukça basittir;
string(argv[i])
Bu argv[i] karakter dizisini alır bir string oluşturur ve daha sonra ==
işaretinin sağ yanındaki ile kolaylıkla karşılaştırabilir. Yukarıdaki program
(--debug=on) ifadesinin tamamını arar. Ayrıca (--debug=) ıda arayabilir,
ondan sonra geleni de (daha fazla seçenek için) görebilirsiniz. İlerleyen
bölümlerde Standart C++ string sınıfı
hakkında ayrıntılı bilgi verilecektir. Hata ayıklama, global değişken
kullanımının anlamlı olduğu nadir yerlerden olmasına rağmen, bu yolun
zorunluluğu hakkında birşey söylenemez. Değişkenlerdeki küçük harfler,
okuyucuya o değişkenin, önişlemleyici bayrağı olmadığını hatırlatmalıdır.
Değişken ve açıklamaları string'lere çevirme
Hata ayıklama kodunu yazma esnasında, değişken ismini(değişken
tarafından takip edilir) içeren karakter dizisinden oluşan yazdırma
ifadelerini kodlamak, oldukça yorucudur. Allahtan, Standart C de,bu
bölümün baş kısmında kullanılan string oluşturma işleci (#) var. Bir
önişlemleyici macro sunda değişkenden önce (#) işareti koyduğumuz
zaman,
önişlemleyici o değişkeni karakter dizisine çevirir. Bu da, aralarında
noktalama işaretleri olmayan karakter dizilerini ardışık olarak birleştirip
tek bir karakter dizisi yapar ve hata ayıklama esnasında değişken
değerlerini yazdırmak için
çok daha uygun macro lara izin verir:
#define PR(X) cout<<#X"="<<X <<"\n" ;
a değişkenini yazdırmak için PR(a) macro sunu çağırırsanız, aşağıdaki
kodla aynı etkiyi oluşturursunuz:
cout<<"a= "<<a<<"\n" ;
Aynı süreç bütün açıklamalarla da çalışır. Aşağıdaki program, kısaltma
amacı ile macro kullanarak, string yapılan açıklamayı yazdırır ve değerini
bularak sonucu yazar;
//:B03:StringizingExpressions.cpp
#include <iostream>
using namespace std ;
#define P(A) cout<<#A<<": "<<(A)<<endl ;
int main( ){
int a=1,b=2,c=3 ;
P(a) ; P(b) ; P(c) ;
P(a+b) ;
P((c-a)/b) ;
}///:~
Özellikle hata ayıklayıcı olmadığı zaman (ya da çok farklı geliştirme
ortamlarında çalışılmak zorunluluğunda), böyle bir tekniğin ne kadar
vazgeçilmez olduğunu görebilirsiniz. Hata ayıklamadan çıkmak
istediğinizde, ayrıca, P(A) yı "hiçbirşey" olarak tanımlamak amacı ile
#ifdef yerleştirebilirsiniz.
C deki assert( ) macrosu
<cassert> standart başlık dosyasında assert( ) hata ayıklama makrosu
vardır. assert( ), bir değişken yerleştirerek kullanılır ve böylece
açıklamanın "true olacağı ileri sürülür". Önişlemleyici ileri sürüleni
sınamak için kod üretir.
İleri sürülen gerçek (true) değilse, program ileri sürülenle ilgili hata iletisi
verir, sonra durur. Aşağıda basit bir örnek verilmiştir;
//:B03:Assert.cpp
//assert( ) macrosunun hata ayıklama da kullanımı
#include <cassert> //assert( ) macrosu
using namespace std ;
int main( ){
int i=200 ;
assert(i!=200) ; ///iptal
}///:~
Standart C kökenli bu makro ayrıca assert.h başlık dosyasında da bulunur.
Hata ayıklama işlemini tamamladığınız zaman, macro tarafından üretilen
kodu kaldırmak için aşağıdaki satır yazılır;
#define NDEBUG
Bu, derleyici komut satırında NDEBUG ın tanımlandığı, ya da <cassert>
yerleştirilmesinden önce programlarda kullanılır. NDEBUG macrolar
tarafından üretilen kodu değiştiren <cassert> teki bir bayraktır. Kitabın
ilerleyen bölümlerde assert in yerine geçen daha ince usuller gösterilecek.
İşlev adresleri
Bir işlev, derlendikten sonra çalıştırılmak üzere bilgisayara yüklenir, bu
sırada belleğin büyük bir parçasını kaplar.
Bu bellek yeri ve dolayısı ile işlevin, bir adresi olur.
C dili hiçbir zaman başkalarının yapmaktan korktuğu işleri yapan bir dil
değildir. Göstergeçleri işlev adreslerini belirten değişken adresleri olarak
kullanabilirsiniz. İşlev göstergeçlerinin bildirimi ve kullanımı başlangıçta
çok kolay anlaşılabilir değildir, zamanla dilin geri kalanındaki biçeme
uyduğunu görürsünüz.
İşlev göstergeçinin tanımı
İşlev göstergeçleri menü tabanlı programlarda yaygın olarak kullanılır. Bu
yüzden iyi kavranmalıdır.
Değişkeni ve geri dönüş değeri olmayan bir işleve ait göstergeç tanımı ;
void (*funcPtr)( );
Bu şekildeki karmaşık tanımlamaları anlamak için en iyi yol orta yerden
başlamak ve işi yolu ile bitirmektir. "Orta yerden başlama" dan kastedilen
funcPtr değişken adından başlamak demektir. "İşi yolu ile bitirmek" ise
sağdaki en
yakın ögeye bakmak demektir (burada sağda en yakın öge
bulunmamaktadır, sadece kapatan ayraç) vardır.),
daha sonra sola bakılır(burada da (*) işareti bulunmaktadır.), daha sonra
sağa bakılır(işlevi gösteren boş listede
değişken( ) bulunmamaktadır.), daha sonra tekrar sola bakılır (void
görülür, geri dönüş tipi yoktur). Bu sağ-sol-sağ
hareketleri bildirimlerin çoğunda kullanılır.
Yineyelim; "orta yerden başla"(funcPtr ..), sağa git(hiçbirşey yok,
kapatan ayraç), sola git * rastla(..i göster..), sağa git
boş değişken listesini bul (..işlev de değişken yok..), sola git void i bul
(funcPtr, değişkeni bulunmayan işleve göstergeçtir, işlevin geri dönüş tipi
yoktur)
Şimdi *funcPtr nin ayraç içinde bulunma nedenini merak edeceksiniz,
ayraçları kullanmazsanız, derleyici ilgili yapıyı
şu biçimde görür;
void* funcPtr() ;
Değişken tanımlamaktan ziyade işlev bildiriminde bulunuyor olacaksınız
(geri dönüşdeğeri void*). Derleyici süreci
bildirim veya tanım olarak hesaplamaları yapar. Ayraçlara gereksinim
vardır, zira bu sayede işlem ileri geri sıçrama yapar. Eğer ayraç
kullanılmazsa sağa ilk hareketten sonra sağa işlemler devam eder ve
böylece boş değişken listesine geçilir, ayraç olduğunda ise, sağa ilk
işlemden sonra sola sıçrama yapılır bu sefer de (*) e geçilir. Daha sonraki
sağa sıçrama sırasında boş değişken listesine rastlanır.
Karmaşık bildirim ve tanımlar
Bir yandan C ve C++ da bildirim sözdizimlerinin hesaplarını yaparken, öte
yandan çok daha karmaşık yapılar oluşturabilirsiniz. Örnek;
//:B03:ComplicatedDefinitions.cpp
/*1.*/ void * (*(*fp1)(int))[10];
/*2.*/ float (*(*fp2)(int,int,float)) (int) ;
/*3.*/ typedef double (*(*(*fp3)( ))[10])( ) ;
fp3 a;
/*4.*/ int (*(*f4( ))[10])( );
int main( ){ }///:~
Herbirinin içinde, sağ-sol adımlar kullanılarak hesaplamalar yapılır.1. de
anlatılan; fp1,int değişkenli işleve göstergeç olup 10 void göstergeçler
dizisi göstergecini geri döndürür. 2.de anlatılan;fp2, üç değişkenli
(int,int,float) işleve göstergeçtir ve int değişkeni olan işlevin göstergeçini
geri döndürür ve sonunda float geri döner. Karmaşık yapılar oluştururken
typedef te kullanılabilir. Şimdi 3.de anlatılanı inceleyelim; typedef her
seferinde oluşturulan tipi saklar.
fp3, değişkeni olmayan işleve göstergeç olup ve 10 göstergeçli dizinin
değişkeni olmayan işleve göstergeçini geri döndürür ve en sonunda double
geri döndürür. Daha sonra da a, fp3 tiplerinden biridir. typedef genellikle
basit yapılardan karmaşık tanımlar oluştururken kullanılır. 4 teki ise
değişken tanımından çok, işlev bildirimidir.f4, 10 göstergeçli diziye
göstergeç geri döndüren işlev olup, en sonunda 10 göstergeçli dizinin
gösterdiği 10 işlevi int olarak geri döndürür.
Yukarıda gösterilen karmaşık biçimli tanımlamalar nadirende olsa
kullanılır. Gerçek hayatta kullanılan benzer tanımlar çok daha az karmaşık
ve anlaşılabilir olur.
İşlevle göstergeç kullanımı
Bir işlev göstergeçi tanımlandığı zaman, kullanmadan önce o göstergeçe
mutlaka adres ataması yapılmalıdır.
Aynı dizi adres belirtilmesinde olduğu gibi; dizi[100] yazıldığında kare
ayraçlar olmasa da yani, sadece dizi yazıldığı zaman dizinin adresi üretilir.
İşlev adresi de sadece işlev adı ile üretilir, yani değişken listesi gerekmez.
func( ) de adresi sadece func üretir. Bunun yanında açık adreslemede
seçebilirsiniz; &func( ). İşlev çağırma ilgili işlevi
referansından kurtararak yapabilirsiniz. Aşağıdaki örnek işlev adreslemeyi
göstermektedir.
//:B03:PointerToFunction.cpp
//İşlevlerde göstergeç tanımlama ve kullanma
#include <iostream>
using namespace std;
void func( ){
cout<<"func( ) çağırılıyor...."<<endl;
int main( ){
void (*fp)( ); //işlev göstergeci tanımlaması
fp=func ;
//fp ye değer ataması
(*fp)( ) ;
//referanstan kurtararak işlev çağırma
void (*fp2)( )=func ; //tanımlama ve atama
(*fp2)( );
}///:~
İşlevin fp göstergeçi tanımlandıktan sonra, işlevin(func( ) ) adres değeri
atanır (fp=func, işlevin değişken listesinin kullanılmadığına dikkat!).
İkinci gösterim de hem tanımlama hem de atama aynı anda
gerçeklenmiştir.
İşlev göstergeç dizisi
En ilginç yapılardan birisi, işlev göstergeçlerinden oluşan dizilerdir. Bir
işlev seçilecek ve siz onu dizi de sıraya koyacaksınız, sonra da çağırırken
referansından kurtaracaksınız. Bu düzenleme tablo-kaynaklı kod fikrini
desteklemektedir; koşul ya da durum deyimleri yerine işlev seçimleri,
durum değişkenlerine (ya da durum değişkenlerinin bileşimine) bağlı
olarak yerine getirilir. Bu tarz bir tasarım, çok sayıda işlevi sıklıkla tabloya
ekleyip
veya çıkarıyorsanız yararlı olur. (oluşturmak istediğiniz de ya da böyle bir
tabloyu dinamik olarak değiştirmek istediğinizde).
Aşağıdaki örnek, önişlemleyici makrosu kullanarak yardımcı işlevler
oluşturmaktadır, daha sonra da bu işlevlere
otomatik toptan atama ile göstergeçler dizisi oluşturulur. Göreceğiniz gibi
bu yapılarda çok az kod değiştirerek tabloda işlev yerleşimi ve çıkarılması
sağlanır. (böylece programın işlevselliği artar)
//:B03:FunctionTable.cpp
//işlevler için göstergeç dizisi kullanma
#include<iostream>
using namespace std;
//yardımcı işlev tanımlayan macro
#define DF(N) void N( ){\
cout<<"işlev..."#N"adlandırılır"<<endl;}
DF(a);DF(b);DF(c);DF(d);DF(e);DF(f);DF(g);
void (*func_table[])( )={a,b,c,d,e,f,g};
int main( ){
while(1){
cout<<"a-g arasında bir tuşa basın" " ya da q ya çıkmak için
basın"<<endl;
char c,cr;
cin.get(c);cin.get(cr); //ikinci CR değer
if(c=='q')
break; //while dışına çıkılır
if(c<'a'||c>'g')
continue;
(*func_table[c-'a'])( );
}
}///:~
Bu tür programlama yapısı ile nasıl yorumlayıcı yazılabileceği veya liste
işlemleme yapılabileceğini düşünün bakalım!.
Make: ayrık derleme yönetimi
Ayrık derleme yapılırken (kodu çevrilebilir birimlere ayırma), her dosyayı
otomatik derleyecek ve ilişimcisi
ilgili parçaları birleştirerek çalıştırılabilir dosya haline getirecek, aşamaları
belirten yöntemlere ihtiyaç vardır. Bu aşamalara inşa süreci adı verilir. İnşa
sürecinde, uygun kütüphane ve başlatma kodu beraber kullanılır.
Derleyicilerin çoğunluğu tek satırlık deyimle bunu yapabilir. Örnek olarak
GNU C++ derleyicisinde ;
g++ kaynakDosya1.cpp kaynakDosya2.cpp
Yaklaşımla ilgili tek sorun, derleyicinin, dosyaları önce derlemesidir.
Yeniden inşa edilme gereksinimi olup olmadığına derleyici bakmaz. İçinde
birçok dosyanın bulunduğu bir proje de, tek bir dosya
değiştirildiğinde,dosyaların tamamının derlenmesi yasaklanmış olabilir. Bu
sorunun çözümü, UNIX te geliştirilmiş olup, bir şekil de öbür işletim
sistemlerinde de bulunur; make. Bu yöntem de, makefile denilen metin
dosyasında belirtilen komutlara göre bir projede ki dosyalar yönetilir.
Böylece make yöntemi uygulanmış olur. Bir projedeki birkaç dosyayı
yazıp, arkasından make yazdığınız zaman, make programı makefile daki
yolaçıcıları kullanarak, kaynak kod dosyalarındaki tarihlerle, onlara
karşılık gelen hedef dosyalardaki tarihleri karşılaştırır, ve eğer kaynak kod
dosyalarının tarihi hedef dosyalarının tarihlerinden daha yeni ise,
derleyiciyi kaynak kod dosyaları için işleme çağırır. make sadece kaynak
kod dosyaları değişikliğe uğramışlarsa ve değişen dosyalardan etkilenen
dosyalar varsa yeniden derleme yapar. Böylece make kullanıldığında,
değişiklik yapılan her seferde, projede bütün dosyaların yeniden
derlenmesine ve ayrıca inşa işleminin doğruluğunu denetlemeye gerek
kalmaz. makefile projeyi biraraya getirecek bütün komutları içerir. make
kullanımını öğrenmek; programcıya hem zaman tasarrufu sağlar, hem de
hayal kırıklıklarından korur. Linux/UNIX makinesine yeni bir yazılım
yüklemek make ile gerçeklenir. Gerçi bu makinelerdeki makefile
dosyaları, kitabımızdakilere göre daha karmaşıktırlar, ve ayrıca bu
dosyalar kendi bünyesinde otomatik olarak üretilirler.
Kitabımız boyunca kullanılacak araçlardan olan make, bütün C++
derleyicilerinde bir biçimde sanal olarak bulunur.
(eğer yoksa, bedava olarak dağıtılan derleyicilerde bulunanları
kullanabilirsiniz.). Bunun yanında, derleyici üreticileri
kendi proje inşa araçlarını geliştirmiş olabilirler. Bu araçlar makefile
benzeri, proje dosyası adı verilen unsurları kullanırlar.Geliştirme ortamı bu
dosyayı bütün süreç boyunca korur ve onunla ilgili bir sorun çıkmasını
engeller.
Proje dosyaları bir geliştirme ortamından ötekine değişiklik gösterirler. Bu
dosyaların oluşumu ve kullanımı ile ilgili bilgileri her geliştirme ortamı
için ayrı yerlerden sağlanır. (genellikle derleyici üreticileri tarafından
sağlanan bu bilgileri öğrenmek oldukça kolaydır.)
Burada anlatılacak olan makefile dosyaları, değişik derleyiciler kullanıyor
olsanız bile, size temel değişmez bilgileri sunacaktır.
Make etkinlikleri
make yazdığınız zaman make programı, bulunduğu dizinde, makefile
proje dosyasına bakar. Bu dosya, kaynak kod dosyalarının arasındaki
bağımlılıkları sıralar, make, dosyalardaki tarihlere bakar. Bir bağımlı
dosyadaki tarih, bağımlı olduğu dosyadaki tarihten daha eski ise, make
bağımlılık sonrası kuralı çalıştırır.
makefile larda bütün yorumlar # işareti ile başlar ve satır sonuna kadar
sürer.
makefile dosyalarının genel yapısı aşağıda verilen kurgudadır;
HEDEF : ÖNKOŞULLAR
KOMUTLAR
HEDEF varılmak istenen dosya tipi olup genellikle programın
çalıştırılabilir halidir. ÖNKOŞULLAR ise derleyici ve ilişimcinin hedef
dosyayı oluşturmak için kullanacağı bütün (.h ve cpp uzantılı) dosyaların
adlarıdır. KOMUTLAR ise önkoşul dosyaları hedef dosyaya çevirebilmek
için gereken komutlardır.
Kolay bir örnek olarak "hello" programı için makefile dosyası ;
#Bir yorum
hello.exe:hello.cpp
mycompiler hello.cpp
Yukarıda hello.exe (hedef), hello.cpp ye bağımlıdır.hello.cpp, hello.exe
den daha yeni tarihe sahipse make,
mycompiler hello.cpp kuralını çalıştırır.make programlarının
çoğunluğunda, kurallar çıkıntılarla başlatılırlar.
Diğerlerinde ise boşluklar genellikle gözardı edilirler ve okunabilirlik için
yeniden düzenlenebilirler.
Kurallar sadece derleyiciye çağırmayla sınırlı değildir; make içinde
görmek istediğini herhangi bir programıda çağırabilirisniz. Aralarında
bağımlı, bağımlılık kurallar öbeğini oluşturarak kaynak kod dosyalarını
değiştirebilirsiniz.
make yazarak, etkilenen bütün dosyaların, doğru şekilde yeniden inşa
edileceğinden emin olabilirsiniz.
Makrolar
Bir makefile dosyasında makrolar da bulunabilir.Yalnız bu makrolar
önişlemleyici makrolarından tamamen farklıdır.
Bunların görevi uygun kelime (string) yerleşimine izin vermektir. Bu
kitapta ki, makefile'larda kullanılan derleyiciyi yardıma çağıran bir makro
aşağıda verilmiştir;
CPP=mycompiler
hello.exe:helllo.cpp
$(CPP) hello.cpp
Buradaki = işareti CPP yi makro olarak bildirir. $ ve ayraç işaretleri ise
makroyu açar. Açma ile kastedilen;
$(CPP) ile mycompiler kelimesinin yerdeğiştirmesidir. Yine yukarıdaki
makro da kullandığınız derleyiciyi cpp derleyicisi ile değiştirmek
isterseniz;
CPP=cpp
yazarak makroyu değiştirmek yeterlidir. Derleyici bayraklarıda (v.d)
makroya eklenebilir ya da ayrı makrolar kullanarak derleyici bayrakları
ilave edilebilir.
Uzantı kuralları
Projede bulunan her cpp dosyası için, make'e derleyiciyi göreve nasıl
çağıracağını yazmak, oldukça meşakkatli bir iştir. Hele her seferinde, aynı
basit süreç işlemlenecekse. Zaman tasarrufu sağlamak için dosya
isimlerinin uzantıları kullanıldığı gibi, ayrıca eylemleride kısaltan yollar
make içinde bulunur. Bu kısaltmalar "uzantı kuralları" olarak bilinir. Bir
uzantı kuralı, make'e uzantısı belli (örnek .cpp) bir dosya tipinin, uzantısı
belli başka (örnek .obj ya da .exe) bir dosya tipine nasıl çevrileceğini
öğretir. Bir kez make'e bir dosya tipinin başka bir dosya tipine nasıl
çevrileceğinin kurallarını öğrettiğinizde, yapacağınızın hepsi; make'e
hangi dosyaların, hangi dosyalara bağımlı olduğunu söylemektir. Bağımlı
olduğu dosyadan daha yeni tarihe sahip bir dosya, make tarafından
bulunduğu zaman, yeni bir dosya oluşturma kuralları kullanılır.
Uzantı kuralı make'e programın inşası sırasında ayrıntının tamamının
bildirimine gerek olmadığını söyler. Fakat onun yerine, dosya uzantıları
temelinde inşaatın hesaplamalarını yapar. Bu durumda söylediği;".exe
uzantılı bir dosyayı .cpp uzantılı bir dosyadan inşa etmek için aşağıdaki
komutları yardıma çağır" dır. Bu örnek aşağıdaki gibi olur;
CPP=mycompiler
.SUFFIXES: .exe .cpp
.cpp.exe:
$(CPP) $<
.Suffixes yönergesi makee uzantıları, bu özel makefile için önem arzeden
dosyaları gösterir. Daha sonra .cpp .exe uzantı kuralını yani; "işte .cpp
uzantılı dosyanın .exe uzantılı dosyaya nasıl çevrileceği " ni görürsünüz.
(.cpp dosyasının .exe dosyasından çok daha yeni tarihli olduğu durumlarda
tabii). Daha önce $(CPP) macrosu kullanılmıştı. Fakat burada yeni bir
yapıya rastlıyoruz; $<. Bu da $ işareti ile başladığı için bir macrodur.
Fakat bu makein yerleşik macrosudur. Ve bu macro sadece uzantı
kurallarında kullanılır ve "uzantı kuralını tetikleyen herhangi önkoşul"
anlamını taşır.(bazen de bağımlı anlamında). Ya da "derlenmek için
gereksinim duyulan .cpp dosyasına" tercüme edilebilir.
Uzantı kuralları bir kez ayarlandığı zaman, basit bir şekilde "make
Union.exe" söyleyebilirsiniz, makefile dosyasında
"Union" olmasa bile uzantı kuralı işlemeye başlar.
varsayılan hedefler
Uzantı kurallarından ve macrolardan sonra,make önce dosyada "hedef"
arar, eğer farklı tanımlamadıysanız. Böylece aşağıdaki makefile dosyası;
CPP=mycompiler
.SUFFIXES: .cpp .exe
.cpp.exe:
$(CPP) $<
target1.exe:
target2.exe:
"make" yazdığınız zaman target1.exe inşa edilir (varsayılan uzantı
kuralına göre), zira ilk rastlanılan hedef target1.exe dir. target2.exe yi
inşa etmek için ise, açık olarak make target2.exe yazmak zorundasınız.
Bu oldukça yorucu bir işlemdir(bir sürü hedef olduğu zaman). Bu nedenle
geri kalan bütün hedefler için "yardımcı hedef"
oluşturulur. Aşağıdaki gibi;
CPP=mycompiler
.SUFFIXES: .cpp .exe
.cpp.exe:
$(CPP) $<
all: target1.exe target2.exe
Burada "herşey"(all) yoktur, "all" adını taşıyan bir dosya yoktur, make
yazılan her seferde, program "all"u listedeki ilk
hedef olarak görür (dolayısı ile varsayılan hedef), daha sonra görülür ki
"all" yoktur, bağımlılıkların tamamını denetleyerek make yapmak daha
iyidir. Böylece "target1.exe" ye bakılır ve (uzantı kuralı kullanılır)
target1.exe (1)
varmı yokmu bakar ve (2) target1.cpp, target1.exe den daha yeni
tarihlimi bakılır, eğer öyle ise uzantı kuralı çalıştırılır. (eğer özel bir hedef
için başka kurallar tanımladıysanız, uzantı kurallarının yerine onlar
kullanılır)
Daha sonra varsayılan hedef listesindeki öteki dosyaya geçilir. Böylece
varsayılan hedef listesi oluşturarak (genel
olarak "all" adı verilir, fakat dilerseniz başka isimde verebilirsiniz.)
projenizdeki bütün çalıştırılabilir dosyaları sadece "make" yazarak
meydana getirirsiniz. İlave olarak, başka görevleri yerine getiren
varsayılan olmayan başka hedef listeleri olabilir, örnek olarak; bu listeyi
öyle ayarlarsınız ki ,"make debug " yazarak bütün dosyalarınızı, hatalarını
ayıklayarak yeniden inşa ettirebilirsiniz.
örnek makefile dosyası
CPP=g++
OFLAG= -o
.SUFFIXES: .o .cpp .c
.cpp.o :
$(CPP) $(CPPFLAGS) -c $<
.c.o :
$(CPP) $(CPPFLAGS) -c $<
all: \
Return \
Declare \
Ifthen \
Guess \
Guess2
#Bu bölüm için geri kalan dosyalar gösterilmiyor.
Return : Return.o
$(CPP) $(OFLAG)Return Return.o
Declare : Declare.o
$(CPP) $(OFLAG)Declare Declare.o
Ifthen : Ifthen.o
$(CPP) $(OFLAG)Ifthen Ifthen.o
Guess : Guess.o
$(CPP) $(OFLAG)Guess Guess.o
Guess2 : Guess2.o
$(CPP) $(OFLAG)Guess2 Guess2.o
Return.o: Return.cpp
Declare.o: Declare.cpp
Ifthen.o: Ifthen.cpp
Guess.o: Guess.cpp
Guess2.o: Guess2.cpp
CPP macrosu derleyicinin adına ayarlanır. Farklı bir derleyici kullanmak
için, ya makefile yazarsınız ya da komut satırından macronun değerini
değiştirirsiniz. Aşağıdaki gibi:
make CPP=cpp
İkinci macro olan OFLAG çıktı dosyalarını göstermek için kullanılır.
Birçok derleyici çıktı dosyaları için girdi dosyalarının temel isimlerini
aynen kabul etmelerine rağmen bazıları kendi özel isimlerini kullanır.
(Linux/UNIX
a.out gibi)
İki adet uzantı kuralını yukarıda görmektesiniz; bunlardan birincisi .cpp
dosyaları için ikincisi ise .c dosyaları içindir.
(C kodlarının derlenme gereksinimi durumlarında.). Varsayılan hedef 'all'
dur ve bu hedefin bütün satırları ters bölü işareti ile devam ettirilir. Ters
bölü işareti listedeki son öge Guess2 ye kadar kullanılır. Bu bölümde daha
birçok dosya olmasına rağmen, kısaltma uğruna bu kadar gösterilmiştir.
Uzantı kuralları, .cpp dosyalarından hedef dosyalar (.o uzantılı dosyalar )
oluşturmayı gözönünde bulundurur. Fakat çalıştırılabilir (executable)
dosya oluşturabilmek için, uzantı kurallarını açık biçimde belirtmek
gerekir. Bunun sebebi bir çalıştırılabilir dosya, birden fazla hedef dosyanın
(.o uzantılı dosyalar) ilişimlenmesi sonucu oluştuğundan, make
yapılması gerekenleri tahmin edemez. Linux/UNIX te çalıştırılabilir
dosyalar için standart bir uzantı bulunmadığından,
uzantı kuralı, böyle basit durumlar için çalışmaz. Böylece son
çalıştırılabilir yapı için bütün kuralları açıkça
göreceksiniz.
makefile dosyası mutlak en güvenli yolu mümkün birkaç make özelliğini
kullanarak sağlar; sadece macrolar olarak hedef ve bağımlılıkları kullanır.
Bu usul sanal olarak mümkün olan en çok sayıda make programı ile
çalışmayı sağlar.
Tabii ki bu daha büyük boyutlu makefile dosyası oluşmasına yol açar.
Kitabımız da bahsedilmeyen daha çok sayıda make özelliği
bulunmaktadır. make, daha kısa ve daha zekice geliştirilmiş
sürüm ve çeşitlerle daha çok zaman tasarrufu sağlar. Kendi kullandığınız
geliştirme ortamının make belgelerini inceleyerek bu konu ile ilgili size
özgü daha ileri bilgiler edinebilirsiniz. make ile proje yönetimi üzerine bir
çok ayrıntılı kitapta bulunmaktadır. İnterneti kullanarak bunlara
erişebilirsiniz. Elinizde make ile ilgili hiçbirşey yok ise
GNU arşivlerinden elde edebilirsiniz.
ÖZET
Bu bölüm, C++ temel sözdizim kuralları üzerine oldukça yoğun bir
çalışmaydı. Bunların büyük bir çoğunluğu da C dilinden aktarılmıştı
(Böylece C++ nın C ile uyumluluğunun övünç kaynağı oluyordu). C++ nın
birçok özelliği ilk kez tanıtılmış olmasına rağmen, aslında burada tutucu
programcılar dikkate alınanarak, önce C dilinin C++ ya altyapı olmuş
yanları incelendi. Eğer siz bir C programcısıysanız, burada anlatılanların
bazıları tanıdık gelmiyebilir, bunları öğrenmek, sizin için de çok yararlı
olmuştur.
--------------------------------------------------------------------------------------------------
Önişlemleyici yönergeleri (derleme koşullarını
belirlemek için kullanılır)
#define
#undef
#include
#ifdef
#ifndef
derler
#endif
#if
#else
belirler
#elif
#line
#error
#pragma
------------- makro tanımlar
------------ makro tanımlamaz
------------- dosya içeriğini metin olarak ekler
------------- makro'daki tanım koşuluna bağlı olarak kodu derler
------------- makro'daki olmayan tanım koşuluna göre kodu
------------- koşullu derleme bloğunun sonunu belirler
------------- açıklamanın sıfır olmadığı koşulda kodu derler
------------- #ifndef, #ifdef ve #if yönergelerinin else kısmını
------------- #else ve #if yönergelerinin birleşimi
------------- o anki satır numarasını ve dosya adını değiştirme
------------- hata iletisi üretir
------------- uygulamaya özgüdür
-------------------------------------------------------------1- For döngüsü ile aşağı sayım
//ORNEK
#include <iostream>
using namespace std ;
int main( ){
for(int i=10; i>0; i--){
cout<<i<<"," ;
}
cout<<"ATES!"<<endl ;
}
--------------------------------------------------------------2- break döngü örneği
//ORNEK
#include <iostream>
using namespace std ;
int main( ){
int i;
for(i=10; i>0; i--){
cout<<i<<", " ;
if(i==5){
cout<<"asagi sayimdan cikis"<<endl ;
break ;
}
}
}
----------------------------------------------------------------3- continue döngü örneği
//ORNEK
#include <iostream>
using namespace std ;
int main( ){
for(int i=10; i>0; i--){
if(i==6)continue
cout<<i<<", " ;
}
cout<<"ATES!"<<endl ;
}
-----------------------------------------------------------------4- işlev örneği
//ORNEK
#include <iostream>
using namespace std ;
int toplama(int a, int b){
int toplam ;
toplam=a+b ;
return (toplam) ;
}
int main( ){
int sonuc ;
sonuc=toplama(5, 6) ;
cout<<"netice= "<<sonuc<<endl ;
}
----------------------------------------------------------------5- ikinci işlev örneği
#include <iostream>
using namespace std ;
int subtraction (int a, int b){
int r;
r=a-b;
return (r);
}
int main (){
int x=5, y=3, z;
z = subtraction (7,2);
cout << "ilk netice= "<< z << '\n';
cout << "ikinci netice= "<< subtraction (7,2) << endl;
cout << "ucuncu netice= "<< subtraction (x,y) << endl;
z= 4 + subtraction (x,y);
cout << "dorduncu netice= " << z << endl;
}
-------------------------------------------------------------------6- ögeleri dayançla aktarım
//ORNEK
#include <iostream>
using namespace std ;
void duplicate (int& a, int& b, int& c){
a*=2;
b*=2;
c*=2;
}
int main (){
int x=1, y=3, z=7;
duplicate (x, y, z);
cout << "x=" << x << ", y=" << y << ", z=" << z<<endl ;
}
--------------------------------------------------7-işlevlerde varsayılan değerler
//ORNEK
#include <iostream>
using namespace std ;
int divide (int a, int b=2){
int r;
r=a/b;
return (r);
}
int main (){
cout << divide (12)<<endl;
cout << divide (20,4)<<endl;
}
--------------------------------------------------------------------------8- bindirilmiş işlev (aynı isim farklı işlevler için kullanılır)
//ORNEK
#include <iostream>
using namespace std ;
int divide (int a, int b){
return (a/b);
}
float divide (float a, float b){
return (a/b);
}
int main (){
int x=5,y=2;
float n=5.0,m=2.0;
cout << divide (x,y);
cout << "\n"<<endl ;
cout << divide (n,m);
cout << "\n"<<endl;
}
-----------------------------------------------------------9- özyinelemeli çağrı ile faktöriyel bulma
//ORNEK
#include <iostream>
using namespace std ;
long faktoryal (long a){
if (a > 1)
return (a * faktoryal (a-1));
else
return (1);
}
int main (){
long sayi;
cout << "bir sayi girin: ";
cin >> sayi;
cout << "!" << sayi << " = " << faktoryal (sayi)<<endl;
}
------------------------------------------------------------------10-dizi örneği
//ORNEK
#include <iostream>
using namespace std ;
int selo [] = {36, 12, 71, 40, 201};
int n, result=0;
int main (){
for ( n=0 ; n<5 ; n++ ){
result += selo[n];
}
cout << result<<endl;
}
--------------------------------------------------------------------11- matris (iki boyutlu dizi) örneği
//ORNEK
#include <iostream>
#define WIDTH 5
#define HEIGHT 3
using namespace std ;
int selo [HEIGHT][WIDTH];
int n,m;
int main (){
for (n=0; n<HEIGHT; n++)
for (m=0; m<WIDTH; m++){
selo[n][m]=(n+1)*(m+1);
}
return 0;
}
-----------------------------------------------------------------------12- öge olarak diziler
//ORNEK
#include <iostream>
using namespace std ;
void printarray (int arg[], int length) {
for (int n=0; n<length; n++)
cout << arg[n] << " ";
cout << "\n";
}
int main (){
int firstarray[] = {5, 10, 15};
int secondarray[] = {2, 4, 6, 8, 10};
printarray (firstarray,3);
printarray (secondarray,5);
return 0;
}
------------------------------------------------------------------------
Göstergeçlerin kullanımı üzerine özel notlar;
C++ dili göstergeçleri birçok alanda kullanır. Kullanırken çok dikkatli
olmalıdır.
Göstergeçlerin yanlış yerlerde kullanılması bir çok soruna neden olur;
1- Göstergeçler işaret ettikleri bellek yerini boşaltmadan görüntü
alanından
(çalışma alanından) çıkarlarsa, bellek artık erişilemez olur. (bellek
kaçağı olur)
2- Programcılar her new komutu için karşı gelen bir delete uygulasalar
bile, delete'e
varmadan evvel bir istisna ortaya çıkarsa, bellek kaçağı oluşur.
3- Göstergeçler ilk değer ataması yapılmadan önce kullanılırsa, sistem
belleği çöker.
(ilk değeri atanmamış göstergeçler)
4- Göstergeçlerin işaret ettikleri bellek boşaltılırken kullanılırsa,
böylece sistem
tarafından yeniden kullanılan bellek çökebilir. (asılı sallanan
göstergeç)
5- Aynı bellek yerini işaret eden birden fazla göstergeç burayı
boşaltırsa, bellek
kümesi tamir edilemez şekilde çöker. (çoklu delete)
Evet göstergeçler programlarda inanılmaz derecede yararlı işlerde
kullanılmalarına rağmen
yukarıdaki kullanımları hatalara yolaçarlar. Bunun nedeni
göstergeçlerle ilgili dilin içinde
güvenlik araçlarının bulunmamasıdır. Sorunlardan kurtulmanın bir
yolu göstergeçleri
sınıflarda yer alan işlevlerin içinde kullanmaktır. Artık göstergeçler
akıllı "cingöz" göstergeç
olur, bu konunun ayrıntıları ilerleyen bölümlerde anlatılacak.
4. BÖLÜM
Veri Soyutlama
C++ program üretimini arttıran araçlardan biridir. Zaten amacımız da
üretimi arttırmaktır. C++ ya geçişte doğan zorluklar, bu nedenle gözardı
edilir.
Bildiğiniz bir programlama dilinden başka bir dile geçip onu kullanma
nedeni, gelecekte yeni dille daha verimli olunacağı içindir. Aslında yeni bir
dil öğrenip kullanılmaya başlandığın da, ilk zamanlar, bildiğiniz önceki
dile göre, verim daha azdır. Daha sonra yeni dilin huyu anlaşıldığın da bu
dil daha yararlı olur.
Bilgisayar terminolojisinde üretkenliğin anlamı; daha az kişi ile daha
karmaşık ve etkin programları, daha kısa sürede
yazabilmektir. Dil seçerken mutlaka başka etkenler de vardır. Bunların
arasında verimlilik (özellikler yavaşlamaya ve kod şişmelerine sebep
oluyormu? ), güvenlik (her zaman programınız da planladıklarınızı yerine
getirebiliyormu ve hataları düzgün yönetebiliyormu? ), ve bakım
(anlamanızı, değiştirmenizi, ve geliştirmenizi kolaylaştıran kodlar
oluşturmaya yardım ediyormu? ) sayılabilir. Bunları da yakından
irdeleyeceğiz.
Üretkenlik için kabaca; üç kişinin bir haftada geliştirdiği programı bir
kişinin üç günde geliştirmesi denebilir. Bu da değişik açılardan ekonomiyi
ilgilendirir. Bazı şeyleri inşa etmekten kaynaklanan mutluluk duymanın
dışında, elemanınız (veya patronunuz) daha az kişi ile daha çabuk program
üretilmesinden mutludur, ve müşteri daha ucuza aldığı programdan dolayı
mutludur. Üretkenliği büyük çapta arttırmanın yolu ise, başkalarının
hazırladığı kodları yani kütüphaneleri kullanmaktır. (Kütüphane .o uzantılı
kodun yeniden kullanımını sağlar. STL ise kaynak kodun.)
Kütüphane, başkaları tarafından yazılıp paketlenmiş kod topluluğudur.
Çoğunlukla en küçük paket, .lib uzantılı bir dosyadır. Kullanılacak
kütüphane, derleyiciye bir veya daha fazla başlık dosya adı ile bildirilir.
İlişimci kütüphane dosyasını nasıl arayacağını bilir ve derlenmiş kodu
uygun şekilde üretir. Ama kütüphaneyi dizgenize eklemenin tek
yolu bulunur. Linux/Unix benzeri işletim sistemlerinde, değişik mimariler
için kütüphaneler kaynak kod ile gelir. Bu sayede kullanılan işlemcinin
mimarisine uygun, düzenleme ve derleme yeniden yapılabilir.
Söyledik, verimi arttırmanın en önemli yolu kütüphanelerdir, ve C++
dilinin tasarım amaçlarından biri de kütüphane kullanımını
kolaylaştırmaktır. Son cümle, C dilinin kütüphane kullanımına zorluklar
çıkardığı anlamını taşıyor. Bunu kavramak, sizin C++ için ilk görüşünüz
olacak, böylece nasıl kullanılacağına nüfuz edeceksiniz. Farklı gruplar
tarafından geliştirilmiş C++ dilini çok paradigmalı hale getiren değişik
kütüphaneler de bulunmaktadır. Örneğin; FC++ kütüphanesi C++ diline
işlevik dil özellikleri kazandırır. LC++ kütüphanesi ise C++ yı mantık dili
haline sokar. Bu özellikler C++ dilini inanılmaz güçlü yapar. Ayrıca Loki
kütüphanesi de tasarım düzenekleri ile modern C++ diline önemli
katkılarda bulunmaktadır. Bunlardan başka çok sayıda özel amaçlara
hizmet eden kütüphaneler de vardır. Örnek olarak derleyiciyi gerçek
zamanlı programlar üretebilir hale getiren özel kütüphaneler gibi. İlerde
isterseniz siz de kendi özel koşullarınıza uygun kütüphaneler
geliştirebilirsiniz.
C benzeri küçük bir kütüphane
Bir kütüphane genellikle çok sayıda işlevle başlar. Ama eğer başkalarının
C kütüphanesini kullanmaktaysanız, genellikle işlevlerden başka unsurlar
da bulunur. Bunlar; davranış, eylem ve işlevlerden daha fazla yaşayacak
etkinliklerdir. Ayrıca veriler tarafından temsil edilen nitelikler de olabilir
(mavi, kilo, aydınlık, vs.). C de sorun ortamındaki birden fazla benzer
unsur, struct'ta biraraya getirilerek temsil edilir. Daha sonra her unsur için
struct'tan değişken (nesne değil, zira bu C dili) üretilebilir. Niteliklerle işe
başlanırken bunlar bilinmelidir.
C kütüphanelerinin çoğunda, struct'lar ve bu struct'larda etkin, işlevler
kümesi bulunur. Şimdi böyle bir sistemin nasıl görüneceğine dair bir örnek
olarak; dizi gibi davranan bir program parçasını düşünelim, ama büyüklük
oluşturulurken, yani çalışma anında belirlensin. Adı CStash olsun. C++ da
yazılmasına rağmen, C deki tarz ile yazım isteniyor.
//:B04:CLib.h
//C benzeri kütüphane için başlık dosyası
//dizi benzeri, adlandırma çalışırken olur
typedef struct CStashTag{
int size;
//bellek büyüklüğü
int quantity;
//bellek sayısı
int next;
//bir sonraki boş bellek yeri
unsigned char* storage; //dinamik olarak yerleştirilen bayt dizisi
}CStash;
void initialize(CStash* s, int size);
void cleanup(CStash* s);
int add(CStash* s, const void* element);
void* fetch(CStash* s, int index);
int count(CStash* s);
void inflate(CStash* s, int increase);
///:~
CStashTag benzeri ek adlar, struct'ın kendini struct'ın içinden dayanak
alma gerektiğinde kullanılır.
Örnek olarak; ilişimlenmiş bir liste oluşturulurken (listedeki her ögede, bir
sonraki ögeyi işaret eden bir göstergeç vardır) bir sonraki struct
değişkenini işaret eden göstergeç gerekir. Bundan dolayı struct gövdesi
içinde göstergecin tipini belirtmek gerekir. Ayrıca genel olarak C
kütüphanesinde, her struct için yukarıdaki gibi typedef'i görürsünüz.
Böyle yapılarak, struct sanki yeni bir tipmiş gibi işlemlenir. Bu struct'ın
değişkenleri aşağıdaki gibi tanımlanır; (dikkat edin C dilinde hep değişken
diyoruz, nesne değil! C++ da bunlar nesnedir)
CStash A, B, C ;
storage göstergeçi unsigned char tipindedir. unsigned char, C
derleyicisinin desteklediği en az bellek kaplayan
tiptir. Gerçi bazı makinelerde en büyük bellek alanı ve en küçük bellek
alanı eşitlenmiştir, bunu yeri gelmişken belirtelim. Uygulamaya bağlı
olmasına rağmen genellikle 1 bayt (=8 bit) uzunluğundadır. CStash her
tipte değişken tutacak şekilde tasarlandı. Burada en uygun void* tir.
Amacımız, bilinmeyen tipte bellek bloğu olarak işlemlemek değil, ardışık
baytlar bloğu olarak işlemlemektir. Uygulama kaynak kodu (para verip
alabilirsiniz-derlenmiş .lib veya .obj veya .dll vs olarak) aşağıda
verilmiştir;
//:B04:CLib.cpp {o}
//C benzeri kütüphanenin uygulaması
//yapı ve işlevlerin bildirimi
#include "CLib.h"
#include <iostream>
#include <cassert>
using namespace std ;
//eklenecek ögelerin sayısı
//bellek artarken
cons int increment ;
void initialize(CStash* s, int sz){
s->size=sz ;
s->storage=0 ;
s->quantity=0 ;
s->next=0 ;
}
int add(CStash* s,const void* element){
if(s->next >= s->quantity) //yeterli yer kaldımı?
inflate(s,increment) ;
//bir sonraki yerden başlayarak
//ögeleri belleğe kopyala
int startBytes=s->next * s->size ;
unsigned char* e=(unsigned char*)element ;
for(int i=0;i<s->size;i++)
s->storage[startBytes+i]=e[i];
s->next++;
return(s->next-1); //dizin sayısı
}
void* fetch(CStash* s,int index){
//dizin sınırlarını denetleme
assert(0<=index);
if(index>=s->next)
return 0; //bitti
//arzu edilen ögeyi gösterin
return &(s->storage[index* s->size]);
}
int count(CStash* s){
return s->next ; //CStash teki ögeler
}
void inflate(CStash* s,int increase){
assert(increse>0) ;
int newQuantity=s->quantity+increase ;
int newBytes=newQuantity* s->size ;
int oldBytes=s->quantity* s->size ;
unsigned char* b=new unsigned char [newBytes] ;
for(int i=0;i<oldBytes;i++)
b[i]=s->storage[i] ; //eskiyi yeniye kopyala
delete [] (s->storage) ;// eski bellek
s->storage=b; //yeni yeri göster
s->quantity=newQuantity;
}
void cleanup(CStash* s){
if(s->storage!=0){
cout<<"bellek boşaltma"<<endl;
delete []s->storage ;
}
}///:~
initialize( ), struct CStash( ) teki iç değişkenlere uygun atamaları yaparak
kurulumu sağlar. Başlangıçta storage göstergeçi sıfırlanır, başlangıç bellek
yeri olmaz.
add( ) işlevi, CStash te bir sonraki yere bir öge yerleştirir. Önce boş yer
olup olmadığına bakar. Eğer yer yoksa inflate işlevini kullanarak yer açar.
İlerde bu işlevin görevi ayrıntılarıyla anlatılacak.
Saklanacak olan değişkenin özel tipi derleyici tarafından bilinmediği için
(bütün işlevin aldığı void* ) hemen atama yapamadığınızdan en uygun
yapılacak şey değişkeni bayt-bayt kopyalamaktır. Kopyalamanın en kolay
yolu diziyi dizinlemektir. Genel olarak storagede bulunan baytlar, next
değeri ile gösterilir. Doğru bayt kaymasını başlatmak amacıyla startByte'ı
üretmek için, next her ögenin büyüklüğü(bayt olarak) ile çarpılır. Daha
sonra element değişkeni unsigned char tipine döküm yapılır. Böylece
bayt bayt adreslenebilir ve var olan storage bellek alanına kopyalanır.
nextin değeri bir arttırılarak bellekteki bir sonraki yer işaret edilir. Bu
değerin önceden saklandığı "dizin sayısı",
fetch( ) işlevi kullanılarak (dizinin değeri) elde edilir.
fetch( ), dizinin sınırların dışında olup olmadığına bakar, ve istenen
değişkenin adresini, index değişkenini kullanarak
hesaplayıp bildirir. CStash'teki kaymalar için index ögelerin sayısını
gösterdiğinden, her parçanın kapladığı alanın bayt sayısıyla çarpılması
zorunludur. Böylece kayma sayısı bayt adedi ile belirlenir. Dizi sıralama
kullanarak bu kayma storage'i dizdiği zaman, adresi alamazsınız bunun
yerine, o adresteki baytı elde edersiniz. Adresi elde etmek için de
& işlecini kullanmalısınız.
count( ) ilk bakışta bir C programcısına biraz acayip görünür. Birçok
sorunun kaynağı gibi görünen bazı şeyleri elle
tek tek yapmak çok daha kolay olabilir. Örnek olarak, intStash adında bir
struct CStashiniz varsa, intStash.next ile kaç tane ögenin bulunduğunu
öğrenmek, count(&intStash) işlev çağrısı ile öğrenmeye göre daha
kolaydır. CStashin iç
gösterimlerini değiştirmek isteseydiniz, ve bu yolla adet hesaplansaydı,
işlev çağrı arayüzü gereken esnekliğe izin verirdi. Fakat maalesef
programcıların çoğu kütüphane için sizin tasarımınızı daha iyi bulma
konusunda canlarını sıkmazlar. struct'a bakarlar ve next değerini
doğrudan bulurlar ve yine olasıdır ki izniniz olmadan nexti değiştirirler.
Kütüphane tasarımcısı için eğer sadece buna benzer değiştirme yolları
olsaydı tabii!. (Evet,önceden bildiriliyor.)
Belleğe dinamik yerleşim
CStash için gereken en büyük bellek miktarını hiçbir zaman bilemezsiniz.
storage tarafından işaretlenen bellek, kümeden (heap) alınır. Küme büyük
bir bellek birimi olup, program çalışırken küçük parçaları saklamak için
kullanılır. Küme, program yazarken kullanılacak bellek miktarını
bilmediğinizde gerekir. Yani sadece, programın
çalışması sırasında; Uçan değişkenler için 20 yerine 200 yer gerekir.
Standart C de bulunan dinamik bellek yerleşim işlevleri şunlardır; malloc
( ), calloc( ), realloc( ) ve free( ) dir. C++, işlev çağrılarının yerine
dinamik bellek yerleşimini çok daha basit ve kolay yerine getirir. C++ da,
dile tümleşik olarak gelen new ve delete anahtar kelimeleri bu amaçla
kullanılır. Gördüğünüz üzere C deki bellek yerleşim ögelerinin tamamı
birer işlevdirler, ama C++ daki new ve delete işlev değil işleçtirler.
CStash için daha büyük bir bellek alanı inflate( ) işlevinde new anahtar
kelimesi ile sağlanır. Bu durumda bellek sadece genişletilir, üstüste
katlama yapılmaz. assert( ) işlevi, negatif bir sayının inflate( ) işlevine
aktarılarak, artış değeri olarak kullanılmasını engeller. Ögelerin yeni sayısı
newQuantity olarak hesaplanarak (inflate( ) işlevi tamamlandıktan sonra)
tutulur ve daha sonra bu, her ögeye düşen bayt sayısı ile çarpılarak
newBytes üretilir. newBytes bellekte yeralan bayt sayısını verir. Böylece
eski yerden alınarak kopyalanacak bayt sayısını biliriz. oldBytes, eski
quantity kullanılarak hesaplanır.
Gerçek bellek yerleşimi yeni-açıklama ile sağlanır. Bu ifadede new işleci
bulunur.
new unsigned char[newBytes] ;
new açıklamasının genel yazım şekli aşağıda verilmiştir;
new Type ;
Burada Type kümeye yerleştirilecek olan değişkenin tipidir. Örneğimizde,
newBytes uzunluğunda unsigned char tipinde dizi gösterilmiştir. Bunun
yanında sadece int ifadesiyle de new işleci kullanılabilir;
new int ;
Bu tür uygulamalara nadir olarak rastlansa da, tutarlı yapılardır.
Yeni bir açıklama istenilen belli bir tipteki nesneye göstergeç geri
döndürür. new Type dediğiniz zaman bir Type a
göstergeç elde edilir. new int dediğiniz de ise, bir inte işaret eden
göstergeç geri alınır. new unsigned char dizisi istediğiniz de ise anılan
dizinin ilk ögesini işaretleyen göstergeç geri alınır. Derleyici, yeniaçıklamanın geri dönüş değerini, doğru tipin göstergeçine atamanızı
güvenle sağlar.
Bellekte yer kalmadığı zaman bellek isteminde bulunursanız, bu isteğiniz
doğal olarak yerine getirilemez. Bellek
yerleşim işlemi başarısız olduğu zaman da C++ da uygulanacak
mekanizmalar bulunduğunu ilerde öğreneceksiniz.
Bellekte birkez yeni bir yer ayrıldığı zaman, eski yerdeki veri, mutlaka
yeni yere kopyalanmalıdır;bu yine dizi dizinleme ile, yani döngü içinde her
seferde bir bayt kopyalayarak başarılır. Veri yeni yere kopyalandıktan
sonra eski yer mutlaka boşaltılmalıdır. Bu boşaltılan yer, programda yeni
yere gereksinim duyan, öteki program parçaları tarafından kullanılır.
delete anahtar kelimesi, new anahtar kelimesinin tamamlayıcısıdır. delete
anahtar kelimesi new anahtar kelimesi tarafından oluşturulan bellek
yerlerini boşaltmada kullanılır (delete kullanımı unutulursa, ilgili bellek
yeri yeniden kullanılamaz ve bellek-kaçağı olarak bilinir). Buna ek olarak
bir diziyi delete ederken özel bir yazım şekli olduğunu da belirtmeliyiz. Bu
yazım şekli, sanki derleyiciye göstergeçin tek bir nesneyi değilde, nesneler
dizisini göstermek zorunluluğundan kaynaklanır; silinecek göstergeçin
önüne, içi boş köşeli ayraçlar konur.
delete []myArray ;
Eski bellek yeri silindiği zaman, yeni bellek yerininin göstergecine,
storage göstergeci atanır, miktar ayarlanır, ve
inflate( ) görevini tamamlar.
Küme yöneticisinin oldukça ilkel çalıştığını belirtelim. Yönetici, bir miktar
bellek bölümünü emrinize verir, ve siz onu delete ettiğiniz zaman geri alır.
Küme bütünlüğü için herhangi iç kolaylık yoktur. Daha büyük bellek
kullanımı için, sıkıştırılmış bellek yönetimi gibi kolaylıklardan
bahsediyoruz. Kümede saklamayı bir süre için uyguladığımızda
yani belleğe yerleştirip tekrar sildiğinizde, işlem parça parça olmuş büyük
miktarda serbest bellekle bitebilir,
ama parçaların büyüklükleri yerleştireceklerinize uygun boyutta
olmayabilir. Küme birleştirici, programları karmaşıklaştırır, bunun nedeni;
bellek bölümlerinin etrafında dolaşılmasıdır, böylece göstergeçleriniz
doğru değerleri
elde edemez. Bazı işletim dizgeleri yerleşik olarak küme birleşleştiricelere
sahiptirler, ve bunlar programcılara göstergeçlerin yerine, özel bellek
kullanım araçları sağlarlar. (bu araçlar geçici olarak göstergeçlere çevrilir,
bellek sabitlendikten sonra küme birleşleştirici onu başka yere
gönderemez.)
Derleme esnasında yığıtta değişken oluşturduğunuzda, o değişken
derleyici tarafından bellek yerine otomatik olarak yerleştirilir, ve sonrada
silinir . Derleyici gereken bellek miktarını, değişkenin ömrünü ve çalışma
alanını bilir. Dinamik bellek yerleşiminde ise, derleyici gereksinim
duyulan bellek miktarını ve ayrıca saklama süresini de bilemez.
Yani saklama yeri otomatik olarak silinemez. Bu nedenle delete ile, siz
silme işlemini yapmak zorundasınız. Bu sayede küme yöneticisi bellek
yerini yeni bir new çağrısında kullandırır. Kütüphanede bunun yerine
getirildiği yer
cleanup( ) işlevidir. Herşeyi korumak için kapatma işlemi bu işlev ile
yapılır.
Kütüphaneyi sınamak için iki tane CStash oluşturulur, birincisi int'leri
ikincisi ise 80 tane char'ı tutsun.
//:B04:CLibTest.cpp
//{L} CLib
//C benzeri kütüphane sınaması
#include "CLib.h"
#include <fstream>
#include <iostream>
#include <string>
#include <cassert>
using namespace std ;
int main( ){
//değişkenleri C deki gibi en başta belirleyin
CStash,intStash,stringStash ;
int i ;
char* cp ;
ifstream in ;
string line ;
const int bufsize=80 ;
//değişkenlere ilk atamalar için hatırlayalım
initialize(&intStash,sizeof(int)) ;
for(i=0;i<100;i++)
add(&intStash,&i) ;
for(i=0;i<count(&intStash);i++)
cout<<"fetch(&intStash, "<<i<<") = "
<<*(*int)fetch(&intStash,i)
<<endl ;
//80 karakterli stringi tutar:
initialize(&stringStash,sizeof(char)*bufsize) ;
in.open("CLibTest.cpp") ;
assert(in) ;
while(getline(in,line))
add(&stringStash,line.c_str( )) ;
i=0 ;
while((cp=(char*)fetch(&stringStash,i++))!=0)
cout<<"fetch(&stringStash, "<<i<<") = "
<<cp<<endl ;
cleanup(&intStash) ;
cleanup(&stringStash) ;
}///:~
C kuralları uygulanarak, bütün değişkenler main( ) işlevinin çalışma
alanının başlangıcında oluşturulurlar.
Ayrıca CStash( ) değişkenlerine initial( ) işlevini çağırdığınızda, mutlaka
ilk değer atamalarını yapmalısınız. C kütüphaneleri ile ilgili sorunlardan
biride; başlatma ve silme işlevlerinin önemini kullanıcıya dikkatlice
aktarmak zorunluluğudur. Bu işlevler programda çağrılmadığı zaman,
birçok sorun ortaya çıkar. Maalesef çoğu zaman kullanıcı, başlatma ve
silmenin zorunlu olduğu ile ilgilenmez. Kullanıcılar onların ne
yapacaklarını bilirler ama " hey önce bunu yapmak zorunluluğun var "
dediğinizde ilgilenmezler. Hatta bazı kullanıcılar yapı ögelerinin başlatma
işleminin kendiliğinden olacağını sanırlar. Şüphesiz C dilinde bunu
engelleyecek mekanizma yoktur. (çok daha önceden belirtmek)
intStash, int değerlerle, ve stringStash karakter dizileriyle doldurulur.
CLibTest.cpp kaynak kod dosyası açılarak bu karakter dizileri üretilir,
satırlar kaynak kod dosyasından okunarak line adlı string'e yazılır. Daha
sonra line'nın karakter temsili için c_str( ) üye işlevini kullanarak bir
göstergeç üretilir. Her Stash yüklendikten sonra, gösterilir. intStash for
döngüsü kullanılarak yazdırılır, ve sınırları belirlemek içinde count( )
işlevi kullanılır. stringStash while ile yazdırılır, fetch( ) işlevi sıfır değeri
döndürdüğünde, while döngüsünden break ile çıkılır, zira while sınırları
bu esnada aşılmış olur. Burada ek bir döküm görürsünüz.
cp=(char*)fetch(&stringStash,i++)
Bu, C++ daki daha katı bir tip denetimi olup, void* e başka herhangi bir
tipe atanmasına izin vermez. (C buna izin verir.)
kötü olasılıklar
Bir C kütüphanesi oluştururken, genel sorunlara bakmadan önce,
anlaşılması gereken birden fazla önemli unsur vardır.
CStash'i kaynak alan herhangi bir dosyada mutlaka CLib.h başlık dosyası
bulunmalıdır, zira yoksa, derleyici o yapının ne olduğunu bilemez. Her
nasılsa işlevin nasıl birşey olduğunu bir şekilde tahmin edebilir, fakat bu
özellik C dilinin
programcıya ana tuzaklarından biridir.
Her zaman başlık dosyalarını ekleyerek işlev bildiriminde bulunulması
gerekmesine rağmen, işlev bildirimleri C dilinde
esas değildir. Bildirimi yapılmamış bir işlevi C dilinde çağırmak
mümkündür, fakat C++ da bildirimi yapılmamış bir işleve çağrıda
bulunulamaz. İyi bir derleyici çağrısı yapılan işlevin öncelikle bildiriminin
yapılması gerektiği konusunda uyarıda bulunur, fakat bu C dili
standartında zorunlu değildir. Aslında böyle bir uygulama tehlikelidir, zira
bildirimde bulunulmamışsa, C derleyicisi int değişkenle çağrılmış bir
işlevin değişkenlerini, float bile olsa int olarak kabul edebilir. Böyle bir
uygulama, programlarda bulunması zor hatalara neden olur.
Her ayrı C uygulama dosyası (.c uzantılıdır) bir çevrim birimidir. Derleyici
her çevrim birimi üzerinde ayrı ayrı çalışır.
Ve çalışırken sadece tek birimle ilgilidir. Başlık dosyalarının eklenmesi ile
sağlanan bilgiler, derleyicinin, programın geri kalan kısmını anlamasında
çok önemlidir. Başlık dosyalarındaki bildirimler oldukça önemlidir, zira
başlık dosyalarının eklendiği heryerde derleyici ne yapacağını tam olarak
bilir. Örnek olarak eğer başlık dosyasında
void func(float) bildirilirse, derleyici, bu işlev int değişken ile
çağrıldığında değişken aktarılırken int'in float'a dönüştürüleceğini bilir.
Buna terfi denir. Bildirim yapılmadığı zaman, C dilinde derleyici func(int)
işlevinin varlığını kabul eder. Yani C de terfi olmaz ve hatalı veri func( )
işlevine aktarılır.
Her çevrim birimi için derleyici bir hedef dosya oluşturur. Hedef
dosyaların .o , .obj veya benzeri uzantıları vardır.
Bu hedef dosyalar gereken başlatma kodları ile birlikte, ilişimci tarafından
biraraya getirilip, çalıştırılabilir dosya haline çevrilirler. İlişim sırasında
bütün dış kaynaklar bilinmelidir. Örneğin; CLibTest.cpp de bulunan
initialized( ) ve
fetch( ) işlevleri bildirilmiş (yani derleyiciye nasıl birşey oldukları
belirtilmiş) ve kullanılmıştır, fakat tanımlanmamıştır. Bunlar CLib.cpp
gibi bir başka yerde tanımlanmışlardır. Böylece CLib.cpp deki çağrılar dış
kaynaklarla olur. İlişimci bütün hedef dosyaları biraraya getirirken
çözümlenmemiş tüm kaynakları alıp bunlara denk düşen adresleri mutlaka
bulmalıdır. Anılan adresler çalıştırılabilir dosyada dış kaynakların yerine
konur.
C dilinde gerçeklenmesi önemli olan, ilişimcinin aradığı dış kaynaklar,
genellikle işlev adları olup, yazımlarında da önlerinde alt çizgi ( _ )
bulunur. İlişimcinin yapacakları işlevin çağrıldığı yerdeki ad ve hedef
dosyadaki işlev gövdesinin birebir uyumunu sağlamaktır. Yanlışlıkla çağrı
yaparak derleyicinin func(int) olarak yorumlamasına neden olunduğu bir
durumu gözönüne alalım, hedef dosyalardan birinde başka bir yerde func
(float) işlev gövdesinin bulunduğunu düşünelim, ilişimci bir yerde _func'ı
görür, başka bir yerde de _func'ı görür, herşeyi doğru görür.
Çağrı noktasında func( ) yığıta bir int değer koyar, func( ) işlev gövdesi
yığıtta bir float değer bekler. İşlev sadece değeri okur, üzerine herhangi bir
şey yazmazsa yığıtta kırılma olmaz. Gerçekte float değer ile yığıtın
okunmaması başka anlamlandırmalara da yolaçabilir. Bu en kötü
durumdur, zira bulunması çok zor hatalara neden olur.
yanlış olan ne ?
Biz yapmamamız lazım geldiği halde, dikkat çekecek kadar uyumlu
davranıyoruz. CStash.h kütüphanesinin stili C programcıları için iyi bir
ana kaynaktır, ama bir süre baktığınızda farkına varacağınız gibi, fazlası ile
hantaldır.
Kullanırken kütüphanedeki her işleve yapının adresini aktarmak
zorundasınız. Kodu okurken işlev çağrılarının
anlamları ile kütüphane mekanizması bileşik olarak bulunur, neler
olacağını anlamaya çalışırken şaşırtıcı olanda budur.
C kütüphanelerinin kullanımı esnasında, çıkan en büyük sorunlardan biri;
isim çatışmalarıdır. C dili, işlevler için tek isim alanına sahiptir; yani
ilişimci işlev adını ararken, tek bir ana listeye bakar. Yani C dili C++ da
olduğu gibi namespace anahtar kelimesine sahip değildir. Ek olarak;
derleyici çevirici birimi üzerinde çalışırken, adı geçen isimli tek bir işlevle
çalışabilir. İşlevlik kelimesi kütüphane ile eşdeğerdir, hatırlatalım.!!
Şimdi iki ayrı üreticiden iki ayrı kütüphane satın almaya karar verdiğinizi
düşünelim, ve her kütüphanenin başlatma değerleri ataması yapan ve bu
değerleri boşaltan yapısı vardır. Her iki kütüphane üreticisi de initialize( )
ve cleanup( ) işlev isimlerini bu amaçlar için seçmiş olsunlar. Tek bir
çevrim birimine, her iki kütüphanenin başlık dosyalarını eklediğiniz
zaman, C derleyicisi ne yapar?. Allahtan C buna hata iletisi verir.
Bildirimde bulunulmuş işlevlerin, farklı iki değişken listelerinde ki tip
uyumsuzluğu, bu hata iletisinde belirtilir. Aslında tek bir çevrim birimine
eklenmemiş
olsalar bile, yine de ilişimci de hala, sorunlar ortaya çıkar. İyi bir ilişimci
isim çatışmalarını algılar, fakat bazı ilişimciler ise, ilk rastladıkları işlev
adını alıp kullanırlar. İlk rastladıkları, sizin verdiğiniz ilişim listesindeki
düzene göre olan hedef dosyalarında bulunur. (Bunu bir özellik olarak
kullanabilir, kütüphane işlevini sizin kendi sürümünüzle yer
değiştirebilirsiniz.). İki farklı kütüphanedeki aynı adı taşıyan bir işlevi
herhangi bir durumda kullanmazsınız. C kütüphane üreticileri bu sorunu
çözmek için bütün işlev adlarının önüne, çoğu kez bazı karakter dizileri
koymuşlardır. Örneğin; initialize( ) ve cleanup( ) işlevleri
CStash_initialize( ) ve CStash_cleanup( ) olurlar. Aslında yapılması
mantıklı olanda budur, zira struct'ın adı dekore edilmiş olur ve işlevin adı
üzerinde işlev çalışmış olur.
Artık C++ da sınıfları oluşturmanın ilk adımlarını atmanın zamanı geldi.
struct içindeki değişken isimleri global değişken adları ile çatışmaz. Böyle
işlevler belli bir struct üzerinde işlem yaparken, neden anılan üstünlüğü
kullanmayasınız?. Yani neden işlevleri structa üye yapmazsınız?.
temel nesne
İlk adımda tam olarak, C++ işlevleri struct'a üye işlevler olarak
yerleştirilir. Şimdi CStash'in C sürümünün, C++
Stash'e çevrilmesini göreceğiz.
//:B04:CppLib.h
//C benzeri kütüphane C++ ya çevriliyor
struct stash{
int size ;
//her alanın boyutu
int quantity ; //bellek alanlarının sayısı
int next ;
//bir sonraki boş alan
//dinamik olarak yerleştirilen bayt dizisi
unsigned char* storage ;
//işlevler
void initialize(int size) ;
void cleanup( ) ;
int add(const void* element) ;
void* fetch(int index) ;
int count( ) ;
void inflate(int increase) ;
};///:~
Gördüğünüz üzere yukarıda typedef yok. Onun yerine, typedef için C++
derleyicisi yapı adını, başka yeni
tip adına çevirir (bunlar; int, char, float ve doubledır).
Bütün veri üyeleri aynen öncekiler gibi, ama işlevler struct gövdesinde
bulunurlar. Ek olarak, kütüphanenin C sürümündeki ilk değişken ortadan
kaldırılır. C++ da, yapıda işlem görecek olan bütün işlevlere, ilk değişken
olarak yapının adresini aktarım zorlunluluğu yoktur, derleyici bu görevi
gizlice yerine getirir. Şimdi ilgilenilen tek işlev değişkeni, işlevin
yapacakları ile ilgili olanlardır. İşlevin işlem mekanizması ile ilgili olanlar
değil.
İşlev kodunu, kütüphanenin C sürümündeki gibi aynen gerçeklemek
oldukça önemlidir. Değişken sayısı aynıdır,
(aktarılan yapı adresini görmeseniz bile, o oradadır) ve her işlev için bir
işlev gövdesi vardır. Yani aşağıdaki gibi;
Stash A,B,C ;
Bunun anlamı; her değişken için farklı add( ) işlevi alınmaz.
Kütüphanenin C sürümü için üretilen kod ile, yeni sürüm için üretilen kod
hemen hemen aynıdır. Zaten isimleri,
Stash_initialize(,) ve Stash_cleanup(,) v.d. ni üretmek için düzenlemek
yeteri kadar ilginçtir. İşlev adları struct
içinde bulunduğu zaman, derleyici de aynı işlemleri yapar. Bundan dolayı
Stash yapısının içinde bulunan initialize( )
başka bir yapıda bulunan initialize( ) işlevi ile çatışmaz. Hatta global
değişken olan initialize( ) ile de çatışmaz.
İşlev adı düzenlemesi ile ilgili hiç şüpheye düşmemelisinizdüzenlenmemiş adlar da kullanabilirsiniz. Ama bazı durumlarda, bu
initialize( ) işlevinin structStashe ait olduğunu, başka herhangi bir structa
ait olmadığını, ayrıntılı belirtmek gerekebilir. Özellikle işlev tanımlama
sırasında ne olduğunu, özellikleri ile belirtmek ihtiyacı doğar. Anılan bu
tam belirleme işlemi, C++ da, görüntü (=çalışma alanı) duyarlık işleci
(scope resolution operator) (::) ile yerine getirilir (böyle adlandırmanın
nedeni isimlerin farklı görüntülerde olabilmesindendir; global görüntüde
veya struct görüntüsü içinde). Örnek olarak; Stash içindeki initialize( )
tanımlamak istediğiniz zaman;
Stash::initialize(int size)
Aşağıdaki örnekte, işlev tanımlarında görüntü duyarlık işlecinin nasıl
kullanıldığını inceleyebilirsiniz.
//:B04:CppLib.cpp {O}
//C kütüphanesi C++ ya çevrilir
//yapı ve işlevlerin bildirimi
#include "CppLib.h"
#include <iostream>
#include <cassert>
using namespace std ;
//saklama artarken eklenecek öge sayısı
const int increment=100 ;
void Stash::initialize(int sz){
size=sz ;
quantity=0 ;
storage=0 ;
next=0 ;
}
int Stash::add(const void* element){
if(next>=quantity) //yeterli yer varmı?
inflate(increment) ;
//bir sonraki yerden başlayarak kopyalamaya başla
int startBytes=next*size ;
unsigned char* e=(unsigned char*)element ;
for(int i=0;i<size;i++)
storage[startBytes+i]=e[i] ;
next++ ;
return(next-1) ; //dizin numarası
}
void* Stash::fetch(int index){
//dizin sınırlarını belirle:
assert(0<=index) ;
if(index>=next)
return 0; //son gösterimi
//istenen ögenin göstergeçini belirle
return&(storage[index*size]) ;
}
int Stash::count( ){
return next ; //CStash teki öge sayısı
}
void Stash::inflate(int increase){
assert(increase>0) ;
int newQuantity=quantity+increase ;
int newBytes=newQuantity*size ;
unsigned char* b=new unsigned char[newBytes] ;
for(int i=0;i<oldBytes;i++)
b[i]=storage[i] ; //eskiyi yeniye kopyala
delete []storage ; //eski saklama yeri
storage=b ; //yeni yeri belirt
quantity=newQuantity ;
}
void Stash::cleanup( ){
if(storage!=0){
cout<<"bellek bosaltimi"<<endl ;
delete []storage ;
}
}///:~
C ve C++ dilleri arasında bir sürü farklılık vardır. Bunlardan ilki; başlık
dosya bildirimlerinin derleyici tarafından istenmesidir. C++ dilinde,
bildirimi yapılmamış işlevi çağıramazsınız. Aksi halde derleyici hata iletisi
verir. İşlevin çağrıldığı yer ile, tanımlandığı yer arasında, işlev çağrısının
tutarlılığını emniyete alma bakımından bu önemlidir. İşlevin çağrılmadan
önce bildirim zorunluluğu, C++ derleyicisi sanal olarak size bildirimi,
başlık dosyaları ile ekletir. İşlevlerin tanımlandığı yerde, aynı başlık
dosyalarını da kullanırsanız, daha sonra derleyici, başlık dosyasındaki
bildirim ile tanımlanan işlevin uyumunu sınar. Bunun anlamı başlık
dosyalarının, işlev bildirimleri için sağlama kaynakları olmasıdır. Ayrıca
proje boyunca kullanılan bütün çevrim birimlerinin kendi aralarında tutarlı
olmaları güvenceye alınmış olur.
Global işlevler tanımlandıkları ve kullanıldıkları her yerde doğal olarak
hala elle bildirilirler (Bu işlem çok yorucu ve sevimsizdir). Her halükarda
yapılar tanımlanmadan ve kullanılmadan önce mutlaka bildirilmek
zorundadır.
Yapıların tanımlanması için en uygun yer başlık dosyalarıdır, sadece
dosyalarda bilerek saklanmış durumlar bundan istisnadır.
Görüntü duyarlılığı ve kütüphanenin C sürümünde bulunan ilk değişkenin,
aleni olmaması hariç, bütün üye işlevler
hemen hemen aynen sanki C işlevleri gibidirler. Aslında aleni olmayan
değişken hala oradadır, zira işlev belli bir struct değişkeni üzerinde
çalışabilir olmak zorundadır. Burada dikkat edilmesi gereken; üye işlevin
içinde ayrıca üye
seçiminin de yapılmasıdır. s->size=sz yerine size=sz yazarız, böylece
yorucu s-> işaretini yazmamış oluruz. Bununla, yazmış olduğunuza
herhangi başka anlam eklenmemiş olur. C++ derleyicisi bunu sizin için
açıkça yapar.
Gerçekten gizli ilk değişkeni alır (elle önceden aktarmış olduğumuz
yapının adresi) ve üye seçiciyi, struct veri üyelerinden herhangi biri
dayanak alındığında uygular. Bunun anlamı; bir, ikinci struct üye işlevinin
içinde bulunduğunuz da, herhangi bir üyeyi (buna ikinci bir üye işlev de
dahil) dayanak almanız için, basit bir şekilde onun adını yazmanız
yeterlidir. Derleyici adı, global sürümünden önce yörel yapıların adlarının
içinde arar. Bu özellikten şunu farkedersiniz; sizin yazdığınız kodlar
sadece yazma bakımından kolay değil, okuma bakımından çok daha fazla
kolaydır. Fakat yapının adresini bazı nedenlerden elinizin altında
bulundurmak isterseniz ne olur?. Bunun yanıtı kütüphanenin C sürümünde
kolaydı, zira s ile çağrılan herbir işlevin ilk değişkeni CStash* idi . C++ da
ise olaylar çok daha tutarlıdır. C++ da özel bir anahtar kelime vardır; this.
this struct adresini üretir. Bu kütüphanenin C sürümündeki 's' e
eşdeğerdir. C tarzı eski gösterime aşağıdaki yazımla varılır;
this->size=Size ;
Derleyici tarafından üretilen kod tamamen aynıdır; böylece this anahtar
kelimesini kullanmak gerekmez. Bazı programlarda this-> ifadesini
görebilirsiniz, bu ifade programa özel bir anlam eklemez, sadece ilgili
programcının deneyimsiz olduğunu gösterir. Siz genel olarak kullanmayın,
fakat gereksinim olursa, elinizin altında olduğunuda unutmayın (ilerleyen
bölümlerde this kullanılan örneklere rastlayacaksınız.)
Şimdi ayrıntıları verilecek son bir konu daha var. C dilinde void* başka bir
göstergeçe atanabilir;
int i=10 ;
void* vp=&i ;
int* ip=vp ;
//C ve C++ da geçerli
//sadece C de geçerli
derleyiciden herhangi bir yakınma sesi çıkmaz. Fakat C++ da bu deyime
izin verilmez. Bunun nedeni C tip denetimi konusunda o kadar hassas
değildir. Bu nedenle belirlenmemiş tipteki göstergeç belirlenmiş tipteki
göstergeçe atanabilir.
Böyle bir işlem C++ da olası değildir. C++ da, tip kritik öneme sahiptir.
C++ da tip bilgisinde sorun ortaya çıktığında,
derleyicinin sesi derhal yükselir. Bu her zaman önemli olmasına rağmen,
struct üye işlevlere sahip olduğu için C++ da
özellikle önemlidir. C++ daki dokunulmazlığını gözönüne alarak
göstergeçleri structa aktarırsanız, struct için üye işlev çağrısını
sonlandırabilirsiniz, hatta bu mantık olarak struct için bulunmayabilir bile.
Felaket için tam bir tedbir. Bundan dolayı C++ da, herhangi bir tipteki
göstergeç void* e (bu orijinal yapısı olup, yeterli büyüklükte veriyi
tutabilecek boyutta olmalı) atanırken, void tipindeki göstergecin başka
herhangi bir tipe atanmasına izin verilmez. Okura anlatmak için herzaman
döküm gerekebilir, sizin yapacağınız derleyicinin varılacak tipi
işlemlemesidir. Bu ilginç bir neticeye yolaçar. C++ dilinin amaçlarından
biri de mümkün olduğunca fazla C kodlarını derlemek olup, yeni dile yani
C++ diline geçişi kolaylaştırmaktır. Doğaldır ki bu, C dilinde kullanılan
bütün kodların, C++ dilinde de kullanılabileceği anlamına gelmez. C
derleyicisi için, çok sayıda ortadan kaldırılması gerekli, tehlikeli ve hata
üretici unsurlar vardır. (bunları kitap ilerlerken göreceğiz.) .C++ derleyicisi
de böyle durumlar da hata uyarıları verir. Bu aslında engelden ziyade
avantajdır. Gerçekte C dilinde geliştirilen programlarda, önlenmek istenen
fakat bulunamayan hataların olduğu birçok durum vardır. Fakat siz bu
programı C++ da tekrar yeniden derlediğiniz de, sorun belirlenir. C
dilinde, elde edilebilecek derlenecek programı çoğunlukla bulursunuz,
fakat daha sonra onu çalıştırmak için sahip olmak zorundasınız. C++ da
ise, program doğru derlenecek olursa, çoğunlukla çalışır. Bunun nedeni;
C++ dili, tip denetimi konusunda, çok daha ince elemesidir. Aşağıdaki
sınama programında kullanılan Stash'in C++ sürümün de çok sayıda yeni
uygulamayı göreceksiniz;
//:B04:CppLibTest.cpp
//{L} CppLib
//C++ kütüphane sınaması
#include "CppLib.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
Stash intStash ;
intStash.initialize(sizeof(int)) ;
for(int i=0;i<100;i++)
intStash.add(&i) ;
for(int j=0;j<intStash.count( );j++)
cout<<"intStash.fetch.("<<j<<")="
<<*(int*)intStash.fetch(j)
<<endl ;
//80 karakter tutan string
Stash stringStash ;
const int bufsize=80 ;
stringStash.initialize(sizeof(char)*bufsize) ;
ifstream in(""CppLibTest.cpp") ;
assure(in,"CppLibTest.cpp") ;
string line ;
while(getline(in,line))
stringStash.add(line.c_str( )) ;
int k=0 ;
char* cp ;
while((cp=(char*)stringStash.fetch(k++))!=0)
cout<<"stringStash.fetch("<<k<<")="
<<cp<<endl ;
intStash.cleanup( ) ;
stringStash.cleanup( ) ;
}///:~
Yukarıda, değişkenlerin tamamı uçuş halindeyken tanımlanmışlardır (uçuş
hali konusu önceki bölümlerden birinde anlatılmıştı). Uçuş hali yerine iş
üzerindeyken kelimeleri de kullanılabilir. Aslında bunların anlamı
görüntünün bulunduğu herhangi bir yerde, yani C de olduğu gibi, görüntü
başlama yeri kısıtlaması yok demektir.
Kod, CLibTest.cpp tekine tamamen benzer. Ama üye işlev çağrılırken
çağrı işlemi, değişken adının yanına üye seçim işleci (.) kullanılarak yerine
getirilir. Bu tarz yazım en uygunudur. Çünkü bu, yapı veri üyesinin
seçimini taklit eder.
Fark ise; işlev üyesi olduğu için, değişken listesinden kaynaklanır.
Aslında derleyicinin ürettiği çağrı, orijinal C kütüphane işlevine çok fazla
benzer.Böylece isim düzenlemesi ve this aktarımı düşünüldüğünde, C++
daki işlev çağrımı; intStash.initialize(sizeof(int),100),
Stash_initialize(&intStash,sizeof(int),100) olarak gelir,bunun altında
olanları anlamak için AT&T nin C++ derleyicisinin cfront anahtar
kelimesi ile üretilen C koduna, ki bu da C derleyicisi altında derlenebilirdi,
bakmak gerekir. Bu yaklaşım da, cfront içinde C derleyicisi bulunan
herhangi bir makineye aktarılabilir,ve böylece C++ derleyici teknolojisi
hızla yayılır. Bildiğiniz üzere C++ derleyicisi C kodunu üretmişti, bu
nedenle mutlaka C++ yazım biçiminin de, C dilinde bir şekilde
belirtilebilmesi gereklidir (bazı C++ derleyicileri hala C kodu üretilmesine
izin vermektedir.)
CppLibTest.cpp deki başka bir farklılıkta; require.h başlık dosyasının
tanıtımındadır. Bu başlık dosyası, assert( ) işlevinin yerine geliştirdiğimiz,
hata ayıklama amacı ile yazılmış çok ayrıntılı denetim sağlayan bir
işlevdir.
Yeni başlık dosyası assure( ) işlevini de içermekte olup, çok sayıda işlevle
beraber bu işlev dosyalar için kullanılır. Anılan işlev dosyalar açıldığı
taktirde denetim yapar ve açılmadığı taktirde de standart hata uyarısı
gönderir (açılmayan dosya adı da burada eklenir) ve programdan çıkar.
require.h işlevleri kitap boyunca kullanılacaktır. Özellikle de komut satırı
değişkenlerinin sayısının doğru miktarda olduğu ve dosyaların uygun
açıldığını güvenceye almak için kullanırız. Anılan başlık dosyası yani
require.h işlevleri, yinelenen ve dikkat dağıtan hata denetim kodlarının
yerini alır. Temelde bu işlevler çok yararlı hata uyarıları verirler. Kitabın
ilerleyen bölümlerin de anılan işlevler daha ayrıntılı işlenecektir.
nesne nedir ?
Başlangıç olarak bir örnek anlattık, şimdi bir adım geriye gidip bazı
terimlerin üzerine düşünelim. İşlevleri yapılara aktarma eylemi, C++ nın C
ye eklentileridir. Bu, yapılar için yeni bir yorum getirilmesine yolaçar. C
dilinde yapılar bir veri yığınıdır. Veri paketindeki kümeleri
işlemleyebilirsiniz. Ama onlar herhangi bir şey değil de programlama
unsuru olarak düşünülmeli, zira başka bir şey olarak anlamlandırmak çok
zordur. Anılan yapıların üzerinde, işlem yapan işlevler başka yerlerde
bulunabilir. Paketteki işlevlerle beraber artık yapı yeni bir oluşumdur, hem
davranışları hem de özgünlükleri (=C dilindeki structlar gibi) belirtebilir.
Nesne fikrinin serbest duruşu vardır, sınırlı varlığı hatırlanabilir ve
eylemciliği kendinden kaynaklanır.
C++ da nesne sadece bir değişkendir, en saf anlamda ise; bellekte sadece
bir alandır.("nesnenin mutlaka bir belirteci olmak zorundadır"
açıklamasının çok daha özel söylenmiş hali. C++ da nesne tek bir bellek
adresine sahiptir.). Veri saklanan yer, ayrıca saklanan veri üzerinde
işlemlerin yapılabileceğini de ima eder.
Maalesef, programlama dilleri arasında, oldukça iyimser kabullere rağmen
bu koşullar da tam bir tutarlılık yoktur.
Hatta bazen nesne yönelimli dillerin ne oldukları hususunda da, bazı
anlaşmazlıklar ortaya çıkmaktadır. Şu ana kadar bunlar gözardı edilmişti.
Nesne tabanlı diller de vardır, ve bu diller C++ yapıları gibi nesnelere
sahiptirler, bu nesnelerin yine C++ daki gibi işlevleri vardır (nesne tabanlı
dillere örnek olarak ADA yı verebiliriz). Nesne yönelimli programlamaya
gelince, burada nesne, resmin sadece bir parçasıdır. Veri yapılarındaki
paketlenmiş işlevleri engelleyen diller nesne tabanlıdır, nesne yönelimli
diller değillerdir.
soyut veri tiplemesi
Veriyi işlevlerle biraraya getirerek yani paketleyerek, yeni bir veri tipi
oluşturulur. Buna sarmalama (encapsulation) adı verilir. Yerleşik veri
tipleride biraraya getirilmiş çok sayıda parçadan oluşur. Örnek olarak float
veri tipinin; anasayısı(mantis), üs (eksponent) ve işaret değeri vardır. Siz
ona yapması gereken şeyleri söylersiniz; başka bir floatla toplama yap,
veya intle topla v.d.,. Tip özgünlük (karakteristik) ve davranış sahibidir.
Stash tanımı yeni bir veri tipi oluşturur. Anılan tiple add( ), fetch( ) ve
inflate( ) işlevlerini kullanabilirsiniz. Aynı float nesnesi oluşturmak için
kullanılan float f gibi Stash s yazarak ilgili tipin nesnesi oluşturulur. Bir
Stash te karakteristik ve davranış sahibidir. Aslında bu, aynı yerleşik
gerçek veri tipi gibi davransa da, onu soyut veri tipi olarak adlandırırız.
Bunun nedeni; sorun ortamından çözüm ortamına geçişi sağlayan düşünce
soyutlamasını sağlamasıdır. Ek olarakta C++ derleyicisi onu yeni bir veri
tip olarak işlemler, ve işlevin birine Stash beklemesini bildirdiğiniz de,
derleyici Stashin ilgili işleve gitmesini emniyete alır. Yerleşik veri tipleri
için uygulanan tip denetimi işlemleri, aynen soyut veri tipleri (bazen
kullanıcı tanımlı veri tipleri de denir) için de uygulanır.
Nesneler üzerinde işlem yaparken hemen bir fark görürsünüz;
nesne.uyeIslev(arglist) dediğiniz de bu "nesne için üye işlev çağrısı"
demektir. Nesne yönelimli deyişiyle "bir nesneye bir ileti gönder"
demektir. Stash s için
s.add(&i) deyimi "s ye bir ileti gönder" add( ) ile bunu kendine ekle der.
Aslında nesne yönelimli programlamayı tek cümlede özetlemek olasıdır;
nesnelere iletiler göndermek. Bir sürü nesne üretin ve onlara iletiler
gönderin, gerçekte NYP de yapılanlar bundan başka bir şey değil. Doğaldır
ki burada en hassas nokta; nesnelerin ve iletilerin neler olduğunu doğru
saptamaktır. Bu başarıldığında, C++ da uygulamalar şaşırtıcı derecede
kolaylaşır.
nesne ayrıntıları
Derslerde karşılaşılan yaygın sorulardan biri "bir nesne ne kadar büyüktür
ve neye benzer ?". Yanıt ; "bir C structından
ne beklediğiniz kadardır". Aslında Bir C structı(C++ süslemeleri olmadan)
için derleyicinin ürettiği kod, C++ derleyicisinin ürettiği kodla genellikle
hemen hemen aynıdır. Böyle bir özellik C programcılarına, kodların şema
ve boyut ayrıntıları hakkında yeniden sağlama yapar ve bazı nedenlerden
belirteç kulanımı yerine doğrudan yapı baytlarına erişilir (belli bir şema ve
boyutta olan yapılar, başka mimari yapılara aynen aktarılamaz unsurlardır.)
structın boyutu, kendini oluşturan bütün üyelerin boyutlarının bileşimidir.
Bazen derleyiciler struct'ı şemalandırırken
sınırları kesinleştirmek için bayt eklemelerinde bulunur-bu da çalışma
verimliliğini arttırabilir. İlerleyen bölümlerde
yapılara "gizli" göstergeç eklemeleri anlatılacak, şimdilik bu konu ile ilgili
meraklanmaya gerek yok.!.
sizeof işlecini kullanarak struct boyutu belirlenebilir.
Basit bir örnek ;
//:B04:Sizeof.cpp
//structların büyüklükleri
#include "CLib.h"
#include "CppLib.h"
#include <iostream>
using namespace std ;
struct A{
int i[100] ;
};
struct B{
void f( ) ;
};
void B::f( ){}
int main( ){
cout<<"sizeof struct A ="<<sizeof(A)
<<"bytes"<<endl ;
cout<<"sizeof struct B= "<<sizeof(B)
<<"bytes"<<endl ;
cout<<"sizeof CStash in C="<<sizeof(CStash)
<<"bytes"<<endl ;
cout<<"sizeof Stash in C++="<<sizeof(Stash)
<<"bytes"<<endl ;
}///:~
Bizim makinemizde ilk yazdırma deyiminin çıktısı 200 dür, zira her int 2
bayt yer kaplar. struct B bazı dengesizlikler gösterir, zira içinde ögeler
yoktur. Aslında böyle bir deyim uygulaması C de geçersizdir, fakat C++ da
tek görevi işlev adlarının faaliyet alanlarını göstermek olan struct
oluşturma ihtiyacı vardır, bu nedenle izin verilir. Fakat hala ikinci
yazdırma deyimi tarafından üretilen sıfırdan farklı bir şaşırtıcı sonuç ortaya
çıkar. Dilin ilk sürümlerin de , boyut sıfırken, bu tür nesneler
oluşturduğunuz da bir sürü zorluklar ortaya çıkardı. Nesne onlar
yaratıldıktan hemen sonra
oluşturulduğun da hepsi aynı adrese sahip olurdu. Adresleri arasında fark
olmazdı. Nesnelerin temel özelliklerinden biri; herbirinin mutlaka tek bir
adrese sahip olmaları zorunluluğudur. Bu sayede veri üyeleri bulunmayan
yapılar da her zaman en az, sıfırdan farklı büyüklüğe sahiptir. Son iki
sizeof işlecinin neticeleri C++ dakilerle C dilindekilerin aynı olduğunu
gösterir. C++ burada başağrıtan ayrıntılar eklemez.
Başlık dosyası etiketi
Üyeleri ile struct oluşturduğunuz da, yeni bir veri tipi oluşturursunuz. Bu
yeni tipe genellikle, siz ve başkaları kolaylıkla erişir. Buna ek olarak,
arayüz (işlev bildirimleri) ile uygulaması (işlev tanımlamaları) ayrık olur.
Bu sayede uygulama, sistemin tamamı yeniden derlenmeden,
değiştirilebilir. Bunun için başlık dosyası ile yeni tip bildirimi yapılır.
İnsan, C programında ilk kez bir başlık dosyasına rastladığında, bu başlık
dosyası ona esrarengiz gelebilir. Çok sayıdaki C kitabı da, başlık
dosyalarını üstünkörü geçerler. Derleyiciler işlev bildirimlerini
desteklemez, yapıların bildirimleri
haricinde, sanki bir opsiyon olarak algılanmalarına yolaçar. C++ da ise
başlık dosyaları bir kristal berraklığındadır. Kolay program geliştirmek için
başlık dosyaları sanal vekillerdir, içlerine çok özel bilgi koyarsınız;
bildirimler.
Başlık dosyası derleyiciye kütüphaneniz de bulunanları bildirir. Sadece
hedef dosyası veya kütüphane dosyası ile birlikte başlık dosyasına sahip
olsanız bile kütüphaneyi kullanabilirsiniz; .cpp uzantılı dosya için kaynak
gerekmez. Başlık dosyası arayüz için gereken özellikleri içerir.
Derleyici tarafından desteklenmemesine rağmen, C dilinde büyük hacimli
projeler geliştirmenin en iyi yolu; kütüphane
kullanmaktır.; aynı hedef modül veya kütüphaneye ilintili işlevler toplanır,
ve işlevler için bütün bildirimleri tutan başlık dosyası kullanılır. C++ da
vazgeçilemezdir; herhangi bir işlevi C kütüphanesine koyabilirsiniz. C++
da ise soyut veri tipleri işlevleri belirler, bu işlevler structtaki verilere
ortak erişim vasıtası ile bağlantılıdır. Herhangi bir üye işlev struct
bildiriminde mutlaka bildirilmek zorundadır, başka herhangi bir yere
konulamaz. İşlev kütüphanelerinin kullanımı C dilinde teşvik edilmişti,
C++ da ise artık bilimselleşti.
Başlık dosyalarının önemi
Kütüphane işlevlerinden biri kullanıldığı zaman, C, başlık dosyasını
gözardı etmenize izin verir, işlevin adını elle yazmanız yeterli olur.
Geçmişte bazı programcılar bunu derleyiciyi hızlandırmak için yaptılar.
Böylece dosyayı ekleyip
açma işleminden kurtulundu. (Bugünkü modern derleyicilerle, genellikle
böyle bir işlem gerçeklenmez). Örnek olarak
C işlevlerinin oldukça yavaşlarından printf( ) (<stdio.h> dan gelir) ele
alalım ;
printf( ....) ;
ayraçlar arasındaki noktalar değişken adlarını belirtir. Bunun anlamı;
printf( ) işlevi, tipleri belli değişkenlere sahiptir.
Şimdilik tipleri gözardı edelim. Sadece ne çeşit değişkenler göründüğü ve
alındığına bakalım. Bu çeşit bildirim, değişkenler üzerindeki hata denetimi
konusun da şüpheler yaratır.
Bu uygulama, sorunlara neden olabilir.İşlevler elle bildirilirse, dosyalardan
birinde hata çıkabilir. Derleyici de sadece
sizin elle yazdığınız bildirimi göreceği için dosyada ki hataya uyar. Daha
sonra program doğru biçimde ilişimlendiği için dosyada ki hata devam
eder. Böyle bir program hatası, bulunması zor hatalardandır. Başlık
dosyası kullanarak böyle hatalardan kolaylıkla sakınmak mümkündür.
İşlev bildirimlerinin tamamını bir başlık dosyasına koyar, ve işlev
kullanılan heryerde başlık dosyasını eklerseniz, ve işlevin tanımlanması
bütün sistemin tutarlı işlev bildirimi güvenliğini sağlar. Tanım dosyasına
başlığı ekleyerek, bildirim ve tanımın uyumunu güvenceye alırsınız.
C++ da başlık dosyasında bir struct bildirilirse, struct üye işlevlerinin
tanımlandığı ve structın kullanıldığı her yere
mutlaka başlık dosyasını eklemelidir. Önceden bildirimi yapılmamış bir
düzenli işlev veya üye işlev çağrılırsa veya
tanımlanırsa, C++ derleyicisi hata iletisi üretir. Başlık dosyalarının doğru
kullanımı, dil kütüphaneleri arasında tutarlılık sağlar, ve ayrıca aynı
arayüzün kullanımı sayesinde hatalar azalır.
Başlık, kütüphane yazarı ile kütüphane kullanıcısı arasındaki antlaşmadır.
Antlaşma; veri yapılarını, değişkenlerin durumlarını ve işlev çağrılarının
geri dönüş değerlerini belirtir. "İşte, benim "işlevliğim" bunu yapar" der.
İlk kez kütüphane yerine "işlevlik" sözcüğü kullanıldı. Aslında "işlevlik"
sözcüğü, programlama da kullanılan kütüphane kelimesine, anlam
bakımından tam karşılık olmaktadır. Kullanıcı uygulamasını geliştirmek
için, işlevlik ile ilgili biraz önce anlatılan bazı bilgilere gereksinim duyar,
derleyici ise uygun kodu üretebilmek için işlevliğin bütün bilgilerine
gereksinim duyar. struct kullanıcısı, başlık dosyasını basitçe ilave eder,
ilgili struct nesnelerini üretir ve hedef modül yada işlevliği ilişimlendirir.
(yani ;derlenmiş kod.)
Derleyici, bütün işlev ve yapıların kullanılmadan önce bildirim
zorunluluğunu destekler. Ayrıca üye işlevler de tanımlanmadan önce
mutlaka bildirilmelidir. Bildirimler başlığa konur, üye işlevlerin
tanımlandığı dosya da başlıka eklenir, ve anılan dosya (/veya dosyalar)
kullanıldığı yerde bulunur. Bütün sistem boyunca işlevliği açıklayan tek
bir başlık dosyasının olması, derleyicinin tutarlı kod üretmesine ve hataları
engellemesine yardımcı olur.
Uygun kodlar ve etkin başlık dosyaları oluşturmak için, belli kurallar
olduğu mutlaka bilinmelidir. İlk kural; başlık dosyalarına neler
konulabileceği ile ilgilidir. Temel kural ;"sadece bildirimler" dir. Yani
sadece derleyiciye bilgi verilecek. Verilen bu bilgi, kod üreterek veya
değişken oluşturarak bellekte saklama ile ilgili değildir. Bunun nedeni; bir
proje de başlık dosyasının çok sayıda çevrim birimin de kullanılmasıdır, ve
bir belirtecin saklama yeri birden fazla ise ilişimci birden fazla tanım hatası
ile karşınıza çıkar. (Bu C++ nın "tek tanım kuralıdır". Unsurları istediğiniz
sayıda bildirilebilirsiniz, fakat herbir unsurun sadece tek bir tanımı olmak
zorundadır.)
Bu kural hızlı ve çok zor değildir. Bir değişkeni başlık dosyasında "dosya
static"(sadece bir dosyada görünürlük) olarak tanımlarsanız, proje boyunca
o verinin birçok durumu olur, fakat ilişimci de çatışma olmaz.(C++ da
"dosya static"
pek tercih edilmez.). Son olarak şu belirtilmeli; başlık dosyasında ilişim
sırasında sorun oluşturacak herhangi bir etkinlik yapılmaz.
Çoklu-bildirim sorunu
İkinci başlık dosyası kuralı da şudur; başlık dosyasına bir struct bildirimi
konulduğun da, karmaşık bir program da
aynı dosya birden fazla defa eklenebilir. iostream buna iyi bir örnektir.
Herhangi bir anda bir struct G/Ç (giriş/çıkış)
esnasında iostream başlıklarından birini ekler. Üzerinde çalışılan cpp
dosyası birden fazla çeşit struct kullanıyorsa (tipik olarak herbirine başlık
dosyası eklenir), birden fazla <iostream> başlık dosyası ekleme ve
iostream'lerin yeniden bildirimi riski ortaya çıkar.
Derleyici yapının yeniden bildirimini, hata olarak algılar (hem struct hem
de class için geçerlidir). Zira aksi taktirde
farklı tipler için aynı adı kullanma izin vermesi gerekir. Çoklu başlık
dosyaları olduğunda hatayı engellemek için, başlık dosyalarına
önişlemleyici kullanarak zekice bazı inşalar yapılır (Aslında standart C++
de bulunan <iostream> gibi bazı başlık dosyaları bu "zekaya" sahiptirler)
Hem C hem de C++ bir işlevin yeniden bildirimine, iki bildirim de uyumlu
olursa izin verir, fakat hiçbirisi bir yapının yeniden bildirimine izin
vermez. Yani işlevin yeniden bildirimine izin var, yapınınkine yok.
Yapının yeniden bildirimine
izin verilmemesi, C++ da özellikle önemlidir, çünkü eğer derleyici bir
yapının yeniden bildirimine izin verseydi, iki bildirim de birbirinden farklı
olunca, hangisi kullanılacaktı?.
Yeniden bildirim sorunu C++ çok kez ortaya çıkar, zira her veri tipi
genellikle kendi başlık dosyasına sahiptir, ve bir veri tipini kullanan başka
bir veri tipi oluşturulursa, bu ikinci veri tipine başlık dosyası eklenmesi
zorunluluğu vardır.
Bir projede bulunan herhangi .cpp dosyasına aynı başlık dosyasına sahip
çok sayıda dosyalar eklenebilir. Bir kez derleme esnasında, derleyici aynı
başlık dosyalarına birçok kez rastlar. Başka birşey yapmazsanız, derleyici
yapıya birden fazla rastladığı için derleme hatası raporu verir.Sorunu
çözmek için önişlemleyici hakkında biraz daha bilgiye gereksinimimiz var.
önişlemleyici yönergeleri
#define, #ifdef ve #endif
#define önişlemleyici yönergesi, derleme-zamanı bayraklarını üretmek için
kullanılabilir. İki seçenek var, birincisi; önişlemleyiciye, bayrağa değer
atanmadan tanım yapılmasının söylenmesi.
#define FLAG
ya da değer atanması(genelde C biçimi böyledir.)
#define PI 3.141519
bayrağın herhangi bir şekilde tanımlanıp tanımlandığı, #ifdef ile
önişlemleyci tarafından sınanır.
#ifdef FLAG
bunun yanıtı doğrudur (true), daha sonra #ifdef i takip eden kodlar
derleyiciye gönderilen paketin içine konur.
Buradaki içerik önişlemleyici aşağıdaki deyime rastlayana kadar sürer,
rastlayınca da durur.
#endif
ya da
#endif //FLAG
#endif ten sonra aynı satırda yorum olmayan ifade bulunması hatadır, bazı
derleyiciler kabul etseler bile . #ifdef/#endif
çiftleri içiçe yuvalanabilirler.
#define nin tamamlayıcısı #undefine dir, bu ifade #ifdef in yanlış (false)
değer üretmesini sağlar. Ayrıca #undef önişlemleyicinin makro
kullanımını durdurur. #ifdefin tamamlayıcısı #ifndef tir.#ifndef etiket
tanımı yapılmazsa doğru değer (true) üretir (başlık dosyalarında
kullandığımız budur).
C önişlemleyicisinin yararlı başka özellikleri daha vardır. Bu özellikler için
C önişlemleyici belgelerine bakmak gerekir.
başlık dosyaları için standart
Künyeli bir başlık dosyasın da, eğer bu başlık dosyası belli bir cpp
dosyasına eklenmişse öncelikle incelenmelidir. Bu işlem, önişlemleyici
bayrağını sınayarak gerçeklenir. Bayrak ayarlanmamışsa dosya
eklenmemiş demektir, ve bayrağı ayarlamanız (yeniden bildirimi
engellemek için) lazım ve künyeyi bildirmelisiniz. Bayrak ayarlanmıştı o
zaman da tipi bildirilmiştir, artık onu bildiren kod hemen gözardı
edilmelidir. Şimdi aşağıda bir başlık dosyası göreceksiniz;
#ifndef HEADER_FLAG
#define HEADER_FLAG
//tip bildirimi yapılacak...
#endif //HEADER_FLAG
Gördüğünüz gibi, başlık dosyası ilk kez eklendiğin de, başlık dosyasının
(tip bildirimini ekleyerek) içeriği önişlemleyici tarafından eklenir. Ekleme
yapıldıktan sonraki tüm adımlarda - tek derleme biriminde- tip bildirimi
gözardı edilir. HEADER_FLAG adı tek olabilir ve takip edilecek
güvenilir bir standart olarakta başlık dosya adı
büyük harfle yazılır. İsimler arası, alt çizgi ile birbirinden ayrılır (Önünde
alt çizgi olanlar; önceden ayrılmış sistem adlarıdır). Bir örnek;
//:B04:Simple.h
//Yeniden tanımlamayı engelleyen basit bir başlık
#ifndef SIMPLE_H
#define SIMPLE_H
struct Simple{
int i,j,k ;
initialize( ){i=j=k=0;}
};
#endif //SIMPLE_H ///:~
Simple.h #endif deyiminden sonra iki bölü işareti ile bir yorum ile
sonlandırılmıştır. Bu yorum önişlemleyici tarafından gözardı edilir. Fakat
belgelendirme bakımından yararlıdır. Yukarıdaki önişlemleyici deyimleri,
eklemelerin yinelenmesini engeller, genellikle de include korumaları
olarak adlandırılırlar.
Başlıklardaki isimalanları
Kitabımız da yönerge kullanımları (using directives) hemen hemen bütün
cpp dosyaların da rastlanacak. Genellikle de aşağıdaki biçim de olacaktır;
using namespace std ;
std bütün standart C++ kütüphanesini saran isimalanıdır. Bu özel kullanım
yönergesi, standart C++ kütüphanesindeki isimlerin nitelik açıklamaları
olmadan kullanımını sağlar. Herhalde hiçbir zaman başlık dosyasında sanal
olarak kullanım yönergesi görmeyeceksiniz.(en azından, görüntü dışında
değil). Bunun nedeni; özel isimalanı korumasının yönerge kullanımı ile
ortadan kaldırılmasıdır ve etkisi o anki derleme biriminin bitişine kadar
sonlanır. Başlık dosyasına bir kullanım yönergesi konursa (görünümün
dışında) bunun anlamı; bu başlığı içeren herhangi bir dosya "isimalanı
koruması" nı kaybeder, bunun başlık dosyası başka başlıklar anlamına da
gelir. Böylece başlık dosyalarına kullanım yönergeleri ile başlarsanız,
pratik olarak heryerde isimalanları "kapanmalarını" sonlandırır bundan
dolayı da isimalanlarının yararlı etkilerini nötralize eder.
Kısaca; kullanım yönergelerini başlık dosyalarına koymayın!.
projelerde başlıkların kullanımı
C++ da bir proje inşa edilirken genelllikle çok sayıda değişik tip biraraya
getirilir (işlevleri ile beraber yapılar). Genellikle her tipin veya tip
gruplarının ayrı ayrı başlık dosyalarına bildirimleri konur, daha sonra da
çevrim
biriminde ilgili tipin işlevleri tanımlanır. Bu tiplerden herhangi biri
kullanılacak olursa o tipin başlık dosyası, bildirimlerin başarımı için
mutlaka eklenmelidir. Bazen bu kitapta takipedilecek tanım, -çoğunlukla
da
çok küçüktür, böylece herşey; yapı bildirimleri, işlev tanımları ve main( ) tek bir dosya da görünür. Aklınız da bulunsun; pratikte ayrı dosyalar ve
başlık dosyaları kullanacağız.
Yuvalanmış yapılar
Global isimalanının dışına işlev adlarını ve veriyi alabilme kolaylığı
yapıların künyelerini geliştirir. Bir yapı künyesini bir başka yapı
künyesinin içine yuvalayabilirsiniz, ve bundan dolayı ilintili ögeler
birarada tutulabilir. Bildirimin yazım biçimi yapıdan ne beklediğinize
göredir, örnekte aşağı inen bir yığıt uygulamasında basit bir ilişimli liste
hiçbir zaman
bellek dışında çalışamaz.
//:B04:Stack.h
//ilişimli listede yuvalanmış yapı
#ifndef STACK_H
#define STACK_H
struct Stack{
struct Link{
void* data ;
Link* next ;
void initialize(void* dat,Link* nxt);
}* head;
void initialize( );
void push(void* dat);
void* peek( );
void* pop( );
void cleanup( );
};
#endif; //STACK_H///:~
Link yuvalanmış structtır. Listedeki bir sonraki Link için göstergeç
bulunur. Linkte bulunan veri için de göstergeç vardır. Bir sonraki
göstergeç sıfır olduğunda, listenin sonundasınız demektir. struct Link
bildiriminden hemen sonra
head göstergeci tanımlanmıştır. Link* head ayrı tanımı yerine, C den
gelen bu yazım tarzı tercih edilmiştir. Yapı bildiriminden hemen sonraki
noktalı virgül, burada özellikle vurgulanmalı. Zira noktalı virgül,
virgüllerle ayrılarak bildirilmiş yapı tipinin, tanım listesini sonlandırır.
(genellikle de liste boştur.)
Yuvalanmış yapı kendi initialize( ) işlevine sahiptir, daha önceki bütün
başlatma işlevlerinde ki gibi bir görevi vardır.
Stack( ) ise hem initialize( ) hem de cleanup( ) işlevlerine sahiptir. push
( ) işlevi göstergeci verinin yerleştireleceği en alttaki boş yere gönderir,
pull( ) işlevi ise göstergeci verinin geri alınacağı en üstteki bellek alanına
yollar, veriyi alarak geri gelir ve alınan verinin yeri boş kalır (bir veriyi
pop( ) ettiğinizde, datanın gösterdiği nesnenin ortadan kaldırılmasından
siz sorumlusunuz.). peek( ) işlevi ise göstergeci verinin gözleneceği en üst
bellek alanına yollar fakat ögeyi Stack üzerinde bırakarak ayrılır. peek( ),
pop( ) gibi göstergeci aynı adrese yollar, fakat veriyi almaz yerinde bırakır,
gözlemde bulunur.
Üye işlev tanımları için bir örnek ;
//:B04:Stack.cpp {O}
//yuvalanma ile ilişimli liste
#include "Stack.h"
#include ".../require.h"
using namespace std ;
void
Stack::Link::initialize(void* dat,Link* nxt){
data=dat ;
next=nxt ;
}
void Stack::initialize( ){head=0; }
void Stack::push(void* dat) {
Link* newLink=new Link ;
newLink->initialize(dat,head) ;
head=newLink ;
}
void* Stack::peek( ){
require(head!=0,"Stack empty");
return head->data ;
}
void* Stack::pop( ){
if(head==0)return 0;
void* result=head->data;
Link* oldHead=head;
head=head->next;
delete oldHead;
return result;
}
void Stack::cleanup( ){
require{head==0,"Stack not empty"};
}///:~
İlk tanım yuvalanmış yapının bir üyesini tanımladığı için özellikle ilginçtir.
Görüntü duyarlılık eklerini kullanarak struct Stack::Link::initialize( )
değişkenleri alır ve onları üyelere atar.
Stack::initialize( ), head değerini sıfır yapar. O zaman nesnede, sahip
olduğu listenin boş olduğunu anlar.
Stack::push( ), değişkenin göstergeç değerini alır, Stacke, saklamak
istediğinizi koyar. Link için saklama yapma sırasında önce new kullanılır
ve en üste veri konur. Daha sonra Linkin initialize( ) işlevi çağrılarak
Link üyelerine uygun değerler atanır.next göstergeci o anki heade atanır,
daha sonra head yeni Link göstergecine atanır. Bu işlem de Linki listenin
tepesine gönderir.
Stack::pop( ) etkinliği data göstergecini o anki Stackin en tepesinde
yakalar ve daha sonra head göstergecini aşağı doğru iter ve Stack'in eski
tepe ögesini siler, ve en sonunda yakalanmış göstergece dönülür. pop( ) en
son ögeyi kaldırınca, daha sonra head tekrar sıfır değerini alır, bunun
anlamı Stack boş demektir.
Stack::cleanup( ) aslında temizlik işlemi yapmaz. Zira Stack ögelerinin
boşaltılıp silinmesinden siz sorumlusunuz.
Stack eğer boş değilse, ortaya çıkan programlama hataları için require( )
işlevi kullanılır.
Neden akıllı bir programcının yapmadığı pop( ) işlemleri yerine, Stack
yokedici (destructor( )) işlevi ögelerin ortadan kaldırılmasından sorumlu
olmadı?. Sorunun kaynağı Stack void göstergeçleridir. İlerleyen
bölümlerde öğreneceğiniz gibi delete çağrısı void için uygun temizliği
yerine getirememektedir. "Bellekten kimin sorumlu olduğu " konusu hala o
kadar kolay yanıtlanamamaktadır. İlerleyen bölümlerde başka ayrıntılarda
göreceğiz.
Stack sınaması için bir örnek aşağıda verilmiştir.
//:B04:StackTest.cpp
//{L} Stack
//{T} StackTest.cpp
//yuvalanmış ilişimli liste sınaması
#include "Stack.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main( int argc, char* argv[]){
requireArgs(argc,1); //dosya adı bir değişken
ifstream in(argv[1]);
assure(in,argv[1]);
Stack textlines;
textlines.initialize( );
string line;
//dosyayı oku satırları yığıta yükle
while(getline(in,line))
textlines.push(new string (line));
//yığıttan satırları al ve yazdır
string* s;
while((s=(string*)textlines.pop( ))!=0){
cout<<*s<<endl;
delete s;
}
textlines.cleanup( );
}///:~
Daha önceki örneğe çok benzemektedir, Stack üzerine dosyadan (string
göstergeci olarak) satırlar yerleştirir, ve daha sonra onları boşaltır ve bunun
sonucu da ters sıralamayla yazdırılır. pop( ) üye işlevi bir void* geri
döndürür, ve bu mutlaka kullanılmadan önce string* e geri döküm
yapılmak zorundadır. string yazımı için dayançtan kurtarılır
(dereferenced). textlines doldurulurken her push( ) için, line içeriği
kopyalanır, bu new string(line) ile sağlanır. Yeni-açıklamadan geri dönen
değer yeni string için göstergeçtir. Oluşturulan stringe, linedaki bilgiler
kopyalanır. push( ) işlevine line adresi aktarılmış olunsaydı, eşdeğer
adreslerle dolmuş Stack sonlandırılacaktı, ve hepsi line gösterecekti.
Kitabın ilerleyen bölümlerinde, "kopyalama"(cloning) süreci ile ilgili daha
fazla bilgi edineceksiniz.
Dosya adı komut satırından alınır. Komut satırında güvence sağlayan
yeterli sayıda değişken bulunur, require.h başlık dosyasında kullanılan
ikinci bir işlev daha vardır; istenen sayıdaki değişken ile argc yi
karşılaştıran requireArgs( ) .
Yeterli değişken olmadığında, uygun hata iletisi yazdırır ve programdan
çıkarır.
Global görüntü çözünürlüğü
Görüntü duyarlık işleci (::), istenen ada rastlanmadığında, derleyici
varsayılanı (adına en yakın olanı) seçip, bulunulan durumdan kurtarır.
Örnek olarak yörel belirteci a olan bir yapıyı ele alalım, üye işlevlerden
birinin içinden ise global belirteç a istensin. Derleyici varsayılan olarak
önce yörel olanı seçeceği için, mutlaka öbürü olduğu açıklanmalıdır.
Global bir ad görüntü çözünürlüğü ile belirtilmek istendiği zaman, önünde
herhangi bir işleç bulunması gerekmez. Aşağıdaki örnek hem işlev hem de
değişken için global görüntü çözünürlülüğünü gösterecektir.
//:B04:Scoperes.cpp
//global görüntü çözünürlüğü
int a ;
void f( ){ }
struct S{
int a ;
void f( ) ;
}
void S::f( ){
::f( );
::a++;
a-- ;
}
//kendini çağırma öbür türlü
//global a seç
//struct içinde ki a
int main( ){
Ss;
f( ) ;
}///:~
S::f( ) görüntü çözünürlüğü olmasaydı, derleyici f( ) ve a nın varsayılan
üye sürümlerini seçerdi.
Özet
Bu bölüm de, C++ nın temel kıvraklığı olan işlev yerleşimi öğrenildi. Yeni
bir yapı tipi anlatıldı. Anılan yeni yapı tipi soyut veri tipidir; bu yapıyı
kullanarak oluşturulan değişkenlere, o tipin nesnesi veya unsuru denilir.
Nesne için üye işlev çağırma, o nesneye ileti göndermektir. Nesneye ileti
gönderme nesne yönelimli programlamanın temel etkinliğidir. İşlev ve
verilerin birarada düzenlenmesi kod yönetimi için çok uygundur.
Kütüphane kullanımını daha da kolaylaştırır, zira isimler saklanarak isim
çatışmaları engellenir. Herşeye rağmen C++ da programı daha güvenli
kılmak için yapılacak daha çok şey var. Bir sonraki bölüm de, struct'ta
sadece işlem yapılan üyelerin nasıl korunacağı anlatılacak. Bu da yapı
kullanıcısının değiştirebilecekleri ile sadece programcının değiştirecekleri
arasındaki sınırları açıklığa kavuşturacaktır.
5. BÖLÜM
Verilerin saklanması
Tipik bir C kütüphanesinde struct vardır, ve ilgili işlevleri bu struct
üzerinde etkinlikte bulunur. Şimdiye kadar
temel olarak C++ da ilgili işlevlerin nasıl yer aldığı, ve struct görüntüsüne
işlev bildirimleri konarak
nasıl ilişkilendirildiği anlatıldı. Ayrıca struct'ın çağırdığı işlevlerin,
çağrım yolunu değiştirmek, ilk değişken olarak
künyenin adres aktarımından vazgeçmek, ve programa yeni bir tip adı
eklemekte (böylece struct eki için, typedef oluşturmak zorunda
değilsiniz.) anlatılanlar arasındadır.
Bunların hepsi kodları düzenlemenize, onları daha kolay yazmanıza ve
okumanıza yardımcı olurlar. C++ da kütüphaneler oluşturulurken, başka
önemli unsurlar da gözönüne alınmalıdır; özellikle de güvenlik ve denetim
bakımından. Bu bölümde künye (=yapı=structure) içinde bulunan sınırlar
konusu incelenecek.
Sınırların düzenlenmesi
Herhangi bir ilişkide, tarafların ortak olduğu sınırlar önemlidir. Bir
kütüphane oluştururken, uygulama geliştirmek amacı ile kütüphaneyi
kullanan, veya başka bir kütüphane geliştiren müşteri programcı ile
bağlantı kurulur.
C de, struct kullanımı için belli kurallar yoktur. Müşteri programcılar
struct ile istedikleri her şeyi yapabilirler, ve zaten özel bir davranışı
geliştirmek için de, başka bir yol yoktur. Örnek olarak; son bölümde
initialize( ) ve cleanup( ) işlevlerinin önemini gördünüz, buna rağmen
müşteri programcı bu işlevleri çağırmayabilir.(bir sonraki bölümde daha
iyi bir yaklaşım incelenecektir.). Hatta siz gerçekten tercih etmeseniz bile,
müşteri programcı struct'ın üyelerinin bazılarını, C de doğrudan
işlemlemeyebilir, ve bunu da önleyecek herhangi bir şey yoktur. Herşey
gözümüzün önünde olur.
Üyelere erişimi denetlemek için iki neden vardır. Bunların ilki; müşteri
programcının elinden, dokunmaması gereken, veri tipinin iç çalışmasını
sağlayan gerekli araçları almak. Burada şunu belirtelim; bunlar müşteri
programcının özel sorunlarını çözen arayüzün parçası değildirler. Aslında
bu programcıya verilen bir hizmettir, zira onlar kendileri için neyin önemli
olduğunu, neyin gözardı edileceğini kolayca bilirler.
Erişim denetiminin ikinci nedeni;künyenin iç işleyişinde, müşteri
programcıyı nasıl etkileyeceği hiç düşünülmeden kolaylıkla değişiklik
yapılmasına izin vermesidir. Son bölümde yer alan Stack örneğinde, her
ögenin eklenmesi sırasında yeni bir bellek alanı kullanımı yerine, hız
için,saklama da daha büyük bellek bölümleri kullanılabilir. Bunu
başarmak, arayüz ve veri üyeleri yerleşiminin, açıkça ayrılması ve
korunması ile sağlanır. Müşteri programcı tarafından sadece yeniden ilişim
istenebilir.
C++ da erişim denetimi
C++ da üç yeni anahtar kelime ile künyedeki sınırları açıklayan erişim
denetimi sağlanır; public, private ve protected.
Bunların kullanımları ve anlamları oldukça basittir. Bu erişim açıklayıcılar,
sadece künye bildirimlerinde kullanılırlar.
Ve bunlar kendilerini takip eden bildirimlerin tamamının sınırlarını
değiştirirler. Erişim açıklayıcı anahtar kelimelerden birinin kullanımından
hemen sonra mutlaka üstüste iki nokta, noktalama işareti kullanılır.
public anahtar kelimesi, kendisini takip eden bütün üyelerin herkese açık
olduğunu gösterir. public üyeler aynı struct üyeler gibidir. Örnek olarak
aşağıdaki struct bildirimleri özdeştir;
//:B04:Public.cpp
//Public C deki struct gibidir
struct A{
int i ;
char j ;
float f ;
void func( ) ;
};
void A::func( ){}
struct B{
public:
int i ;
char j ;
float f ;
void func( ) ;
};
void B::func( ){ }
int main( ){
A a; B b;
a.i=b.i=1;
a.j=b.j='c' ;
a.f=b.f=3.14159;
a.func( );
b.func( );
}///:~
Öte yandan private anahtar kelimesi, tipin oluşturucusu haricinde, yani
tipin kendi işlevleri dışında hiç kimsenin o üyeye ulaşamayacağı anlamına
gelir. private, müşteri programcı ile tipin oluşturucusu arasında ki beton
duvardır. Dışardan birisi private üyeye erişmek isterse, derleme sırasında
hata oluşur. Yukarıdaki örnekte struct B de gösterilen bazı bölümleri
saklayabilirsiniz, ve bu bölümlere sadece struct B içindeki işlevler erişir.
//:B04:Private.cpp
//sınırları ayarlama
struct B{
private:
char j;
float f;
public:
int i;
void func( );
};
void B::func( ){
i=0;
j='0';
f=0.0;
};
int main( ){
B b;
b.i=1;
//! b.j='1'; //geçersiz private
//! b.f=1.0; //geçersiz private
}///:~
func( ) işlevi B nin bütün ögelerine erişebilir(zira func( ) işlevi B nin bir
ögesidir.), fakat global bir işlev olan main( ) işlevi erişemez. Öteki künye
üye işlevlerine erişememesi de doğaldır. Sadece künye bildiriminde açıkça
bildirilen üye işlevler private üyelere erişebilir.
Erişim açıklayıcı anahtar kelimeler için yazımda bir sıra yoktur, dilenirse
herbiri birden fazla kullanılabilir. Bu anahtar kelimeler kendilerini takip
eden bütün üyeleri etkilerler, ta ki yeni anahtar kelimeye rastlayana kadar.
protected
Sonuncu erişimi açıklayıcı anahtar kelime protecteddır.protected aynı
private özellikleri gösterir, yalnız şu ana kadar ayrıntıları pek anlatılmayan
"kalıtım" künyelerinin erişim hakları hariç. Kalıtım yolu ile türetilmiş
üyeler protected üyelere erişebilirler. 14. bölümde kalıtım anlatıldıktan
sonra bu konu daha açık hale gelecek. Şimdilik protected anahtar
kelimesini, private olarak algılamak yeterlidir.
Friend'ler
Künye üyesi olmayan bir işleve erişim hakkını elde etmek istersek ne
yapılabilir?. Bu, o işlevi künye içinde friend olarak bildirirerek olur.
friend bildiriminin künye içinde yer alması çok önemlidir, zira derleyici
(ve sizde)
künye bildirimini mutlaka okuyabilmelidir ve veri tipinin davranış ve
boyutu ile ilgili bilgileri görebilmelidir. Herhangi bir ilişkideki en önemli
kural: "benim özel uygulamama kim erişebilir?." Sınıf denetimleri ile
kodlar üyelere erişir. friend değilseniz, dışarıdan içerideki üyeye erişmek
için sihirli bir yol yoktur. Yeni bir sınıfı "hey ben ahmet'in arkadaşıyım "
"hamili kart yakinim olur" diye bildiremezsiniz, zira Ahmet'in private ve
protected üyelerini görebilmelisiniz. Bir global işlevi friend olarak
bildirebilirsiniz, ve ayrıca başka bir künyenin üye işlevi olarak da
bildirebilirsiniz, ve hatta bütün künyeye friend olarak bildirebilirsiniz. İşte
bir örnek;
//:B05:Friend.cpp
//Friend özel erişim yeteneğine sahiptir
//Bildirim (tamamlanmamış tip belirtimi):
struct X;
struct Y{
void f(X*);
};
struct X{//tanım
private:
int i;
public:
void initialize( );
friend void g(X*,int); //global friend
friend void Y::f(X*); //struct üyesi friend
friend struct Z;
//Bütün structa friend
friend void h( );
};
void X::initialize( ){
int i;
}
void g(X* x,int i){
x->i=i;
}
void Y::f(X* x){
x->i=48;
}
struct Z{
private:
int j;
public:
void initialize( );
void g(X* x);
};
void Z::initialize( ){
j=99;
}
void Z::g(X* x){
x->i +=j;
}
void h( ){
X x;
x.i=100; //doğrudan veri işlemi
}
int main( ){
X x;
Z z;
z.g(&x);
}///:~
struct Y nin üye işlevi f( ), X tipinin nesnesini değişikliğe uğratır.
Derleyici, kullanılmadan önce herşeyin bildirimini istediği için burada bir
kelime oyunu yapıldı. Bu nedenle struct Y, struct X te friend olarak
bulunan üyesi Y::f(X*)
den önce mutlaka bildirilmek zorundadır. Y::f(X*) in bildirilebilmesi
içinde önce, struct X bildirilmek zorundadır.
Çözüm burada ; Y::f(X*), X nesnesinin adresini alıyor. Buranın hassas
nokta olması, derleyicinin her zaman sabit büyüklükteki adresi
aktarabilmeyi bilmesidir. Aktarılacak nesnenin ne olduğuna bakılmaksızın
yapılan bu işlem, tip büyüklüğü ile ilgili tam bilgi olmasa bile, derleyici
tarafından yerine getirilir. Bütün nesneyi aktarmayı denerseniz,
derleyici X künye tanımının tamamını görmek zorundadır, zira Y::g(X)
biçimindeki bir işlevin bildiriminden önce
derleyici büyüklüğü bilip nasıl aktarılacağını bulmalıdır. X in adresini
aktararak, derleyici Y::f(X*) ten önce, X in "tamamlanmamış tip
belirtimine" izin verir. Bu aşağıdaki yazımla başarılır;
struct X;
Yukarıdaki bildirim, basitçe bu adda bir struct olduğunu derleyiciye
duyurur. Bu noktaya kadar herşey tamamdır, isimden fazla bir şeye
şimdilik gereksinim duyulmaz .
Şimdi, struct X te Y::f(X*) sorunsuz bir şekilde friend olarak
bildirilebilir. Eğer o Y nin tam tanımından önce bildirilmiş olsaydı,
derleyici hata iletisi üretirdi. Bu tutarlılığı sağlayan ve dolayısı ile hatalara
imkan vermeyen güvenlik özelliğidir.
Öbür iki friend işlevi inceleyelim. Bunlardan ilki; g( ) global işlevi, friend
olarak bildirilmiştir. Fakat g( ), önceden global ortamda bildirimde
bulunulmamıştı. Bu yolla aynı anda, işlev hem bildirimde bulunulmuş olur,
hem de friend olarak duyurulur. Bu yolu bütün künye içinde
genişletebiliriz;
friend struct Z;
Z henüz tanımlanmamış tip belirtimi olup Z künyesinin tamamını friend
seviyesine getirir.
Yuvalanmış friendler
Yuvalanmış künye oluşturmak private üyelere erişimi otomatik olarak
sağlamaz. Bunu başarmak için özel bir yol takip edilmelidir; önce
yuvalanmış künye bildirilmeli( tanımlamaksızın), daha sonra friend
bildirilir, ve son olarak künye tanımlanmalı. Künye tanımı mutlaka friend
bildiriminden ayrı olmalıdır, aksi takdirde derleyici tarafından üyesi
olmadığı kabul edilir. İşte bir örnek;
//:B05:NestFriend.cpp
//Yuvalanmış friendler
#include <iostream>
#include <cstring> //memset( )
using namespace std;
const int sz=20;
struct holder{
private:
int a[sz];
public:
void initialize( );
struct Pointer( );
friend Pointer( );
struct Pointer{
private:
Holder* h;
int* p;
public:
void initialize(Holder* h);
//dizi etrafında dolaş
void next( );
void previous( );
void top( );
void end( );
//erişim değerleri
int read( );
void set(int i);
};
};
void Holder::initialize( ){
memset(a,0,sz* sizeof(int));
}
void Holder::Pointer::initialize(Holder* rv){
h=rv;
p=rv->a;
}
void Holder::Pointer::next( ){
if(p<&(h->a[sz-1]))p++;
}
void Holder::Pointer::previous( ){
if(p>&(h->a[0]))p--;
}
void Holder::Pointer::top( ){
p=&(h->a[0]);
}
void Holder::Pointer::end( ){
p=&(h->a[sz-1]);
}
void Holder::Pointer::read( ){
return *p;
}
void Holder::Pointer::set(int i){
*p=i;
}
int main( ){
Holder h;
Holder::Pointer hp,hp2;
int i;
h.initialize( );
hp.initialize(&h);
hp2.initialize(&h);
for(i=0;i<sz;i++){
hp.set(i);
hp.next( );
}
hp.top( );
hp2.end( );
for(i=0;i<sz;i++){
cout<<"hp= "<<hp.read( )
<<", hp2= "<<hp2.read( )<<endl;
hp.next( );
hp2.previous( );
}
}///:~
Pointer birkez bildirildikten sonra, Holder'ın private üyelerine erişmek
için aşağıdaki bidirim yeterlidir.
friend Pointer;
struct Holder, int dizilerini ihtiva eder, ve Pointer onlara erişmenizi
sağlar. Zira Pointer güçlü bir şekilde struct Holder ile ilişkilendirilmiş, ve
Holder üye künyesini algılayabilir olmuştur. Fakat Pointer, Holder
sınfından ayrı bir sınıf olduğu için, main( ) işlevinde onlardan birinin
yapabileceğinden çok daha fazla iş yapabilir, dizinin farklı bölümlerini
seçmek için onları kullanabilirsiniz. Pointer ham C göstergecine karşılık
gelen bir künyedir, bu sayede herzaman Holder içini işaret etmeyi
emniyete alırsınız.
Yukarıdaki programda Standart C işlevliğinden memset( ) (<cstring> de
yeralır) program gereği kullanılmıştır.
Bu, bütün belleği belli bir adrese(ilk değişken) belli bir değer atayarak
(ikinci değişken) başlatıp ayarlar, bu işlemi başlangıç adresinden itibaren n
bayt sürdürür(n üçüncü değişkendir). Tabii dilerseniz kendiniz yineleme
döngüsü kurgulayıp aynı işlevi oluşturabilirsiniz, fakat denenmiş memset
( ) gibi bir işlev tercih edilmelidir. Zira aynı işlevi kendiniz oluşturmaya
kalkarsanız hata üretebilirsiniz. Büyük olasılıkla memset( ) sizin
işlevinizden daha sağlıklıdır.
Arıtılmışmı ?
Sınıf tanımı her türlü ayrıntıyı verir. Bu sayede sınıfa bakarak, sınıfın
private kısımlarını değişikliğe uğratma izni olan işlevleri görebiliriz.
İşlevin biri friend ise bu onun üye olmadığını gösterir, fakat ona bir
şekilde private verileri değiştirme izni verebilirsiniz, ve mutlaka herkesin
görebileceği şekilde sınıf tanımında imtiyazlı işlev olarak listeye
alınmalıdır.
C++ bileşik nesne yönelimli dildir, yani arı dil değildir, friend aniden
ortaya çıkan uygulama sorunlarını çözmek için eklenmişti. Burada şunu
belirtelim; bu ekleme C++ dilini daha az saf yapmıştır, ama iyi ki daha az
saftır, C++
aslında pragmatik olmalıdır, yoksa sorunlar kolayca çözümlenemezdi, zira
ideal soyut yapılar gerçek hayatın sorunlarına çözüm getiremezler.
Nesne Şeması
4. bölüm de C derleyicisi için struct yapısı anlatıldı, ve daha sonra C++ ile
derlendiğinde değişmedi. Bu temel olarak struct nesne şemasına denk
gelir, yani her değişkenin saklama yerinin, nesnenin bellek yerleşimin de
ayarlanmasıdır.
C++ derleyicisi C struct şemasını değiştirseydi, o zaman yazılmış olan C
kodları struct değişkenlerinin yerbilgileri avantajları ortadan kalkardı.
Erişim belirteçlerini kullanmaya başladığınız da C++ gerçeği ile
karşılaştınız, artık bazı şeyler biraz değişecek demektir. "Erişim
bütünlüğü" taşıyan özel bölümde değişkenler aynı C de olduğu gibi ardışık
olarak bulunur. "Erişim bütünlükleri" nesnelerde ise, daha önce
bildirildikleri sırada görünmeyebilir. Derleyici genellikle erişim
bütünlüklerini aynı görüldüğü gibi işlemlemesine rağmen, bununla ilgili
genel bir kural yoktur, zira özel işlemci mimarisi veya işletim sistemi
private ve protected için açık destek sağlar, ki bunlar özel bellek
alanlarını kullanırlar. Programlama dilinin özellikleri bu avantajı
sınırlamak istemez.
Erişim belirteçleri künyenin bir bölümüdür, ve künye tarafından
oluşturulan nesneleri etkilemezler. Erişim özellik bilgileri program
çalışmaya başlamadan önce, ve genellikle de derleme sırasında ortadan
kaybolur. Program çalışırken nesneler "bellek yerinden" başka bir şey
değildirler. Eğer gerçekten isterseniz kuralları yıkıp belleğe doğrudan
erişebilirsiniz, aynı C de olduğu gibi. C++ tasarımı sizin aptalca işler
yapmanızı engelleyecek şekilde yapılmamıştır. Sadece size çok daha kolay
ve uygun seçenekler sunmak için tasarlanmıştır.
Program yazarken uygulamaya özgü herhangi bir şeye bağlı olmak,
genellikle iyi fikir değildir.
Uygulamaya özgü bağımlılıklar zorunlu olarak ortaya çıkarsa, onlar
künyede sarmalanarak tutulur, böylece aktarım değişiklikleri belli bir
noktaya yöneltilmiş olur.
Sınıf
Erişim denetimi çoğunlukla yapılacakların saklanması demektir. İşlevlerin
içinde bulunduğu künyeler, davranış
ve karakteristik özellikleri olan veri tipi üretirler, erişim denetimi veri tipi
içindeki sınırları belirler. Bu, iki nedenden yapılır; ilki müşteri
programcının neyi kullanıp neyi kullanamayacağını belirlemektir. Künyeye
kendi iç mekanizmalarınızı kurarsınız, ve müşteri programcı bu
mekanizmaları kullanacakları arayüzün parçası olarak düşünür.
Bu da doğrudan ikinci nedeni besler; arayüzü yerine getirilenlerden ayırma
nedeni. Künye bir program topluluğu tarafından kullanılırsa, müşteri
programcı başka birşey yapamaz sadece public arayüze iletiler gönderir.
Daha sonra kodlarında değişiklik yapmaksızın private olan herhangi bir
şeyi değiştirebilirsiniz.
Sarmalama ve erişim denetimi, bir C structından daha fazla şey ortaya
koymuşlardır. Şimdi artık nesne yönelimli programlamanın dünyasındayız,
künye; aynı sizin kuşların sınıfı ya da balıkların sınıfı gibi tanımladığınız
nesneler sınıfıdır.Sınıfa ait herhangi bir nesne, sınıfın karakteristiklerini ve
davranışlarını taşır. Künye bildirimi, bir tipin bütün nesnelerinin
görünümünü ve davranışını tanımlamanın yoludur.
Esas NYP programlama dili olan Simula-67 de, class anahtar kelimesi yeni
veri tip tanımlaması için kullanılmıştı.
Bu Stroustrup'a C++ da, class anahtar kelimesini kullanma ilhamı vermiş,
bütün dilin odak noktası olduğunu vurgulamasına yolaçmıştı; yeni veri
tipleri oluşturma, işlevleri ile birlikte C structlarından daha fazla olur. Bu
mutlaka yeni bir anahtar kelime için bir hak gibi görünmektedir.
Fakat C++ da class anahtar kelimesinin kullanımı sanki gereksiz gelir. Zira
class structla birisi hariç her yönden özdeştir; classın varsayılanları
private, structın varsayılanları publictir. Aşağıda aynı neticeyi veren iki
künye verilmiştir;
//:B05:Class.cpp
//class ve struct benzerlikleri
struct A{
private:
int i,j,k;
public:
int f( );
void g( );
};
int A::f( ){
return i+j+k;
}
void A::g( ){
i=j=k=0;
}
//Aşağıdaki ile aynı neticeler ortaya çıkar.
class B{
int i,j,k;
public:
int f( );
void g( );
};
int B::f( ){
return i+j+k;
}
void B::g( ){
i=j=k=0;
}
int main( ){
A a;
B b;
a.f( ); a.g( );
b.f( ); b.g( );
}///:~
C++ da class NYPnin temel unsurudur.Bu kitapta artık sınıf anlamına
gelen class anahtar kelimesini sıksık kalın harflerle ifadeyi gereksiz
görüyorum, zaten ikide bir bunu açıklamakta rahatsız edici bir
şey.Sanıyorum Stroustrup
struct anahtar kelimesinden kurtulmak istemiş ve classı tercih etmişti,
C++ da hala structın kalmış olması C++ nın
geriye uyumunu devam ettirmek olsa gerek.
İnsanların çoğunluğu sınıfları oluştururken bir tarz tercihinde bulunurlar;
bu da, sınıf benzerinden ziyade structa yakındır.Bunun nedeni; sınıftaki
varsayılan private üzerine yazıp, sınıfın davranışlarını public olarak
başlatmak içindir.
class X{
public:
void interface_function( );
private:
void private_function( );
int internal_representation;
};
Bunun arkasındaki mantık; okuyucuya ilginç üyeleri daha anlamlı kılmak,
ve böylece private olduğunu söyleyen herhangi bir şeyi daha sonra
gözardı edebilmektir. Bunun yanında, öteki üyelerin sınıf içinde
bildirilmesinin nedeni; derleyicinin nesne büyüklüğünü öğrenmesi ve
onları belleğe doğru yerleştirmesi ve tutarlılığı emniyete almak içindir.
Bundan sonra, kitabımızda ki örnekler de private üyeleri önce bildireceğiz
class X{
void private_function( );
int internal_representation;
public:
void interface_function( );
}
Bazılarıda kendi private üyelerinin adlarını düzenlerken sorunlar
çıkarırlar;
class Y{
public:
void f( );
private:
int mX; //"kendinden-düzenlemeli" isim
};
Y nin görünümün de mX daha önceden saklı olduğu için, m (üyelik için)
gereksizdir. Çok sayıda global değişken içeren
projelerde, üye işlev tanımlarında hangi verinin global, hangisi üye
ayırmak oldukça yararlıdır.
Erişim denetimi için Stash değişimi
4. bölümdeki örnekleri alıp onlarda class ve erişim denetimi kullanmak
konuyu anlamayı kolaylaştırır. Bu arada
müşteri programcının arayüz parçası belirgin biçimde farkedilir. Böylece
müşteri programcının kazara, sınıfın dokunulmaz bölümlerine ulaşması
olanağı kalmaz.
//:B05:Stash.h
//erişim denetimi kullanım çevrimi
# ifndef STASH_H
#define STASH_H
clas Stash{
int size;
//her birimin büyüklüğü
int quantity; //saklama alanı sayısı
int next;
//sonraki boş yer
//dinamik olarak yerleştirilen bayt dizileri
unsigned char* storage;
void inflate(int increase);
public:
void initialize(int size);
void cleanup( );
int add(void* element);
void* fetch(int index);
int count( );
};
#endif //STASH_H///:~
inflate( ) işlevi sadece add( ) işlevi tarafından kullanıldığı için, ve
arayüzün değil kullanımın (=yerine getirilenler) parçası olduğundan
private olarak bildirilmiştir. Bu daha sonra, altı çizili yerine getirilenler
bellek yönetimin de farklı sistem kullanmak için değiştirilebilir.
include dosyasının adından başka, yukarıdaki başlık, örneğimizde
değiştirilebilen tek şeydir. Uygulama dosyası ve sınama dosyasının her
ikisi de aynıdır.
Erişim denetimi kullanarak Stack değişimi
İkinci örnek olarak Stack class'a çevrilecek. Şimdi yuvalanmış veri yapısı
private olur, ve müşteri programcı ne Stack'in iç yapısına bağlıdır, ne de
bakmak zorundadır.
//:B05:Stack2.h
//ilişimlenmiş listelerle yuvalanmış structlar
#ifndef STACK2_H
#define STACK2_H
class Stack{
struct Link{
void* data;
Link* next;
void initialize(void* dat, Link* nxt);
}* head;
public:
void initialize( );
void push(void* dat);
void* peek( );
void* pop( );
void cleanup( );
};
#endif //STACK2_H///:~
Önceden olduğu gibi uygulama değişmedi, böylece yinelenmedi. Sınama
da aynısıdır. Değişen tek şey class
arayüzünün sağlamlığıdır. Erişim denetiminin gerçek değeri, geliştirme
sırasında sınırlardan geçmeyi önlemesidir. Aslında derleyici class
üyelerinin koruma düzeylerini bilen tek araçtır. İlişimciye aktarılan üye
adına eklenmiş erişim denetimi bilgisi yoktur. Bütün koruma denetimi
işlemleri derleyici tarafından yerine getirilir, ve programın çalışması
sırasında ortadan kalkar.
Burada göze çarpan başka birşey de, müşteri programcının arayüz olarak
kullandıklarının gerçekten aşağılarda yığılmasıdır. İlişimlenmiş listeler
olarak uygulanırlar ve siz bunları, müşteri programcının onlarla iletişimini
etkilemeksizin değiştirebilirsiniz, ya da (daha da önemlisi) tek satırlık
müşteri kodu kullanırsınız.
Aracı sınıflar
C++ da bulunan erişim denetimi arayüzü verilerden ayırır, fakat
uygulamanın bir kısmı gizli kalır. Derleyici
nesneyi doğru oluşturmak ve işlemlemek için, mutlaka hala nesnenin bütün
parçalarının bildirimlerini görmek zorundadır. Şöyle bir programlama dili
düşünün; sadece nesnenin public arayüzü gereksin ve private uygulama
saklı kalsın, fakat C++ da ise, tip denetimi mümkün olduğunca statik
olarak derleme sırasında yapılır. Bunun anlamı da; bir hata olduğunda
mümkün olan en kısa sürede öğrenilecek demektir. Başka bir anlamı da;
program veriminin artacağıdır. Ama private gizlemesi iki etki yaratır;
erişemeseniz bile görebilirsiniz, ve yeniden derlemeye ihtiyaç bırakmaz.
verilerin saklanması
Bazı projelerde müşteriye,bir kısım uygulamalar gösterilmez. Bunlar
kütüphane başlık dosyasında bulunan, rakiplerin eline geçmesi istenmeyen
stratejik bilgilerdir. Güvenlikle ilgili bir sistem üzerinde- örnek;şifreleme
akışları- çalışılıyor olabilir, bu nedenle kodların kırılmasına neden
olabilecek başlık dosyalarındaki ipuçları gösterilmek istenmez. Veya
kütüphanelerinizi "düşman" eline verebilirsiniz, programcılar bir şekilde
göstergeçleri ve dökümleri kullanarak, private unsurlara doğrudan
erişirler. Bütün bu durumlarda başlık dosyasında bulundurmaktan ziyade,
gerçek künyenin uygulama dosyasında derlenmiş olarak bulunması daha
değerlidir.
Yeniden derlemeyi azaltma
Bir dosya değiştirilirse, programlamada yer alan yönetici bu dosyayı
yeniden derler, veya ilgili dosyaya bağlı başka bir dosya- ekli bulunan
başlık dosyası- değişirse de yeniden derleme yapılır. Bunun anlamı,
class'ta ya public arayüz veya private üye bildirimlerinde bir değişiklik
yapılırsa, başlık dosyalarının yeniden derlenmesi zorunluluk olur. Buna
kırılgan ana-sınıf sorunu denir. Çok büyük projelerin başlangıç
aşamalarında böyle bir durum kolaylıkla yönetilemez, zira önemli
uygulamalar sıklıkla değişir, derleme için harcanan zaman hızlı geri
dönüşü, yani netice almayı engeller.
Sorunu çözümlemek için kullanılan tekniğin adı; aracı sınıflar veya "van
kedisi" dir. Uygulamayla ilgili herşey- bir göstergeç hariç "gülümse"-
ortadan kaybolur. Göstergecin dayanak aldığı künyenin tanımı, bütün üye
işlevlerin tanımları ile beraber uygulama dosyasındadır. Böylece arayüz
değişmediği sürece, başlık dosyası değişmez. Uygulama istendiğinde
değiştirlir, ve sadece uygulama dosyası yeniden derlemeye ve proje ile
yeniden ilişim kurmaya gerek duyar.
Aşağıda, tekniği gösteren basit bir örnek verilmiştir. Başlık dosyasında
sadece public arayüz vardır, ve eksik tanımlanmış sınıfın bir göstergeci
bulunur.
//:B05:Handle.h
//Aracı sınıflar
#ifndef HANDLE_H
#define HANDLE_H
class Handle{
struct Van; //sadece sınıf bildirimi
Van* smile;
public:
initialize( );
void cleanup( );
int read( );
void change(int);
};
#endif //HANDLE_H///:~
Bunlar bütün müşteri programcıların görebildikleridir. Aşağıdaki satır;
struct Van;
eksik tip belirtimi veya sınıf bildirimidir. (bir sınıf tanımı, sınıf gövdesidir
unutmayın). Derleyiciye Van'ın künye adı olduğunu bildirir, struct
ayrıntıları ile ilgili bilgi vermez. Bu dahi structı gösteren bir göstergeç için
yeterli bilgidir. Fakat struct gövdesi yerleştirilene kadar nesne
oluşturulamaz. Bu teknikte künye gövdesi uygulama dosyasında görünmez
olur;
//:B05:Hidden.cpp {O}
//Aracı uygulaması
#include "Handle.h"
#include ".../require.h"
//aracı uygulamasının tanımı
struct Handle::Van{
int i;
};
void Handle::initialize( ){
smile=new Van;
smile->i=0;
}
void Handle::cleanup( ){
delete smile;
}
int Handle::read( ){
return smile->i;
}
void Handle::change(int x){
smile->i=x;
}///:~
Van yuvalanmış bir künyedir, o nedenle mutlaka görüntü duyarlılığı ile
tanımlanmalıdır;
struct handle::Van{
Handle::initialize(,) da saklama Van künyesinin yerleşimi için yapılır, ve
Handle::cleanup( ) ta ise saklananlar silinirler. Bu bellek, sınıfın private
üyelerinin konduğu yer olan bütün veri üyelerine karşılık olarak kullanılır.
Handle.cpp derlendiği zaman, onun künye tanımı hedef dosyada
görünmez olur ve onu kimse göremez. Van'ın ögeleri değişirse yeniden
derlenmesi zorunlu tek dosya Handle.cpp dir, zira başlık dosyası
değişmemiştir.
Handle kullanımı öteki sınıf dosyalarının kullanımı gibidir; başlığı ekle,
nesneleri oluştur, ve iletileri gönder.
//:B05:UseHandle.cpp
//{L} Aracı
//Handle sınıfının kullanımı
#include "Handle.h"
int main( ){
Handle u;
u.initialize( );
u.read( );
u.change(1);
u.cleanup( );
}///:~
Müşteri programcının erişebileceği tek yer, değişen tek unsur uygulama
olduğu sürece, sadece public arayüzdür.O zaman da, yukarıda ki dosyanın
yeniden derlenmesi hiçbir zaman gerekmez. Böylece, bu mükemmel bir
veri gizlemesi olmamasına rağmen, büyük bir ilerlemedir.
ÖZET
C++ da erişim belirteçleri, sınıf geliştirene çok değerli bir denetim olanağı
sağlar. Sınıf kullanıcıları açık biçimde neyi kullanacaklarını ve neyi
gözardı edeceklerini görebilirler. Daha da önemlisi müşteri programcı
sınıfın önemli verilerinin değişiminden etkilenmez. Bunu sınıf geliştiren
olarak bilirseniz, esas verileri müşteri programcı etkilenmeden
değiştirebilirsiniz, zira sınıfın o bölümlerine müşteri programcı erişemez.
Esas verileri değiştirme yeteneğine sahip olduğunuz zaman, ilerde
programı daha da geliştirmenizin yanında, hata yapma özgürlüğüne de
sahip olursunuz. Ne kadar planlı ve ne kadar iyi tasarım yaptığınız o kadar
önemli değildir, hata yapabilirsiniz. Hatalarla ilgili olarak göreceli olarak
güvende olmayı bilme; daha deneysel, daha hızlı öğrenme ve projenin daha
çabuk bitmesi demektir.
Müşteri programcı, bir sınıfın public arayüzünü görebilir, bu kısım sınıfın
doğru çözümlenmesi ve tasarımlanması
gereken en önemli bölümüdür. Fakat hatta değişim için sizi mecbur
bırakabilir. Eğer ilk seferde doğru arayüz elde edilemezse, daha fazla işlev
eklenebilir, ve kaldırılmadıkları sürece de müşteri programcı tarafından
kendi kodların da kullanılabilirler.
Çalışma soruları
1-public, private ve protected veri üyeleri ve üye işlevleri bulunan bir
sınıf oluşturun. Bu sınıftan bir nesne yapın.
Bütün sınıf üyelerine erişmeye çalıştığınız zaman derleyicinin ürettiği ileti
çeşitlerini inceleyiniz.
2-Adı Lib olan ve içinde adı a, b, ve c olan string nesnelerini içeren bir
struct yazınız.
6. BÖLÜM
İlk değer ataması (=başlatma) ve silme
4. bölümde, tipik bir C kütüphanesinde bulunan parçaların tamamını alıp
onları künyede sarmalama ile, kütüphane kullanımında önemli bir ilerleme
sağlanmıştı ( soyut veri tipi bundan sonra class olarak adlandırılacaktır).
Bu sadece, kütüphane bölümüne girişte bilinen tek unsur değildir, ayrıca
işlev adları da sınıf içinde saklanır.
5.bölüm de ise erişim denetimi (verilerin saklanması) anlatıldı. Bu, sınıf
tasarımcısına müşteri programcıya neleri işlemleyeceği ile nelere
işlemleyemeceği arasındaki sınırları, açıkça belirleme olanağını verir. Yani
veri tipinin işlem iç mekanizmaları sınıf tasarımcısının takdir ve denetimi
altındadır, ve böylece müşteri programcı hangi üyeleri
dikkate alabilir ve alması lazım gelir açıkça bilir.
Sarmalama ve erişim denetimi beraberce, kütüphane kullanımını
kolaylaştırırlar. "Yeni veri tipi" fikrinin sağladığı kolaylıklar, C dilinde
bulunan yerleşik veri tiplerine göre daha fazladır. Şimdi ise C++
derleyicisi, o yeni veri tiplerinin tip denetimini güvenceye alır, ve ilgili
veri tipi kullanıldığında sistemin güvenlik seviyesi korunur.
C++ derleyicinin güvenlikle ilgili yapabilecekleri, C den çok daha fazladır.
Bu ve önümüzdeki bölümlerde, C++ teknolojisine eklenmiş özellikleri
göreceksiniz. Bu özellikler gözden kaçan ve hızla ortadan kaybolan
hataları, hatta derlemeden evvel, genel derleyici uyarıları şeklinde bildirir.
Dolayısı ile senaryolar umulmayacak derece de olumlu sonlanır, yani bir
C++ programı çoğu kez derlendikten sonra, ilk seferde dahi çalışır.
İlk değer atama yani başlatma ve silme işlemi yukarıdaki güvenlik
unsurlarından iki tanesidir. C dilinde yazılan programların hatalarının en
büyük bölümü; programcının bir değişkenin ilk değer atamasını, ve silme
işlemini unutmasından kaynaklanır. Burada yeri gelmişken belirtelim; ilk
değer atama yani başlatma ile atama işlemleri aynı değildir. Her ikisi de
programcılık açısından farklı anlamlar taşır, sonuçları da farklıdır. Bunu
unutmayın.
Müşteri programcı bir structa ilk değer atamasını nasıl yapacağını
bilmediği zaman, ve hatta ne yapmak zorunda olduğunu bilmediği
durumlar, özellikle C kütüphanelerinde rastlanır. (kütüphanelerde ilk değer
atama işlevi çoğunlukla bulunmaz, ve bu nedenle müşteri programcı
structa ilk değer atama işlemini elle yapar.). C programcıları değişkenlerin
işi bittiğinde, onu unutma rahatlığına sahip oldukları için, kütüphane
structı için gereken silme işlemi gözden kaçar, o nedenle silme işlemi özel
bir sorun olur.
C++ da ise, ilk değer atama ve silme işlemleri kolay kütüphane kullanımı
için esastır, ve müşteri programcı bu etkinlikleri unuttuğu zaman,
görünmeyen hatalar olmaz. Şimdi C++ da doğru ilk değer atama ve silme
özellikleri incelenecek.
Yapıcı işlev ile ilk değer atama
Daha önce tanımlanmış olan hem Stash hem de Stack sınıfların da
initialize( ) işlevi vardı, adından anlaşılacağı gibi bir nesne kullanılmadan
önce initialize( ) işlevi çağrılması lazımdır. Maalesef bu, müşteri
programcının ilk değer ataması işlemini doğru yapmak zorunda olduğu
anlamını taşımaktadır. Müşteri programcılar sorunlarını çözmek için, sizin
şaşırtıcı kütüphanelerinize düşüncesizce saldırdıklarından, ilk değer atama
gibi benzeri ayrıntıları gözden kaçırırlar. C++ da ilk değer atama işlemi,
müşteri programcıya bırakılmayacak derecede önemlidir. Sınıf tasarımcısı
yapıcı işlev denilen özel bir işlev yardımı ile, bir nesnenin ilk değer atama
işlemini güvenceye alır. Eğer bir sınıfta yapıcı işlev var ise, bir nesne
oluşturulurken, müşteri programcı nesneyi elde etmeden önce, derleyici
otomatik olarak o noktaya yapıcıyı çağırır. Hatta yapıcı işlev çağrımı
müşteri programcı için bir seçenek değildir; nesnenin tanımlandığı noktada
derleyici tarafından kendiliğinden yapılır.
Bir sonraki aşama; bu işleve verilecek olan addır. İki seçenek var.
Birincisi; sınıf içinde kullanacağınız herhangi bir üye ismi ile çatışma riski
bulunan bir isim. İkincisi ise derleyicinin yapıcıyı çağırmaktan sorumlu
olduğu ve mutlaka her zaman hangi işlevin çağrılacağını doğru bildiren
isim. Bu, ilk değer ataması sırasında işlevin kendiliğinden gelmesini de
anlamlı kılar.
Şimdi içinde yapıcı işlev bulunan basit bir sınıf örneği verelim;
class X{
int i;
public:
X( ); //yapıcı işlev
};
Şimdi bir nesne tanımlanma zamanı
void f( ){
X a;
///....
}
Sanki a int değişkenmiş gibi aynı işlemler olur, nesne için bellek yeri
ayrılır. Fakat program a nın tanımlandığı sıraya
( çalışma noktası) eriştiği zaman, yapıcı otomatik olarak gelir.Yani a
nesnesine, tanım noktasında, derleyici sessizce X::X( ) işlev çağrımını
yerleştirir.Herhangi benzer üye işlev de olduğu gibi, ilk ( gizli ) değişken
this göstergecidir- çağrılmakta olan nesnenin adresini verir-. Yapıcının bu
durumunda, this ilk değer ataması yapılmamış bellek alanını gösterir. Ve
bu belleğe ilk değer atamasını uygun biçimde yapmak yapıcı işlev
ödevidir.
Herhangi bir işleve benzer şekilde, constructor bir nesnenin nasıl
oluşturulacağını belirleyen değişkenlere sahip olabilir, ilk atanan değerleri
verebilir, vesaire. Yapıcı değişkenleri nesnenin bütün parçalarına uygun
ilk değer atamalarını yapar. Örnek olarak; Agac sınıfındaki yapıcı ağacın
yüksekliğini veren, ve tek bir int değişkene sahipse, bir ağaç nesnesini
aşağıdaki biçimde oluşturmak zorundasınız demektir;
Agac a(10); //12 metre uzunluğunda ağaç
Agac(int) tek yapıcıysa, derleyici başka bir şekilde nesne oluşturmanıza
izin vermez. (bir sonraki bölümde birden fazla constructor kullanımı
işlenecek ve yapıcı çağrımlarının farklı usulleri görülecektir)
Gerçekten herşey yapıcıda bulunur; nesnenin oluşturulduğu noktada her
nesne için derleyici tarafından otomatik olarak çağrılan, özel adlı bir işlev
yapıcı. Bütün basitliğine rağmen, çözümlediği sorunlar, kodların yazımı ve
okunmasın da sağladığı kolaylıklar bakımından eşsiz değerdedir. Önceki
kod parçasında tanımdan ayrı olarak initialize() işlevine çağrı açık bir
şekilde görülmez. C++ da tanım ve ilk değer atama birleşik olarak
yapılır.Biri olmadan diğeri olamaz.
Hem yapıcı (constructor) hemde yıkıcı (destructor) işlevler, bilinen
işlevlere benzemezler; geri dönüş değerleri yoktur.Bu, void geri dönüş
değerinden tamamen farklıdır.Zira işlev hiçbir değer döndürmesede, hala
başka bir şey yapma seçeneğine sahiptir. Yapıcı ve yıkıcı işlevler ise hiçbir
değer döndürmez ve başka bir seçenekte sağlamazlar.
Programın içine bir nesneyi almak ve sonra çıkarmak eylemi çok özeldir,
aynı doğum ve ölüme benzer, ve bundan dolayı, derleyici işlemin
yapıldığından emin olmak için, işlev çağrımlarını her zaman kendi
yapar.Eğer bir geri dönüş değeri olsaydı, ve eğer siz kendinizinkini
seçseydiniz, derleyici geri dönüş değeri ile bir şekilde ne yapacağını
bilmek zorunda olacaktı, ya da müşteri programcı yapıcı ve yıkıcı işlevleri
kendi çağırmak zorunda olacak ve böylece güvenliği tehlikeye düşürecekti.
Yıkıcı işlev ile silme işlemi
C programcıları ilk değer atamasının önemini bilirler, fakat silme işlemi ile
ilgileri pek azdır. Herşeyden sonra, bir int değişkeni silmeye ne
gereksiniminiz var?. Şimdilik bunu unutalım diyebilirler.Bir biçimde
işlevlikle birlikte nesne de yerinde silinmeden bırakıldığında, sistem o
kadar güvenilir olmaz. Zira Ya silinmeyen bilgi donanım da değişiklik
yapıyorsa? Ya ekrana farklı bir bilgi gönderiyorsa? Ya da bellek kümesi
üzerine veri yerleştiriyorsa? İşte siz onu orada unutursanız, nesne hiçbir
şekilde o dünyadan ayrılıp çıkamaz. C++ da ise silme ile, ilk değer ataması
işlemi aynı önemdedir, ve yıkıcı (destructor) işlev tarafından yerine
getirilir.
Yıkıcı işlevin yazım biçimi, yapıcı işleve benzer; işlev adı olarak sınıf adı
kullanılır.Yıkıcı işlevi yapıcı işlevden ayırmak başına (~ ) işareti kullanılır.
Buna ek olarak yıkıcı işlev hiçbir zaman değişken almaz, zira yıkıcı işlevin
bu seçeneğe gereksinimi yoktur.Aşağıda bir yıkıcı işlev örneği verilmiştir.;
class Z{
public:
~Z( );
};
Nesne, ortamdaki işini bitirdiği zaman, yıkıcı işlev derleyici tarafından
otomatik olarak çağrılır. Yapıcı işlevin, nesne tanımlandığı noktada
çağrıldığı görülebilir, fakat yıkıcı işlevin çağrıldığının tek delili; nesneyi
saran kapanan zincirli ayraçtır. Hatta goto ile ortamın dışına çıkma işlemi
yapılsa bile yine de yıkıcı işlev çağrılır.(goto deyimi C++ da, C ile geriye
dönük uyum için kullanılmaktadır.) Bu arada bir konuyu belirtelim;
standart C de bulunan yörel olmayan
setjmp( ) ve longjmp( ) işlevleri yıkıcı işlevi çağırmaz. (Bu bir özelliktir,
derleyici bunu o şekilde uygulamasa bile, özellik olmamaya dayanarak
kodlar aktarılamaz demek değildir.)
Aşağıda hem yapıcı hem de yıkıcı işlevleri ayrıntıları ile gösteren bir
örnek;
//:B06:Yapici1.cpp
//Yapıcılar ve yıkıcılar
#include <iostream>
using namespace std;
class Tree{
int height;
public:
Tree(int initialHeight); //Yapıcı işlev
~Tree( );
//Yıkıcı işlev
void grow(int years);
void printsize( );
};
Tree::Tree(int initialHeight){
height=initialHeight;
}
Tree::~Tree( ){
cout<<"Tree yıkıcı içi"<<endl;
printsize( );
}
void Tree::grow(int years){
height+=years;
}
void Tree::printsize{
cout<<"Tree yüksekliği"<<height<<endl;
}
int main( ){
cout<<"zincirli ayracı açma öncesi"<<endl;
{
Tree t(10);
cout<<"Tree oluşum sonrası"<<endl;
t.printsize( );
t.grow(5);
cout<<"zincirli ayraç kapama öncesi"<<endl;
}
cout<<"zincirli ayraç kapama sonrası"<<endl;
}///:~
Yukarıdaki programın çıktısı aşağıda verilmiştir;
zincirli ayracı açma öncesi
Tree oluşum sonrası
Tree yüksekliği 10
zincirli ayraç kapama öncesi
Tree yıkıcı içi
Tree yüksekliği 15
zincirli ayraç kapama sonrası
Yukarıda da gördüğünüz gibi yıkıcı işlev, zincirli ayraç kapandıktan sonra
otomatik olarak çağrılmaktadır.
Tanım bütünlüğünün ortadan kaldırılması
C de başlangıçta yer alan zincirli ayracın hemen başında, bütün
değişkenleri her zaman mutlaka bir bütün halinde tanımlamak
zorundasınız. Bütün programlama dillerinde aynı uygulama kullanılmaz.
Gerçi bu yöntem iyi programlama usulü olduğu ileri sürülerek kabul
görmüş, fakat bizim ciddi şüphelerimiz var. Bunun nedeni; programı
yazmaya başladıktan uzun bir süre sonra, yeni bir değişkene ihtiyaç
duyduğumuz da, programın en başındaki değişken tanımlama bölümüne
gidip, bu yeni değişkeni ekleme durumudur.Bu programcı için zordur,
ayrıca değişkeni kullanıldığı yerde tanımlama, okumayı kolaylaştırır.
Değişkenlerin yazımı belki bir tarz olsa da, C++ da değişkenlerin
tamamının en başta tanımlanması bazı sorunlar oluşturur. Yapıcı işlev
varsa, bir nesne oluşturulurken mutlaka yapıcı işlev çağrılır. Bir şekilde
yapıcı işlev bir ya da birden fazla değişken alıyorsa, başlangıçta yer alan
ilk değer atamalarını nasıl bilebilirsiniz?. Genel programlama koşullarında
bunu bilemezsiniz. Zira C dilinde private uygulaması yoktur, tanım ve ilk
değer atamanın bu ayrımı sorun değildir. C++ da ise bir nesne
oluşturulurken, aynı anda ilk değer ataması da güvenceye alınmıştır.
Böylece C++ da ilk değer ataması yapılmamış nesnelerin sistemde
bulunmadığından emin olursunuz. C dili ise bunu dert etmez ;
C dili ilk değer atamasının gerekmeden önce yapılmasını yani, başlangıçta
değişkenleri bütün olarak tanımlamanızı ister. Aslında bu C dilinin
programcı için fazla esnekliği olmadığını gösterir.
Genel olarak C++ dilinde, yapıcı işleve ilk değerleri atanmadan önce, bir
nesnenin oluşturulmasına izin verilmez. Bunun nedeni ; değişkenleri en
başta tanımlamış olsaydınız bu dil (C++) uygulanabilir olmazdı. Gerçekte
dilin kurgusu, nesnenin kullanımından hemen önce, mümkün olan en yakın
yerde tanımlanmasını izin verir. C++ da herhangi bir nesneye uygulanan
kurallar, otomatikman yerleşik tiplerin nesneleri için de geçerlidir. Bunun
anlamı; bir sınıf nesnesi ya da yerleşik tip değişkeni kullanılmadan önce
herhangi bir yerde tanımlanbilir. Bunun başka bir anlamı da; bir değişkeni
tanımlamadan önce onunla bilgiyi elde edene kadar bekleyebilirsiniz, yani
ilk değer ataması ve tanımlamayı aynı anda yapabilirsiniz demektir.
//:B06:DefineInitialize.cpp
//Tanımlamaların gereken herhangi bir yerde yapılması
#include "../require.h"
#include <iostream>
#include <string>
using namespace std;
class G{
int i;
public:
G(int ii);
};
G::G(int ii){ i=ii;}
int main( ){
cout<<"ilk atanan değer " ;
int retvalue=0;
cin>>retval ;
require(retval!=0) ;
int y=retval+4;
G g(y);
}///:~
Yukarıdaki program parçasını incelediğiniz de, program kodlarının bir
kısmı çalıştırıldıktan sonra, retval tanımlanır, ilk değeri atanır ve kullanıcı
girişini yakalamak için kullanılır, ve daha sonra y ve g tanımlanır. C
dilinde ise bildiğiniz gibi herhangi bir değişkenin tanımı sadece
başlangıçta yapılır, başka bir yerde tanımlanmasına izin verilmez.
C++ dilinde değişkenlerin tanımı mümkün olduğunca kullanıldığı yere en
yakın yerde yapılır, ve her zaman ilk değer ataması da tanımın yapıldığı
yerde gerçekleştirilir.(Aslında yazım biçimi ile ilgili olan bu uygulama
sadece bir seçenektir, tavsiye edilir).Bu güvenlik unsurudur.Künye
görünümün de değişken görünme süresini kısaltma, değişkenin başka
bölümlerde yanlış kullanımını engeller. Değişkenin kullanıldığı yerde
tanımlanması işleminin bir diğer yararı da
programın okunabilirliğini arttırmasıdır, zira programın ilerleyen
bölümlerinde değişkenin tipini öğrenmek için, geri gidip sonra yine ileri
gitme işlemleri ortadan kalkar.
for döngüleri
C++ dilinde for döngü sayacı hemen sağındaki tanımla beraber sıklıkla
rastlanan bir ifadedir. for deyimi kullanım örneği aşağıda verilmiştir;
for(k=0;k<100;k++){
cout<<"k= "<<k<<endl;
}
for(i=0;i<100;i++)
cout<<"i= "<<i<<endl;
Yukarıdaki deyimler önemli özel durumlar içindir ve çoğu kez yeni C++
programcılarının kafalarını karıştırır.
i ve k değişkenleri for deyiminin açıklamasının yapıldığı hemen yanındaki
ayraçlar arasında tanımlanır, ve aynı anda da ilk değer ataması yapılır
(bunu C dilin de yapamazsınız) .Daha sonra for döngüsü için kullanılabilir
olur. Bu yazım biçimi i ve k değişkenleri ilgili tüm sorunları kaldırdığı için
en uygun yazım biçimidir. i ve k değişkenlerinin ömrü for döngüsü
görünümünün sonunda biter, biraz şaşırtıcı olan da budur.
Bölüm üçte, switch ve while deyimlerinin denetim açıklamaları ile
nesneleri tanımlamaya izin verdiğini görmüştük.
Aslında bu for döngülerinden daha az önemli görünen bir uygulamadır.
Değişkenleri kuşatan görünümden gizleyen,
yörel değişkenlere dikkat edilmelidir. Yuvalanmış değişken ile görünümün
global değişkeni için aynı adı kullanma, kafa karışıklığına neden olduğu
gibi, hata yapma eğilimini artırmaktadır.
Bence küçük görünüm iyi tasarımın en belirgin niteliğidir. Bir işlev için
sayfalarca kod yazılmışsa, herhalde o işlevle
çok iş yapılacak demektir. Daha parçalı işlevler sadece daha kullanışlı
değil, aynı zamanda hataları da bulmak daha kolaydır.
Saklama yerleşimi
Bir değişken artık, görünümün herhangi bir yerinde tanımlanabilir, tanım
yerine gelene kadar değişkenin bellekteki yeri belli değildir. Derleyici daha
ziyade C dilinde ki pratikte olduğu gibi, zincirli ayracın ilk açıldığı yerde
başlayan, görünümde ki bellek yerleşimini izler. Dert değil tabii ki. Bir
programcı olarak tanımlanana kadar bellek yerine (nesne) erişemezsiniz.
Bloğun en başında bellek yerleşimi yapılmasına rağmen, nesne tanımlama
sırası gelene kadar yapıcı işlev çağrılmaz, zira belirteç o zamana kadar
belli değildir. Hatta derleyici nesne tanım yerinden koşullu geçiş
yaptığınızda, tanımı koymadığınızı denetleyerek emin olur, zira switch ya
da goto deyimleri kullanılarak tanım yeri atlanmış olabilir. Aşağıda ki
kodlar da yorumlanmamış deyimler hata iletileri ve uyarılar üretir.
//:B06:NoJumps.cpp
//yapıcılar sıçranarak geçilemez.
class X{
public:
X( );
};
X::X( ){}
void f(int i){
if(i<10) {
//! goto jump1; //Error:goto bypasses init
}
X x1; //yapıcı çağrımı
jump1:
switch(i){
case 1:
X x2; //yapıcı çağrımı
break;
//!case 2: //Error :bypasses init
break;
}
}
int main( ){
f(9);
f(11);
}///:~
Yukarıdaki kod parçasında switch ve goto deyimleri, yapıcı işlevin
çağrıldığı yerin, sıçranarak geçilmesini sağlar.
Yapıcı çağrılmamış olsa bile nesne görüntüde kaldığı için derleyici hata
iletisi verir. Bu bir kez daha ilk değer ataması gerekmese bile, nesnenin
oluşturulamayacağını kesinleştirir.
İncelenen bellek yerleşiminin tamamı yığıtta yer alır. Derleyici yığıt
göstergecinin değerini azaltarak bellek yerleşimini sağlar.(aslında azalma
açıklaması göreceli bir deyimdir, makineden makineye değişebilir, yığıt
göstergeç değerinin azalması veya artması anlamına gelebilir.) . Nesneler
küme (heap) benzeri belleğe, new anahtar kelimesi kullanılarak
yerleştirilir. Bu konu ilerleyen bölümlerden birinde, daha ayrıntılı
incelenecektir.
Stash'te yapıcı ve yıkıcı işlevlerin kullanımı
Önceki bölümlerde verilen bazı örneklerde yapıcı ve yıkıcı işlevlerin
görevini yapan işlevler kullanılmıştı; initialize( ) ve cleanup( ), şimdi de
aynı Stash başlığın da yapıcı ve yıkıcı işlevler kullanılacak.
//:B06:Stash2.h
//yapıcı ve yıkıcılarla birlikte işlem
#ifndef STASH2_H
#define STASH2_H
class Stash{
int size;
//bellek alanı boyutu
int quantity ; //bellek alan sayısı
int next ;
//bir sonraki boş bellek alanı
//dinamik yerleşmiş bayt dizisi
unsigned char * storage ;
void inflate(int increase) ;
public:
Stash(int size) ;
~Stash( ) ;
int add(void* element) ;
void* fetch(int index) ;
int count( ) ;
};
#endif //STASH2_H///:~
Üye işlevlerden sadece initialize( ) ve cleanup( ) işlevleri yapıcı ve
yıkıcılarla yer değiştirmişlerdir.
//:B06:Stash2.cpp {O}
//yapıcılar ve yıkıcılar
#include "Stash2.h"
#include "../require.h"
#include <iostream>
#include <cassert>
using namespace std ;
const int increment=100 ;
Stash::Stash(int sz) {
size sz ;
quantity=0;
storage=0 ;
next=0 ;
}
int Stash::add(void* element){
if(next>=quantity)
//yeterli bellek sınaması
inflate(increment) ;
//bir sonraki boş yerden başlayarak
//belleğe kopyalama yap
int startBytes=next*size ;
unsigned char* e=(unsigned char*)element ;
for(int i=0;i<size;i++)
storage[startBytes+i]=e[i] ;
next++ ;
return (next-1) ;
}
void* Stash::fetch(int index){
require(0<=index, "Stash::fetch (-)index");
if(index>=next)
return 0 ; //sonlanma
//istenen ögeyi gösteren göstergeç
return &(storage[index*size]);
}
int Stash::count( ){
return next; //Cstash teki öge sayısı
}
void Stash::inflate(int increase){
require(increase>0,"Stash::inflate sıfır ya da negatif değerlerle");
int newQuantity=quantity+increase;
int newBytes=newQuantity*size ;
int oldBytes=quantity*size;
unsigned char* b=new unsigned char[newBytes];
for(int i=0;i<oldBytes;i++)
b[i]=storage[i]; //eskiyi yeni yere kopyala
delete [](storage); //eski yer
storage=b ;
//yeni bellek yeri işareti
quantity=newQuantity;
}
Stash::~Stash( ){
if(storage!=0){
cout<<"bellek boşaltma"<<endl;
delete []storage;
}
}///:~
require.h işlevleri assert( ) yerine programcı hatalarını saptamak için
kullanılmaktadır. assert( ) işlevlerin geçersiz çıktıları kullanılmadığı için,
require.h işlevleri tercih edilmiştir.
inflate( ) in private olması nedeni ile, öbür üye işlevlerden birisi inflate( )
işlevine yanlışlıkla hatalı değer aktardığı durum, require( ) ın tek hata
yaptığı tek durumdur. Bunun olmayacağından eminseniz, require( )
işlevini kaldırmayı düşünebilirsiniz. Fakat akılda tutulması gereken bir şey
de, sınıf dengeli olana kadar, her zaman hata kaynağı olabilecek yeni
kodların sınıfa eklenebileceğidir.require( ) işlevinin maliyeti fazla değildir
(önişlemleyici kullanılarak kaldırılabilir) ve kod sağlamlığı çok artar.
Aşağıdaki sınama programından da farkedeceğiniz gibi, Stash nesnelerinin
tanımları gereksinim ortaya çıkmadan hemen önceki yerde ortaya
çıkmaktadır, ilk değer atama işlemi ise değişken listesinde tanımın bir
parçası olarak görünür;
//:B06:Stash2Test.cpp
//{L} Stash2
//yapıcılar ve yıkıcılar
#include "Stash2.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std ;
int main( ){
Stash intStash(sizeof(int)) ;
for(int i=0;i<100;i++)
intStash.add(&i) ;
for(int j=0;j<intStash.count( );j++)
cout<<"intStash.fetch("<<j<<")= "
<<*(int*)intStash.fetch(j)
<<endl ;
const int bufsize=80 ;
Stash stringStash(sizeof(char)*bufsize) ;
ifstream in("Stash2Test.cpp") ;
assure(in,"Stash2Test.cpp") ;
string line ;
while(getline(in,line))
stringStash.add((char*)line.c_str( )) ;
int k=0 ;
char* cp ;
while((cp=(char*)stringStash.fetch(k++))!=0)
cout<<"stringStash.fetch("<<k<<")= "
<<cp<<endl ;
}///:~
cleanup( ) işlevinin nasıl elendiğini, ve bunun yanında intStash ile
stringStash görünüm dışına çıktığı zaman, yıkıcıların otomatik olarak
nasıl çağrıldığını gözlemleyiniz.
Stash örneklerinde dikkat edilmesi gereken bir konu da ;yerleşik veri
tiplerinin kullanımında çok dikkatli olmamızdır, zira yerleşik veri tiplerinin
yıkıcı işlevleri bulunmamaktadır. Eğer sınıf nesnelerini Stashe kopyalamış
olsaydınız, bütün sorun kaynaklarını çalıştırmış olacak ve böylece
programınız doğru çalışmayacaktı. Aslında standart C++ işlevliği
nesnelerin doğru kopyalarını kılıflarına yerleştirir, fakat bu kafakarıştıran
karmaşık bir süreçtir. Aşağıdaki Stack örneğinde bu unsuru etraflıca
anlatmak için göstergeçler kullanılmıştır. Bir sonraki Stash örneğinde de
göstergeçler kullanılacaktır.
Yığıt ile birlikte yapıcı ve yıkıcı işlevler
Yapıcı ve yıkıcıları ile ilişimlenmiş listenin(Stack içinde) yeniden
uygulanması, yapıcı ve yıkıcıların new ve delete ile ne kadar düzgün
çalıştığını göstermektedir. İşte değiştirilmiş başlık dosyası;
//:B06:Stack3.h
//yapıcı/yıkıcılarla birlikte
#ifndef STACK3_H
#define STACK3_H
class Stack{
struct Link{
void* data ;
Link* next ;
Link(void* dat,Link* nxt) ;
~Link( );
}* head ;
public:
Stack( );
~Stack( );
void push(void* dat) ;
void* peek( );
void* pop( );
};
#endif //STACK3_H///:~
Sadece Stack yapıcı ve yıkıcı işlevlere sahip değildir, aynı zamanda
yuvalanmış Link sınıfı da yapıcı ve yıkıcı işlevlere sahiptir.
//:B06:Stack3.cpp {O}
//yapıcılar/yıkıcılar
#include "Stack3.h"
#include "../require.h"
using namespace std ;
Stack::Link::Link(void* dat,Link* nxt){
data=dat ;
next=nxt ;
}
Stack::Link::~Link( ){}
Stack::Stack( ){head=0;}
void Stack::push(void* dat){
head=new Link(dat,head);
}
void* Stack::peek( ){
require(head!=0,"Stack empty");
return head->data;
}
void* Stack::pop( ){
if(head==0)return 0;
void* result=head->data;
Link* oldHead=head;
head=head->next;
delete oldHead;
return result;
}
Stack::~Stack( ){
require(head==0,"Stack boş değil");
}///:~
Link::Link( ) yapıcı işlevi data ve next göstergeçlerinin ilk değerini
atar.Böylece Stack::push( ) da bulunan aşağıdaki satır;
head=new Link(dat,head) ;
sadece yeni bir Link yerleştirmez, fakat ayrıca o Link için olan
göstergeçlere ilk değeri doğru bir şekilde atar.Dinamik olarak new ile
nesne oluşumu 4. bölümde işlendi) Şimdi Link için yıkıcı işlev neden bir
şey yapmıyor merak ediyorsunuzdur. -özellikle data göstergecini delete
etmiyor. İki sorun var.4.bölümde Stacke ilk giriş yapıldığında
belirtildiği üzere eğer bir nesneyi gösteriyorsa, void göstergeçuygun
şekilde delete edilemez. Ek olarakta, Link yıkıcısı data göstergeçini
silseydi, pop( ) silinen nesneyi işaret eden göstergeç geri dönüşünü sona
erdirirdi,bu da bir hata kaynağı olacaktı. Bu bazen sahip olma unsuru
olarak anılır. Link ve bu nedenle Stack sadece göstergeçleri tutarlar, ama
onların silinmesinden sorumlu olmazlar.Bunun anlamı; kimin sorumlu
olduğu hususunda mutlaka çok dikkatli olunmalı ve kim olduğu
bilinmelidir. Örneğin; eğer Stackte bulunan bütün göstergeçleri pop( ) ve
delete etmezseniz,
Stackte bulunan yıkıcılar onları otomatik olarak silmez. Bu bellek
kaçaklarına yol açabilir, kimin nesneyi silme işleminden sorumlu olduğunu
bilmek, başarılı bir programla hatalı bir program arasındaki farkı
oluşturur.Bu nedenle Stack nesnesi yıkıma rağmen boş değilse,
Stack::~Stack( ) hata iletisi üretir.
Link nesnelerinin yerleştirilmesi ve silinmesi Stackte saklandığı için,uygulamanın önemli bir parçasıdır- sınama programında ne olduğunu
göremezsiniz. Tabii ki pop( ) tan geri gelen göstergeçlerin silinmesinden
siz sorumlu olmanıza rağmen.
//:B06:Stack3Test.cpp
//{L} Stack3
//{T} Stack3Test.cpp
//yapıcılar/yıkıcılar
#include "Stack3.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std ;
int main(int argc,char* argv[]){
requireArgs(argc,1) ; //dosya adı bir değişkendir
ifstream in(argv[1]) ;
assure(in,argv[1]) ;
Stack textlines ;
string line ;
//dosya okuma ve yığıtta saklama
while(getline(in,line))
textlines.push(new string(line)) ;
//satırları yığıttan alıp yazdırma işlemi
string* s ;
while((s=(string*)textlines.pop( ))!=0){
cout<<*s<<endl ;
delete s ;
}
}///:~
Yukarıdaki durum da textlines da yeralan bütün satırlar alınıp silinir, eğer
bu yapılmasaydı bellek kaçağı anlamına gelen require( ) iletisi alınacak .
İlk değer atamalarının bir yerde toplanması
Konu başlığımızın anlamı, değişik tiplerin (örnek;struct ve class her iki
tipin nesneleri gibi) birarada bulunması demektir. Bir dizi (array) ise
bilindiği gibi aynı tipin örneklerinin birarada bulunmasıdır.
İlk değer atamalarının hepsini birarada yapmak, sorun yumağını sarmak
olduğu için karışık ve zordur. C++ birleşik ilk değer atamalarını çok daha
güvenli yapar. Nesneler birarada oluşturulduğu zaman, zorunluluk olan her
şey atamadır.
Ve derleyici ilk değer atamasını dikkate alır.Bu değer atamaları değişik
biçimlerde uygulanabilir, fakat bütün durumlarda, mutlaka atanan ögeler
zincirli ayraçlarla çevrili bulunmalıdır. Yerleşik veri tipleri için bu oldukça
kolay bir işlemdir;
int a[5]={1,3,5,7,9};
atanan ilk değerlerin sayısı dizi boyutundan daha fazla ise, derleyici hata
iletisi verir. Ya peki ilk değer sayısı dizi boyutundan daha az ise ne olur;
int b[7]={0} ;
Burada derleyici dizinin ilk ögesi olarak atanan sıfır değerini alır, öteki
ögelerin yerine de sıfır atar. Yani dizi ögelerinin
tamamına sıfır atanır. İlk değer atama listesi bulunmayan bir dizi de, atama
işlemleri yerine getirilmez. Yukarıdaki yazım biçimi, bir dizinin bütün
ögelerine for döngüsü kullanmadan 0 (sıfır ) atamanın en kısa yoludur. Ve
bu biçim sayesinde sıfır değer ataması ögelerden herhangi birini sıfırdan
farklı yapmaksızın gerçeklenir. (derleyiciye bağlı olarak, for döngüsüne
göre daha verimlidir)
Otomatik saydırma diziler için ikinci kısaltmadır, bu kısaca; derleyici, ilk
değer atamalarının sayısı ile dizi boyutunu belirler.
int c[]={2,3,4,5,6} ;
Burada yeni bir öge eklenmesi işlemi sadece ilk değer atama listesine
konulan rakamla sağlanır. Kodlar öyle düzenlenmeli ki, değişim sadece tek
bir yerde yapılsın, böylece hata yapma olasılığı çok azalır. Peki bir dizinin
boyutunu nasıl belirleyeceksiniz?. sizeof c/sizeof *c, dizi boyutu
değişmeyecekse, değişmeden kullanılacak ipucudur.
(dizi boyutu, dizinin ilk ögesinin boyutuna bölünür). İlerleyen bölümlerde
dizi boyutu hesaplamalarında kullanılan şablonlar incelenecek.
for(int i=0;i<sizeof c/sizeof *c;i++)
c[i]++ ;
Künyede de, birlikte bulundurmaları nedeni ile, ilk değer atamaları benzer
şekilde yapılabilir. C tipi structlarda bütün üyeler public olduğu için
bütün atamalar doğrudan yapılabilir;
struct X{
int i;
float f;
char c;
};
X x1={1,3.3,'z'};
Bu tür nesnelerden oluşan bir diziniz varsa, her nesneye karşılık gelen
yuvalanmış zincirli ayraçlar kullanarak, ilk değer atamaları gerçekleştirilir.
X x3[3]={{1,1.1,'s'},{2,3.14,'k'}};
Üçüncü nesneye ilk değeri atanmadığı için, 0 (sıfır) demektir.
Eğer veri üyelerinden herhangi biri private ise (bu durum C++ da yer alan
class tiplerinden biridir), veya üyelerin hepsi public olsa bile, yapıcı varsa
herşey farklıdır. Yukarıda ki örneklerde bir aradaki ögelere, ilk değerler
doğrudan atanır,
fakat yapıcı işlevler sayesinde, bu daha resmi arayüzle gerçeklenir. Artık
yapıcılar ilk değer atamaları için çağrılmak zorundadır. Aşağıdaki gibi bir
structınız varsa;
struct Y{
int i;
float f;
Y(int a);
};
Mutlaka yapıcı çağrılarını açıkça belirtmelisiniz. En iyi yaklaşım aşağıda
gösterilmiştir;
Y y1[]={Y(1),Y(2),Y(3)};
3(üç) nesne, 3(üç) tane de yapıcı çağrısı var. Yapıcı işlevinin olduğu
herhangi bir durum da; ister bütün üyeleri public olan struct olsun, isterse
veri üyeleri private olan class olsun, tüm ilk değer atama işlemleri mutlaka
yapıcı tarafından gerçeklenmelidir. Ve hatta birarada bulunan atanmalar
olsa bile.
Aşağıdaki örnek birden fazla değişkeni olan yapıcı işlev örneğidir;
//:B06:Multiarg.cpp
//çok değişkenli yapıcı
//ve birarada ilk değer atamaları
#include <iostream>
using namespace std ;
class Z{
int i,j;
public:
Z(int ii,int jj);
void print( );
};
Z::Z(int ii,int jj){
i=ii;
j=jj;
}
void Z::print( ){
cout<<"i= "<<i<<", j= "<<j<<endl;
}
int main( ){
Z zz[]={Z{1,2},Z{3,4},Z{5,6},Z{7,8},Z{9,0}};
for(int i=0;i<sizeof zz/sizeof *zz;i++)
zz[i].print( );
}///:~
Gördüğünüz gibi her nesne tarafından çağrılan bir yapıcı var.
Varsayılan yapıcılar
Varsayılan yapıcı değişkeni bulunmayan yapıcı olarak tanımlanmıştır.
Varsayılan yapıcı "sade nesne" oluşturmak için kullanılır, önemi de
derleyiciye ayrıntıları verilmeyen bir nesne oluşturması isteminde
bulunulduğun da ortaya çıkar.
Örnek olarak önceden tanımı verilen struct Y yi alalım, ve aşağıdaki gibi
tanımda kullanalım;
Y y2[2]={Y{1}};
Derleyici varsayılan yapıcıyı bulamadığı için şikayet iletileri gönderir.
Derleyici dizide ki ikinci nesneyi, değişkensiz olarak varsayılan yapıcı ile
üretir, yani varsayılan yapıcının arandığı yer burasıdır. Aslında basitçe, Y
nesnelerinin dizisini tanımlarsanız;
Y y3[8];
derleyici hala şikayetçidir, zira dizide her nesneye ilk değeri atamak için,
mutlaka varsayılan yapıcı bulunmalıdır. Aşağıdaki gibi tek nesne
oluşturulursa da aynı sorun ortaya çıkar.
Y y4 ;
Hatırlayın! yapıcı işleviniz varsa, derleyici yapım olmasını her zaman,
bulunulan durumu gözardı ederek, güvenceye alır.
Varsayılan yapıcı o kadar önemlidir ki, ancak (ve ancak) künye de (struct
ve classta) bir tane varsayılan yapıcı işlev yoksa, derleyici otomatik olarak
bir tane bizim için üretir. Bu şöyle çalışır,
//:B06:AutoDefaultConstructor.cpp
//otomatik olarak varsayılan yapıcı üretimi
class V{
int i; //private
}; //yapıcı yok
int main( ){
V v, v2[10];
}///:~
Eğer herhangi bir yapıcı işlevi tanımlanmışsa, bir şekilde varsayılan yapıcı
yoksa, V nin yukarıdaki durumları derleme
sırasında hatalar ürer.
Derleyici kökenli yapıcıları zekice ilk atamaları yapan unsurlar olarak
düşünebilirsiniz, örnek olarak nesne için gereken bütün belleği
sıfırlayabilir. Tabii bunu yapmaz, zira programcının denetimi dışında olan
bu durum ilave sorun olur.
Bellekte ilk atama değerleri sıfırlanmak istenirse, bu programcı tarafından
bizzat varsayılan yapıcı ile gerçeklenmelidir.
Derleyici varsayılan yapıcıyı programcı için otomatik olarak üretmesine
rağmen, derleyici kökenli varsayılan yapıcı nadiren istenenleri yerine
getirir. Bu özelliği güvenlik ağı olarak düşünmelisiniz, ve az
kullanmalısınız. Genel olarak kendi yapıcılarınızı açık olarak kendiniz
tanımlamalısınız, ve bunu derleyicinin yapmasına izin vermemelisiniz.
ÖZET
C++ tarafından sağlanan oldukça yetenekli mekanizmalar, başlatma ve
silme işlemlerinin kritik önemi
hakkında ipucu verir. Stroustrup C++ yı tasarlarken C de ilk gözlemlediği,
üretkenlikle ilgili programlama sorunlarının büyük bölümü, değişkenlere
uygun olmayan ilk değer atamalarıydı. Bunlar saptaması zor hatalara
yolaçıyordu, ayrıca aynı hatalar yanlış silme işleminden de olabiliyordu.
Yapıcı ve yıkıcı işlevler ilk değer atama ve silme işlemlerini güvenceye
alır. (derleyici, nesne oluşumu ve silinmesi için uygun olmayan yapıcı ve
yıkıcı işlev çağrımına izin vermez.). Böylece tam denetim ve güvenlik
sağlanmış olur.
Toptan ilk değer atama işlemi benzer amaca yöneliktir. Yerleşik veri
tiplerine toptan ilk değer atama, hatalardan korur, ve kodu daha kısa yapar.
Ama gerektiğinde başlatma yapabilmekte esneklik sağlar.
Kodlama sırasında güvenlik, C++ dilinin büyük bir yeteneğidir. Başlatma
ve silme bunun önemli bir bölümüdür. Güvenlik konusunun öbür
bölümlerini ileride göreceksiniz.
7. BÖLÜM
İşlev bindirimi ve varsayılan değişkenler
Programlama dillerindeki en önemli konulardan biri, isimlerin doğru
kullanılmasıdır. Bir nesne veya değişken oluşturduğunuz zaman, onun
bellekteki yerine bir ad verirsiniz. Bir işlev adı aslında bir eylem adıdır.
Geliştirilen sisteme uygun isimler vermek, diğer kişilerin programı
okumasını ve düzeltmesini daha kolay kılar. Aslında lafı fazla uzatmadan
şunu söylemeliyiz; amaç programı okuyanla ilişkidir.
Günlük kullanılan dil ile programlama dilleri arasındaki fark nedeni ile,
günlük dilin programa aktarılması sırasında sorun çıkar. Çoğu kez aynı
kelime, bulunduğu yerden dolayı farklı anlamlar taşır. Aynı ad birden fazla
anlam taşıyabilir, buna bindirim denir. Bu özellik, küçük farklılıklar
olduğu zaman çok kullanışlıdır. Örnek olarak; "elbise temizle, araba
temizle" demek istiyorsunuz, bunu "elbise_temizle elbise, araba_temizle
araba " şeklinde aptalca yazarsanız, dinleyen, yapılacak eylemler
arasındaki farkı kavrayamaz. İnsan dilleri kendi içinde kısaltmalara
sahiptir, hatta arada bir kaç kelime kaçırılmış olsa bile, cümlede anlam hala
belirlenebilir. Bu durumlarda belli belirteçlere gereksinim duyulmaksızın,
ilişkilerden yararlanarak tümdengelimle anlamlandırma yapılır.
Programlama dillerinde ise, her işlev için belli bir belirtecin olması istenir.
Yazdırmak istediğiniz üç değişik veri tipiniz varsa; int, char, float,
genellikle üç değişik işlev adı oluşturmak zorundasınız demektir. Örnek
olarak;
print_int( ), print_char( ) ve print_float( ). Bunlar, program yazarken ve
okuyucu programı anlamaya çalışırken ek görevlerdir.
C++ da işlev ad bindirimleri için zorlayıcı olan ikinci unsur ; yapıcılardır.
Çünkü yapıcı işlev adı, önceden sınıf adı tarafından belirleniyor. Bu,
sadece bir yapıcı işlev olabilirmiş gibi mana veriyor. Fakat birden fazla
yolla bir nesne oluşturulmak istenirse ne olacak?. Örnek olarak; standart
yolla kendine ilk değer atamalarını yapabilen bir sınıf kuralım
ve ayrıca bir dosyadan bilgiler okusun. Burada iki tane yapıcı işleve
gereksinim var, birinin değişkenleri yok (varsayılan yapıcı), öteki ise
değişkeni string olan bir yapıcı işlev olup, nesneye ilk değer atamasını
gerçekleştirecek dosya adını taşır. Her ikisi de yapıcı işlevlerdir, her ikisi
de mutlaka aynı adı taşımalıdır; sınıf adını. Bu nedenle işlev bindirimi;
aynı işlev adının kullanımına izin verir - bu durumda yapıcı- ve farklı
değişkenlerle kullanılabilir.
Yapıcılar için bir mecburiyet olan işlev bindirimi, genel uygulama olarak
ve herhangi bir işlevle de kullanılabilir, üstelik sınıf üyesi işlev olması da
gerekmez. Bunlara ilaveten işlev bindirimi, aynı adlara sahip işlevlerin
olduğu iki tane işlevlik anlamına gelir, değişken listeleri farklı olduğu
sürece bu işlevler çatışmaz. Şimdi bu konuların bütün ayrıntılarını
inceleyeceğiz.
Bu bölümün ana teması; işlev adlarının doğru kullanılmasıdır. İşlev
bindirimi; farklı işlevlere aynı adın konmasına izin verir, fakat işlev
çağrımı için başka uygun bir yol daha var. Aynı işlevi farklı bir yolla
çağırmak isterseniz ne yapacaksınız ?. İşlevlerin uzun değişken listeleri
olduğu zaman, ve çağrıların tamamındaki değişkenlerin çoğunluğu aynı
olunca, işlev çağrılarını yazmak oldukça yorucu ve zahmetlidir. Varsayılan
değişkenler, C++ da yaygın kullanılan özelliktir. Varsayılan değişken, eğer
işlev çağrımında belirtilmemişse derleyici tarafından sisteme sokulan bir
unsurdur.
Böylece f("merhaba"), f("hey",1) ve f("ne_haber",1,'z') çağrılarının
hepsi aynı işlevi çağrırırlar. Bunların üç tane, bindirimli işlev olduğu
görünmekte. Benzer değişken listeleri olursa tek işlev çağrısı ile genellikle
benzer davranış beklenir. Burada bir konuyu vurgulayalım; bindirimli işlev
ile baskın işlev tamamen farklıdırlar.
Bindirimli işlevler ve varsayılan değişkenler konusu, aslında çok karışık
değildir. Bu bölümün sonuna geldiğiniz de, kullanılacakları zamanı, ve
derleme ile ilişim sırasında kullanımlarının temel mekanizmalarını anlamış
olacaksınız.
İsim düzenlemesi üzerine ayrıntı
Dördüncü bölümde isim düzenlemelerine giriş yapılmıştı, aşağıdaki kod
parçasında;
void f( );
class X{void f( );};
class X görüntüsünde bulunan f( ) işlevi, f( ) işlevinin global sürümü ile
uyuşmazlık etmez. Derleyici f( ) in global sürümü ile X::f( ) için farklı iç
isimler üreterek bunu başarır. Bilindiği gibi 4. bölüm de; önerilen sınıf adı
ile birlikte basitçe düzenlenen isimler için derleyicinin kullandığı iç
isimlerin _f ve _X_f olur. Böyle işlev ad düzenlemelerinde,
örnekte görüldüğü gibi sınıf adından daha fazlası bulunur.
İşte sebebi burada. İki işlev adını biribirine bindirelim.
void print(char);
void print(float);
Her ikisi de sınıf içinde veya global görüntüde olmuş farketmez. Eğer
sadece işlev isimlerinin görüntüsü kullanılırsa, derleyici belli tek iç
belirteçler üretemez. Her iki durum _print ile bitirilir. Bindirilmiş işlev
fikri, aynı işlev adının farklı değişken listelerine sahip işlevler için
kullanılmasıdır. Yani bindirilmişlerle çalışacak derleyici, mutlaka işlev
adını değişken tiplerinin adları ile düzenlemelidir. Global alanda
tanımlanmış yukarıdaki işlevler, print_char, print_float benzeri iç
isimleri üretirler. Burada hemen belirtelim, derleyicinin isim düzenleme de
kullanması zorunlu bir standart yoktur, bu nedenle derleyiciden derleyiciye
değişen neticeler elde edilir. (derleyiciden assembly dili çıktısı isterseniz,
neticelerin neye benzediğini görürsünüz.). Belli bir derleyici ve ilişimci
için, kütüphaneler satın alırsanız,
bu farklılıklar sorun olur.- isim düzenlemeleri standart yapılmış olsa bile
derleyicilerin farklı yollardan kod üretmeleri nedeni ile engellemeler
olacaktı.
Yani gerçekten bindirimin işlevsel olması ; aynı işlev adının, farklı
değişken listesine sahip değişik işlevler için de kullanılabilmesidir.
Derleyici isim, alan, (görüntü) ve değişken listesine, kendisi ve ilişimcinin
kullanması için, iç isimler üretir.
Geri dönüş değerleri üzerine bindirim
Yaygın merak; "niçin sadece görüntü ve değişken listeleri?. niçin geri
dönüş değerleri değil?." İç işlev adları ile birlikte ayrıca geri dönüş
değerini düzenlemek, başlangıçta anlamlı gibi gözükür. Daha sonra, geri
dönüş değerleri üzerine bindirim yapılabilir;
void f( );
int f( );
Derleyici şüpheye yer vermeyecek şekilde, int x=f( ) teki gibi, ilişkiyi
belirlediği zaman gayet iyi çalışır. Yalnız,
C de her zaman işlev çağrılabilir, ve daha sonra geri dönüş değeri gözardı
edilebilir. (yani işlevi, yan etkileri için çağırabilirsiniz.) Bu durumda ki
anlamı, derleyici hangi çağrının olduğunu nasıl ayırabilecek?. En kötü
olasılık; okuyucunun hangi işlev çağrısının ne anlama geldiğini bilmedeki
zorluğudur. Geri dönüş değeri üzerine bindirim biraz ince bir konudur. Bu
nedenle, C++ da izin verilmez.
Tip-güvenlik ilişkisi
İsim düzenlemenin tamamında ek yararlar bulunur. C de, müşteri
programcı işlev bildirimini yanlış yaptığı zaman, veya daha kötüsü; işlevi
bildirmeden önce çağırırsa, berbat bir sorun ortaya çıkar, o zaman derleyici
işlev bildirimini, çağrıldığı yerde ima yolu ile anlamlandırır. Bazen bu
yolla işlev bildirimi doğru netice verebilir, ama doğru olmadığı zaman
bulunması çok zor hatalar oluşturur.
C++ da ise işlevler kullanılmadan önce mutlaka bildirilmek zorunda
olunduğu için, buna benzer hataların ortaya çıkma olasılığı kalmaz. C++
derleyicisi sizin adınıza otomatik olarak işlev bildirimi yapmayı reddeder,
zira bu sizin uygun başlık dosyası eklemenize benzer. Bir şekilde bazı
nedenlerden hala yanlış bildirilmiş işlevi yönetiyorsanız -ya elle yazarak
bildirim, ya da yanlış başlık dosyası ekleme (belki günü geçmiş)- isim
düzenlemesi, tip-güvenlik bağlantısı olarak bilinen güvenlik ağı kurulur.
Aşağıdaki senaryoyu inceleyelim. Bir dosya işlev tanımı için
kullanılmıştır.
//:B07:Def.cpp {O}
//İşlev tanımı
void f(int){}
///:~
İkinci dosyada işlev yanlış bildirilmiş ve daha sonra çağrılmış.
//:B07:Use.cpp
//{L} Def
//yanlış işlev bildirimi
void f(char) ;
int main( ){
//! f(1) ;
}///:~
// ilişimci hatasına neden olur
Aslında f(int) olsa bile, derleyici onun daha önceden böyle olduğunu
bilmez, zira ona işlevin f(char) olduğu açıkça
bildirilmişti. Böylece derleme başarılı olur. C dilinde ayrıca ilişimci de
başarılıdır, ama C++ da değil. Derleyici isimleri düzenlediği için,
kullanımı f_char olan işlevin tanımı, f_int benzeri gibi bir şey olur. f_char
ı çözümleyen ilişimci
sadece f_int ile karşılaşır, ve hata iletisi üretir. Bu, tip-güvenlik ilişkisidir.
Sorun sıklıkla ortaya çıkmamasına rağmen, kazara özellikle büyük
projelerde olursa, bulunması çok zor hatalara yol açar. Aslında bu durum C
programlarında, C++ derleyicileri kullanıldığında kolayca bulunabilen, zor
hatalardan biridir. C++ da, böyle bir hatayı, ilişimci kolaylıkla yakalar.
Bindirim örneği
Şimdi artık daha önceki örnekleri, işlev bindirimi kullanmak için
değiştirebiliriz. Daha önce de belirtildiği gibi işlev bindirimi için en uygun
yer yapıcılardır. Stash sınıfının aşağıdaki sürümünde bunu görebilirsiniz.
//:B07:Stash3.h
//işlev bindirimi
#ifndef STASH3_H
#define STASH3_H
class Stash{
int size ;
//bellek alanı boyutu
int quantity ;
//bellek alanı sayısı
int next ;
//bir sonraki boş yer
//dinamik olarak yerleştirilmiş bayt dizisi
unsigned char* storage ;
void inflate(int increase) ;
public:
Stash(int size) ; //sıfır sayı
Stash(int size,int initQuantity) ;
~Stash( ) ;
int add(void* element) ;
void* fetch(int index) ;
int count( ) ;
};
#endif //STASH3_H///:~
İlk Stash( ) yapıcı işlevi önceki ile aynıdır, fakat ikinci yapıcı işlev de
Quantity değişkeni bulunur ve ilk atanacak bellek yer sayısını gösterir.
Tanımda yeralan quantity iç değeri sıfıra eşittir, ve bunu storage
göstergeçi belirler. İkinci yapıcı daki inflate(initQuantity) çağrısı
quantity yi yerleşim boyut değerine arttırır;
//:B07:Stash3.cpp {O}
//işlev bindirimi
#include "Stash3.h"
#include "../require.h"
#include <iostream>
#include <cassert>
using namespace std ;
const int increment=100;
Stash::Stash(int sz){
size=sz ;
quantity=0 ;
next=0 ;
storage=0 ;
}
Stash::Stash(int sz,int initQuantity){
size=sz ;
quantity=0 ;
next=0 ;
storage=0 ;
inflate(initQuantity) ;
}
Stash::~Stash( ){
if(storage!=0){
cout<<"belleği boşalt"<<endl ;
delete []storage ;
}
}
int Stash::add(void* element){
if(next>=quantity) //yeterli bellek alanı varmı
inflate(increment) ;
//ögeyi kopyala
//bir sonraki bellek yerinden başla
int startBytes=next*size ;
unsigned char* e=(unsigned char*) element ;
for(int i=0;i<size;i++)
storage[startBytes+i]=e[i] ;
next++ ;
return(next-1) ; //dizin numarası
}
void* Stash::fetch(int index){
require(0<=index,"Stash::fetch (-)index") ;
if(index>=next)
return 0 ; //sona gelindi
//istenen ögenin göstergeçini üretir
return &(storage[index*size]) ;
}
int Stash::count( ){
return next ;
//Cstash teki öge sayısı
}
void Stash::inflate(int increase){
assert(increase>=0) ;
if(increase==0)return ;
int newQuantity=quantity+increase ;
int newBytes=newQuantity*size ;
int oldBytes=quantity*size ;
unsigned char* b=new unsigned char[newBytes] ;
for(int i=0;i<oldBytes;i++)
b[i]=storage[i] ;
// eskiyi yeniye kopyala
delete []storage ; // eski yeri boşalt
storage=b ;
//yeni yeri göster
quantity=newQuantity ;
//boyutu ayarla
}///:~
Birinci yapıcı işlev kullanılırsa, bellekte storage için yer ayrılmaz. İlk defa
yerleşim, bir nesneyi add( ) etmeyi denerseniz ya da belleğin o anki
bütünü add( ) e doğru aşıldığı herhangi bir anda olur.
her iki yapıcı işlevin kullanımı aşağıdaki sınama örneğinde gösterilecek;
//B07:Stash3Test.cpp
//{L} Stash3
//işlev bindirimi
#include "Stash3.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std ;
int main( ){
Stash intStash(sizeof(int)) ;
for(int i=0;i<100;i++)
intStash.add(&i) ;
for(int j=0;j<intStash.count( );j++)
cout<<"intStash.fetch("<<j<<")= "
<<*(int*)intStash.fetch(j)
<<endl ;
const int bufsize=80 ;
Stash stringStash(sizeof(char)*bufsize,100) ;
ifstream in("Stash3test.cpp") ;
assure(in,"Stash3Test.cpp") ;
string line ;
while(getline(in,line))
stringStash.add((char*)line.c_str) ;
int k=0 ;
char* cp ;
while((cp=(char*)stringStash.fetch(k++))!=0)
cout<<"stringStash.fetch("<<k<<")= "
<<cp<<endl ;
}///:~
stringStash için çağrılan yapıcı ikinci değişkeni kullanır; ihtimaldırki
Stash in başlangıç büyüklüğünü, çözümlenecek özel bir sorun için
biliyorsunuzdur.
Union'lar
struct ile class arasındaki fark; struct ta varsayılanlar public, class ta
varsayılanlar private dir. Beklendiği üzere bir struct ta da yapıcı ve yıkıcı
işlevler olabilir. Bir union'unda ayrıca yapıcı ve yıkıcı işlevlere, üye
işlevlere ve hatta erişim denetimine sahip olabileceği söylenebilir.
Aşağıdaki örnekte yine bindirimin kullanışı ve yararları incelenecek;
//:B07:UnionClass.cpp
//yapıcılar ve üye işlevlerle birlikte union
#include <iostream>
using namespace std ;
union U{
private:
int i;
float f;
public:
//erişim denetim aracı
U(int a) ;
U(float b) ;
~U( );
int read_int( );
float read_float( );
};
U::U(int a){i=a;}
U::U(float b){f=b;}
U::~U( ){cout<<"U::~U( )\n";}
int U::read_int( ){return i;}
float U::read_float( ){return f;}
int main( ){
U X(12),Y(1.9F) ;
cout<<X.read_int( )<<endl;
cout<<Y.read_float( )<<endl;
}///:~
Yukarıdaki örnekten göreceğiniz gibi, class ile union arasındaki fark;
verinin saklanma şeklidir. (yani int ve float aynı bellek yerini kaplar) .
Kalıtım uygulaması sırasında union ana sınıf olarak kullanılamaz, bu
nedenle nesne yönelimli bakış açısından oldukça sınırlı kullanıma sahiptir.
(kalıtımı 14. bölümde öğreneceksiniz)
Üye işlevlerin uniona erişimi medenileştirilmiş olmasına rağmen, uniona
bir kez ilk değer ataması gerçeklendikten sonra, hala müşteri programcının
yanlış öge tipini seçmesini engelleyecek bir yol yoktur. Yukarıda ki
örnekte uygun olmasa bile X.read_float( ) diyebilirsiniz . En güvenilir
union sınıfa sarmalanmış olandır. Aşağıdaki örnekte enum
anahtar kelimesinin kodu nasıl açıkladığını, ve yapıcılarla bindirimin nasıl
kullanıldığını göreceksiniz.
//:C07:SuperVar.cpp
//süper bir değişken
#include <iostream>
using namespace std;
class SuperVar{
enum{
charaacter,
integer,
floating_point
}vartype; //anonim union
union{ //tanımı
char c;
int i;
float f;
};
public:
SuperVar(char ch) ;
SuperVar(int ii) ;
SuperVar(float ff) ;
void print( );
};
SuperVar::SuperVar(char ch){
vartype=character ;
c=ch ;
}
SuperVar::SuperVar(int ii){
vartype=integer ;
i=ii ;
}
SuperVar::SuperVar(float ff){
vartype=floating_point ;
f=ff ;
}
void SuperVar::print( ){
switch(vartype) {
case character:
cout<<"character: "<<c<<endl ;
break;
case integer:
cout<<"integer: "<<i<<endl;
break,
case floating_point:
cout<<"float: "<<f<<endl;
break;
}
}
int main( ){
SuperVar A('c'), B(12), C(1.44F) ;
A.print( );
B.print( );
C.print( );
}///:~
Yukarıdaki kodda enum tip adına sahip değildir (uzantısız enum denir).
Burada yapıldığı gibi enum durumlarını hemen tanımlayacaksanız,bu o
zaman kabul edilebilir. Gelecektede enum tip adına gereksinim yoktur, ve
bu nedenle tip adı bir opsiyondur.
unionun tip adı, ve değişken adı yoktur. Anonim union denme nedeni
budur. union için bellekte yer ayrılır fakat union ögelerine erişmek için,
değişken adına ve nokta işlecine gerek yoktur. Örnek: sizin anonim
union'unuz,
//:B07:AnonymousUnion.cpp
int main( ){
union{
int i;
float f;
};
//niteleyiciler kullanılmaksızın erişim üyeleri
i=12;
f=1.22;
}///:~
Anonim union'un üyelerine, sanki normal değişkenlermiş gibi
erişebilirsiniz. Tek fark sadece her iki değişkenin aynı bellek alanını
kullanmasıdır. Anonim union dosya görüntüsünde ise (bütün işlevler ve
sınıflar dışarıda), o zaman iç ilişim sağlamak için mutlaka static olarak
bildirilmelidir.
SuperVar güvenli olmasına rağmen, yararlılığı biraz şüphelidir, zira ilk
anda union kullanımındaki amaç; bellek alanından tasarruf etmektir, ve
vartype ilavesi ile uniondaki verilere göre biraz daha fazla bellek alanı
kaplar, bu yüzden bellek yeri kazancı ortadan kalkar. Bu şemanın
çalışabilir olması için birkaç seçenek vardır. Eğer vartype union'un birden
fazla unsuru tarafından denetlenirse-ki eğer hepsi aynı tipte olsaydı- o
zaman grup için bir tanesine gereksinim duyulacaktı, ve bellek alanının
daha azı kullanılacaktı. Daha kullanışlı bir yaklaşım da; bütün vartype
kodu etrafında #ifdef kullanmaktır, geliştirme ve sınama sırasında
yapılacakların doğru yapılması için güvence sağlanmış olur. Kodları
tamamlarken fazladan bellek alanı kullanımı ve zaman kaybı engellenmiş
olur.
Varsayılan değişkenler
Stash3.h de, baktığınız da iki adet yapıcı işlev görürsünüz, O kadar
birbirlerinden farklı değilller, değilmi? Aslında ilk yapıcı işlev ikinci
yapıcı işlevin özel halinden başka birşey değildir, burada size ilk değeri
sıfırlanmıştır. Benzer bir işlevin iki farklı sürümünü oluşturmak ve
sürdürmek boşuna çabadır.
C++, varsayılan değişkenler ile, buna çare olur. İşlev çağrımında
değişkenlere bir değer atamadığınız zaman, derleyici
bildirimde bulunulan değişkene otomatik olarak değer atar, ve bu değer de
varsayılan değişkendir. Stash örneğinde iki işlev yer değiştirebilir.
Stash(int size) ;
//sıfır sayısı
Stash(int size,int initQuantity) ;
bunu tek bir işlevle,
Stash(int size,int initQuantity=0) ;
Stash(int) tanımı kadırılarak, yerlerine tek başına Stash(int,int)
konmuştur. Şimdi iki nesne tanımı verelim;
Stash A(100), B(100,0) ;
tamamen aynı neticeleri verirler. Her iki durumda da aynı yapıcı çağrılır.
Ama derleyici A için, ilk değişkeni int olarak gördükten sonra ikinci
değişken olmadığını görünce, bu değişken derleyici tarafından otomatik
olarak yerleştirilir. Derleyici varsayılanı gördüğünde, böylece bilirki ikinci
değişkeni atadığı zaman işlev çağrımını yapabilir, zaten o varsayılan
yapılarak ona yapacakları söylenmiş olur.
Varsayılan değişiklikler işlev bindirimleri gibi bir kolaylıktır. Her iki,
özellik farklı durumlarda aynı adı kullanmaya izin verir. Varsayılanlarla
meydana gelen fark; değişkenleri siz atamak istemediğiniz de bu görevi
derleyici otomatik olarak yerine getirir. Önceki örnek, işlev bindirimi
yerine varsayılan değişkenlerin kullanılacağı uygun bir yerdir.Aksi
takdirde benzer davranış ve imzalara sahip iki veya daha fazla işlevle işi
bitirirsiniz. Eğer işlevler çok farklı davranışlara sahipse, varsayılan
değişkenleri kullanmak genellikle pek anlamlı değildir. (böyle bir durumda
çok farklı davranışlara sahip işlevlerin aynı adı kullanmalarını
sorgulamanız lazım.)
Varsayılan değişkenleri kullanırken iki kurala mutlaka uymalısınız.
Birincisi; sonraki varsayılan olmalı. Yani, varsayılan bir değişkenin
ardından varsayılan olmayan bir değişken gelmemelidir. İkinci kural; özel
bir işlev çağrımında
varsayılan değişken ile başlarsanız, o işlevin değişken listesinde yer alan
takip eden bütün değişkenler de varsayılan olmak zorundadır.(bu zaten ilk
kuralın ayrıntılı açıklamasıdır).
Varsayılan değişkenler sadece işlev bildirimin de yeralırlar.(tipik olarak
başlık dosyasında). Derleyici kullanabilmek için, önce mutlaka varsayılan
değeri görmelidir. Bazen programcılar işlev tanımında, varsayılan
değişkenlerin anlatımı amacı ile, yorumlu değerlerini yerleştirirler.
void fn(int x /* =0 */){//...
Yer tutucu değişkenler
İşlev bildirimin de yer alan değişkenler belirteçleri olmadan da
bildirilebilir. Bunlar varsayılanlarla kullanıldığı zaman,
biraz matrak görünebilir. Şöyle sonlanırlar;
void f(int x,int=0,float=2.2) ;
C++ da işlev bildirimin de belirteçlere gereksinim yoktur.
void f(int x,int,float flt) {/*.... */}
İşlev gövdesinde x ve flt esas alınabilir, ama ortadaki değişken (yani int )
alınamaz, çünkü adı yok. İşlev çağrıları ise yertutucular için hala bir değer
temin etmek zorundadır; f(1) veya f(1,2,3.0). Bu yazım biçimin de
yerleştirilen değişken yertutucu olarak konur, fakat kullanılmaz. İşlev
tanımı daha sonra değiştirilmek istenirse, yertutucular kullanılır, böylece
işlevin çağrıldığı yerdeki bütün kod değişmemiş olur. Tabii aynı şeyi
isimlendirilmiş değişkenle de başarmak olasıdır. Fakat değişkeni işlev
gövdesi için onu kullanmaksızın tanımlarsanız, derleyicilerin çoğu uyarı
iletisi verir, ve sizin mantık hatası yaptığınızı kabul eder. Planlı bir şekilde
değişken adı dışarıda bırakılırsa,
bu uyarı bastırılmış olur.
Daha önemlisi, eğer işlev değişkeni kullanarak başlar ve daha sonra
gereksinim olmadığına karar verilirse, onu etkili bir şekilde uyarı almadan
kaldırabilirsiniz, ve işlevin önceki sürümünü çağırmış olan müşteri kodları
bozulmaz.
Varsayılan değişkenlere karşılık işlev bindirimlerinin
seçimi
Hem işlev bindirimi hem de varsayılan değişkenler işlev adlarının
çağrımları için bir kolaylıktır. Hangi tekniğin kullanılacağını bilme
hususunda kafa karışıklığı var gibi görünmektedir. Örneğin aşağıdaki aracı
gözönüne alalım;
bu araç bellek bloklarını otomatik olarak yönetmek amacı ile
tasarlanmıştır.
//:B07:Mem.h
#ifndef MEM_H
#define MEM_H
typedef unsigned char byte ;
class Mem{
byte* Mem ;
int size ;
void ensureMinSize(int minSize) ;
public:
Mem( );
Mem(int sz) ;
~Mem( );
int msize( );
byte* pointer( );
byte* pointer(int minSize) ;
};
#endif //MEM_H ///:~
Bir Mem nesnesi bir bir byte bütününü tutar ve o nedenle yeterli bellek
yeri olduğundan emin olun. Varsayılan yapıcı bellek alanına herhangi
yerleşim yapmaz, ikinci yapıcı ise Mem nesnesinde sz saklamasını güvene
alır. Yıkıcı saklananları siler, msize( ) işlevi o anki Mem nesnesinde kaç
byte olduğunu hesaplar, pointer( ) işlevi ise saklananların başlangıç
adresini üretir, (Mem oldukça alt seviyeli bir araçtır). pointer( ) işlevinin
bindirilmiş sürümü olan
pointer(int minSize) işlevi, bir byte bütününü gösterir ve bütünün en az
minSize boyutunda olmasını temin eder.Böylece müşteri programcı boyutu
saptam olanağı bulur.
Hem yapıcı hem de pointer( ) üye işlevi private ensureMinSize( ) üye
işlevini bellek boyutunu arttırmak amacı ile kullanırlar (dikkat!! eğer
bellek boyutu değiştirilmişse pointer( ) işlevinin sonucunu tutmak
güvenilir değildir.)
Bir sınıf uygulaması örnek olarak aşağıda verilmiştir;
//:B07:Mem.cpp {O}
#include "Mem.h"
#include <cstring>
using namespace std ;
Mem::Mem( ){mem=0;size=0;}
Mem::Mem(int size){
mem=0;
size=0;
ensureMinSize(sz);
}
Mem::~Mem( ){delete []mem;}
int Mem::msize( ){return size;}
void Mem::ensureMinSize(int minSize){
if(size<minSize){
byte* newmem=new byte[minSize] ;
memset(newmem+size,0,minSize-size) ;
memcpy(newmem,mem,size) ;
delete []mem ;
mem=newmem ;
size=minSize ;
}
}
byte* Mem::pointer( ){return mem;}
byte* Mem::pointer(int minSize){
ensureMinSize(minSize) ;
return mem ;
}///:~
Gördüğünüz gibi ensureMinSize( ) işlevi bellek yerleşiminde tek
sorumludur, ve ikinci yapıcı tarafından kullanılır, ikinci bindirilmiş
pointer( ) işlevi bu işi görür. size yeterli büyüklükte ise ensureMinSize( )
işlevinin içinde herhangi bir şey yapılmasına ihtiyaç yoktur. Bellek alanını
arttırmak için, yeni bir saklama alanı eklemesi zorunluluğu doğarsa,
daha önce 5. bölümde anlatılan standart C işlevliğinden memset( )
kullanılarak yeni eklenen bellek bölümü sıfırlanır.
Bir sonra ki işlev çağrımı olan memcpy( ), mem de varolan byte'ları
newmem'e kopyalar (tipik olarak etkin biçimde)
Son olarakta eski bellek bilgileri silinir, ve yeni bellek bilgileri ile birlikte
boyutta, uygun üyelere atanır.
Mem sınıfı başka sınıfların içinde bellek yönetimlerini kolaylaştırmak için
tasarlanmıştır (aslında bellek yönetimi için çok daha ayrıntılı künyeler
kullanılabilir, işletim sistemlerinin bellek yönetimleri gibi..). Basit bir
string sınıfı yardımı ile şimdi sınama yapılacak;
//:B07:MemTest.cpp
//Mem sınıfının sınanması
#{L} Mem
#include "Mem.h"
#include <cstring>
#include <iostream>
using namespace std;
class MyString{
Mem* buf;
public:
MyString( );
MyString(char* str) ;
~MyString( );
void concat(char* str) ;
void print(ostream& os) ;
};
MyString::MyString( ){buf=0;}
MyString::MyString(char* str){
buf=new Mem(strlen(str)+1) ;
strcpy((char*)buf->pointer( ),str) ;
}
void MyString::concat(char* str){
if(!buf) buf=new Mem;
strcat((char*)buf->pointer( buf->msize( )+strlen(str)+1),str) ;
}
void MyString::print(ostream& os){
if(!buf) return ;
os<<buf->pointer( )<<endl;
}
MyString::~MyString( ){delete buf ;}
int main( ){
MyString s("My test string") ;
s.print(cout) ;
s.concat("some additional stuff") ;
s.print(cout) ;
MyString s2 ;
s2.concat("varsayılan yapıcı kullanımı") ;
s2.print(cout) ;
}///:~
Bu sınıfla MyString nesnesi oluşturup metni ardarda sıralayıp daha sonra
ostreame yazdırabilirsiniz. Yine sınıfta Mem gösteren tek bir göstergeç
vardır, bunun varsayılanla farkı göstergeçin sıfıra ayarlanmasıdır, ikinci
yapıcı Mem oluşturur ve verileri oraya kopyalar. Varsayılan yapıcı işlevin
avantajı nesneleri kolayca oluşturması, örneğin; büyük boyutlu boş
MyString nesnesi hem ucuz hem de çok kolayca, varsayılan yapıcı
tarafından oluşturulur. Zira her nesnenin boyutu tek bir göstergeçtir, ve
sıfır değeri atanması varsayılan yapıcı işlevin görevidir. MyString
nesnesinin maliyeti veriler ardarda dizilmeye başladığında gelir; o anda
eğer yoksa Mem nesnesi oluşturulur. Bir şekilde varsayılan yapıcı
kullanıyor ve hiç bir zaman verileri ardarda dizmiyorsanız, yıkıcı çağrısı
hala güvenilir, zira sıfır için delete çağrısı tanımlıdır, yani bellek silinmesi
için teşebbüste bulunmaz, aksi takdirde sorun çıkardı.
Her iki yapıcıyı incelerseniz,başlangıçta varsayılan için birisi baş aday gibi
görünebilir. Varsayılan yapıcıyı bırakıp, öbür kalan yapıcıyı varsayılan
değişken ile yazarsanız;
MyString(char* str=" ") ;
herşey doğru çalışır, fakat önceki, herzaman bir Mem nesnesinin oluşması
kazancı ortadan kaybolur. Bunu tekrar elde etmek için, yapıcıyı
değiştirmek gerekir;
MyString::MyString(char* str){
if(!*str){//boş stringi gösterir
buf=0;
return ;
}
buf=new Mem(strlen(str)+1) ;
strcpy((char*)buf->pointer( ),str) ;
}
Etkin bir biçimde bunun anlamı; varsayılan değer bir bayrak olur ve bu
bayrakta ayrı kod parça işlemlenmesine neden olur, eğer varsayılan
olmayan bir değer kullanımından başkası ise. Bunun kadar küçük bir
yapıcı yeteri kadar suçsuz olmasına rağmen, genellikle bu pratik sorunlara
neden olur. Eğer varsayılanı, onu normal bir değer olarak işlemlemekten
ziyade aramak zorundaysanız, o zaman bir işlev gövdesinin içine iki işlev
yerleştirilerek bir ipucu bırakılması lazımdır; birisi normal durum için
öbürü varsayılan için. Dilenirse iki farklı işlev gövdesine de
yerleştirilebilir ve seçim işi derleyiciye bırakılır. Görünmemesine rağmen
verimlilikte bir miktar artış sağlar, zira ek değişken aktarılmaz ve ek kodlar
çalıştırılmaz. Daha da önemlisi iki farklı işlev için, varsayılan değişken de
birleştirip kullanmak yerine iki farklı işlevde kod yerleştirilir, özellikle
işlevler aşırı büyük olduğunda bu, bakım açısından kolaylık sağlar.
Öte yandan Mem sınıfını ele alalım. İki yapıcı ve iki pointer( ) işlevinin
tanımlarına bakarsanız, göreceksiniz ki her iki durumda da kullanılan
varsayılan değişken tanımları üye işlev tanım değişikliklerine yolaçmaz.
Böylece sınıf aşağıdaki gibi olur;
//:B07:Mem2.h
#ifndef MEM2_H
#define MEM2_H
typedef unsigned char byte ;
class Mem{
byte* mem ;
int size ;
void ensureMinSize(int minSize) ;
public:
Mem(int sz=0) ;
~Mem( ) ;
int msize( ) ;
byte* pointer(int minSize=0) ;
};
#endif //MEM2_H///:~
Farkına vardığınız gibi ensureMinSize(0) çağrısı her zaman oldukça
yararlıdır.
Her iki durumda da verimlilik üzerine tarafımızdan bazı karar verme süreci
uygulanmıştır, fakat siz bu tür koşullarda sadece verimliliği gözönünde
bulundurma tuzağına düşmeyin. Sınıf tasarımında en önemli unsur sınıfın
arayüzüdür.( onun public üyelerine müşteri programcı kolaylıkla erişir.)
Eğer bunlar kullanımı ve yeniden kullanımı kolay sınıflar üretmişse o
zaman başarmışsınız demektir; gerekirse daha sonra her zaman verimlilik
için ayar yaparsınız, fakat kötü tasarımlanmış bir sınıfta programcının
verimlilik üzerine odaklanması feci sonuçlara neden olabilir. İlk hedef
müşterisi tarafından kolaylıkla kullanılabilen arayüz tasarımı ile kolaylıkla
okunabilen kodlarıdır. Daha önce gördüğünüz gibi MemTest.cpp de ki
MyString değişmez. Burada varsayılanın kullanılıp kullanılmadığına ve
verimliliğin az ya da çok olup olmadığına bakılmaz.
ÖZET
Bir yol gösterici olarak, kodu koşullu çalıştırmak amacı ile, varsayılan
değişkeni kodlara bağımlı bayrak olarak kullanmamanız lazımdır.
Mümkünse işlevi iki veya daha fazla bindirimli işleve ayırmak lazımdır.
Varsayılan değişkenin değeri, kolaylıkla yerleştirilebilecek bir değer
olmalıdır. Bu değer geri kalanlardan daha fazla oluşmalıdır, böylece
müşteri programcı genellikle onu gözardı edebilir, veya ancak ve ancak
varsayılan değeri değiştirmek istediğinde kullanabilir.
Varsayılan değişken ilavesi işlev çağrılarını daha kolaylaştırmak için
yapılır, özellikle işlevler belli değerlere sahip çok sayıda değişkene
sahiplerse. Sadece çağrı yazımları kolaylaşmaz, okunmaları da kolaylaşır,
özellikle sınıf geliştiricisi değişken listesindeki değişkenlerin en az
değiştirilebilir olanını liste sonuna koymuşsa.
Varsayılan değişkenlerin özellikle en önemli kullanımlarından birisi de, bir
grup değişkene sahip işlevle işe başlandığı zamandır. Bunun nedeni bir
süre sonra yeni değişkenler eklemek gereksinimi duyulacağına inançtır.
Bütün yeni değişkenler varsayılan yapılırsa, önceki arayüzü kullanan
müşteri kodlarının bozulmaması güvenceye alınır.
8. BÖLÜM
Sabitler (const)
Sabitler (const) programcıya, nelerin değişip nelerin değişmeyeceği
arasındaki çizgiyi belirlemesine yardım eder.
C++ programlama projesinde güvenlik ve denetimi sağlar. Kaynağından
ötürü, const farklı çok sayıda
amaca hizmet eder. Bu arada C diline geriye, yani anlamının değiştiği yere
döneceğiz. Burada const anahtar kelimesinin ne zaman, neden, ve nasıl
kullanılacağını öğreneceğiz. Başlangıçta hepsi biraz şaşırtıcı gelebilir.
Bölüm sonunda const'ın yakın akrabası (zira her ikisi de değişimle ilgili)
volatile incelenecek, ve her ikisinin de yazım biçimi bakımından özdeş
olduğu göreceğiz. volatile anahtar kelimesi önümüzdeki günlerde koşut
denetim (multithreaded) yani eş anlı programlama yüzünden pek moda
olacak, onu da anmadan geçmeyelim.
const anahtar kelimesi önişlemleyicide değer ataması yapan #define yerini
almak amacı ile ortaya çıkmıştı. const, işlev değişkenleri, göstergeçler,
geri dönüş tipleri, sınıf nesneleri ve üye işlevlerde de kullanılır. Bunların
hepsinin arasında çok az da olsa fark vardır, ama temelde benzerler, ve
hepsi bölümün değişik yerlerinde anlatılacak.
Değer ataması
C dilinde programlama yaparken makro oluşturmak için, önişlemleyici
oldukça rahat kullanılır, ve değer ataması yapılır. Önişlemleyicinin aslında
yaptığı basit bir şekilde metin yerleştirmesidir, burada ne fikir olarak ne de
kolaylık olsun diye tip denetimi yapılmaz, bu nedenle önişlemleyici değer
atamaları sorunlu olur. Bundan korunabilmek, C++ da const kullanımı ile
mümkündür.
Önişlemleyicinin tipik kullanımı olan isimlere değer atamasının C deki
görünümü aşağıdaki gibidir;
#define BUFSIZE 100
BUFSIZE sadece önişlemlemleme esnasında var olan bir isimdir. Bu
nedenle adı saklanmaz.Onu kullanan tüm çevrim birimleri için tek bir
değer, başlık dosyasına yerleştirilir. Değer koymak kod bakımı için çok
önemlidir, zira "büyülü sayılar" gerekmez. Kodlarınız da büyülü sayılar
kullanırsanız, okur sayıların kaynağı hakkında bir şey bilmediği gibi, neyi
temsil ettiklerini de bilmez, ama bir değeri değiştirme kararı verdiğiniz de,
mutlaka elle yazmalısınız, ve değerlerinizden birini kaçırıp kaçırmadığınız
hakkında emniyeti sağlayacak bir takipçi yoktur.(ya da değiştirmemeniz
lazım gelipte değişeni takip gibi, tabii kazara)
Uzun bir süre boyunca BUFSIZE normal bir değişken gibi davranır, ama
her zaman değil. İlaveten tip denetimi de yapılmaz. Bu da bulunması çok
zor olan hatalara yol açar. C++ dili bu sorunu ortadan kaldırmak için,
değer
atama işlemini const ile yaparak derleyici bölgesine sokar. Şimdi kolayca ;
const int bufsize=100 ;
Artık bufsize değerini, derleme sırasında, derleyicinin bilmek zorunda
olduğu her yerde kullanabilirsiniz. Derleyici bufsize'ı sabit dosyalamayı
başarmak için kullanabilir, bunun anlamı; karmaşık sabit bir açıklamayı
derleme sırasında, gerekli hesaplamalar yaparak basit bir açıklamaya
indirgemektir. Bu işlem özellikle dizi tanımlamalarında önemlidir.
char buf[bufsize] ;
Bütün yerleşik veri tipleri (void, char, float ve integer) ve onların
akrabaları (daha sonra göreceğiniz sınıf nesnesi)
için, const anahtar kelimesi kullanılabilir. Önişlemleyicilerin neden olduğu
ince hatalar nedeni ile, her zaman değer atamalarında #define yerine, const
kullanılması lazımdır.
Başlık dosyalarında const
#define yerine const kullanırken, başlık dosyalarında bulunan #define
ifadesini const ile yerdeğiştirmek zorundasınız. Bu yolla tek bir yere const
tanımını yerleştirebilir, ve başlık dosyasına ekleyerek değişik çevrim
birimlerine dağıtabilirsiniz. C++ da const iç bağlantı varsayılanıdır, yani
sadece tanımlandığı dosya içinde görülebilir, ve asla ilişim sırasında başka
çevrim birimlerinden görülemez. extern bulunan bildirim haricinde bütün
koşullarda, const tanımında mutlaka değer atanmalıdır.
extern const int bufsize ;
Normalde C++ derleyicisi const için ada saklama yapmaktan sakınır, onun
yerine tanımı simge tablosunda tutar. extern'i const ile kullandığınız da
ise, ad saklayarak yerleşim için zorlama yaparsınız. (bu bazı özel durumlar
için de doğrudur, const adreslerini almak gibi). Saklama yeri mutlaka
doldurulmalıdır zira extern'in derleyiciye söylediği "dış ilişim kullan"
demektir, bunun anlamı da; birden fazla çevrim birimi mutlaka bu unsuru
dayanak alır, o nedenle saklama yeri dolu olmalıdır.
Normal koşullarda extern tanımın bir parçası değilse, saklama olmaz.
const kullanıldığı zaman ise derleme sırasında dosyalanır.
Karmaşık yapılar da, const için hiç bir zaman saklama yapılmaması amacı
tutmaz. Bir şekilde derleyici saklama yapmak zorunda kalınca, sabit
dosyalama engellenir. (bunun nedeni; derleyicinin saklanan değeri bilme
konusunda emin bir yolu olmamasıdır, zaten bilebilseydi saklama ihtiyacı
olmazdı.)
Derleyicinin const için saklama yapmaktan her zaman sakınamaması
nedeni ile, const tanımlar mutlaka iç ilişimi varsaymak zorundadır, yani
ilişki sadece o özel çevrim birimiyledir. Aksi takdirde karmaşık yapılı
const'lar da birden fazla .cpp dosyasında saklama yapıldığı için ilişimci
hataları ortaya çıkardı. Birden fazla hedef dosyasında ilişimci aynı tanımı
görecek ve rahatsız olacaktı, zira const iç ilişimi varsaydığı için, ilişimci
çevrim birimlerinde o tanımlarla ilişim kurmaya çalışmayacak, ve
çatışmalar olmayacaktı. En çok karşılaşılan, const ifadelerin bulunduğu
yerleşik veri tipli durumlarda, derleyici her zaman sabit dosyalama yapar.
Güvenlik const'ları
Sabit açıklamalarda kullanılan const, sadece #define yerine kullanılmaz.
Bir değişkene çalışma esnasında ilk değeri atanıyorsa ve sizde ömrü
boyunca o değişken değerinin değişmediğini biliyorsanız, o değeri const
olarak tanımlamak iyi programcılık gereğidir, zira siz onu bir şekilde
kazara değiştirmeye kalkarsanız derleyici hata iletisi verir.İşte bir örnek;
//:B08:SafeCons.cpp
//emniyet amacı ile const kullanımı
#include <iostream>
using namespace std ;
const int i=100 ; //tipik bir sabit
const int j=i+100 ; //sabit açıklamasından üretilmiş başka bir sabit
long address=(long)&j ; //saklamaya zorlar
char buf[j+10] ; //hala const ifade
int main( ){
cout<<" bir karakter yaz &CR: " ;
const char c=cin.get( ) ; //değiştirilemez.
const char c2=c+'b' ;
cout<<c2 ;
//...
}///:~
Gördüğünüz gibi i derleyici zamanı sabitidir, ama j sabiti i den
hesaplanarak bulunur. i sabit olduğundan ve j de i den hesaplandığı için j
de hala derleyici zamanı sabitidir. Bir sonraki satır j nin adresini
istemektedir, bu da derleyiciyi j için saklama yapmaya mecbur bırakır.
Henüz buf büyüklüğünü belirleme konusunda j kullanımı engellenmez,
zira derleyici j nin sabit olduğunu bilir, ve programın herhangi bir saklama
yerinde o değer olsa bile, o değer geçerlidir.
main( ) içinde c tanımlayıcısında çok farklı türde const ifade görürsünüz,
zira derleme sırasında değeri bilinemez. Bunun anlamı saklama
gereksinimidir, derleyici simge tablosunda herhangi bir veri tutmak
istemez.( C dilinde de aynı davranış gözlemlenir). İlk değer atama işlemi
yine hala tanım esnasında yerine getirilmek zorundadır, ve bir kez ilk
atama gerçekleştikten sonra, bu değer değiştirilemez. c2 değeri
gördüğünüz gibi c değerinden yararlanarak hesaplanır, ve ayrıca görüntü
diğer tiplerde olduğu gibi burada da geçerli uygulamadır- şimdi #define
yerine kullanılan başka bir kolaylıktır. Pratik bir gerçek olarak bir değeri
değiştirmek istemezseniz, onu const yapmalısınız. Bu sadece
dikkatsizlikten kaynaklanan hataları engellemez, ayrıca derleyicinin daha
başarılı kodları, saklama ve okumaları eleyerek oluşturmasını sağlar.
const'ları biraraya toplama
Birarada bulunan değerleri tek bir const altında toplamak mümkündür.
Ama sanal olarak derleyicinin simge tablosunda değerleri birarada
bulundurması mümkün değildir, bu yüzden saklama yeri yerleşimi yapılır.
Böyle durumlarda const anlamı "değiştirilemeyen saklama yeri kısmı"
demektir. Yani derleme sırasında kullanılamayan değer demektir, zira
derleyici derleme sırasında saklama yerindeki değeri bilmek istemez.
Aşağıdaki kod parçasında uygun olmayan deyimleri de göreceksiniz;
//:B08:Constag.cpp
//sabitler ve birliktelikler
const int i[ ]={1,2,3,4} ;
//! float f[i[3]] ; //geçersiz
struct S{int i,j;};
const S s[ ]={{1,2},{3,4}};
//! double d[s[1].j] ; //geçersiz
int main( ){ } ////...
Bir dizi tanımında derleyici yığıt göstergeçini kullanarak diziyi yığıta
yerleştirir. Yukarıdaki her iki geçersiz durumda da
derleyici dizi tanımında sabit bir ifade ile karşılaşmadığı için hata üretir.
C ile farklılıklar
C++ dilinin ilk sürümleri ortaya çıktığında C dilinin standart özelliklerinde
const ifadesi hala olgunlaştırılmaya çalışılıyordu. C komitesi const
kelimesini standartlarına eklemeye karar vermiş olmalarına rağmen,
"değiştirilemeyen normal değer" anlamında kullanmışlardı. C dilinde
const, saklama yeri olarak ve adı global anlamda kullanılır. C dili
derleyicisi, const ifadeyi derleme zamanı sabiti olarak işlemleyemez. C
dilinde eğer aşağıdaki ifadeyi yazarsanız;
const int bufsize=100 ;
char buf[bufsize] ;
yukarıda mantıklı bir iş yapılmış görünse de, hata iletisi ürer. Zira bufsize
bir yerlerde saklanır, ve C derleyicisi derleme sırasında değeri bilemez. Bir
seçenek olarak aşağıdaki ifadeyi yazabilirsiniz;
const int bufsize ; //C de geçerli C++ da değil
C de C++ da değil, C derleyicisi bunu başka bir yerlerde ki saklama yeri
bildirimi olarak kabul eder. Bunu anlamlı kılan, C dilinin const ifadeleri
dış ilişim olarak varsaymasıdır. C++ dili ise bunu iç ilişim olarak
varsaydığı için, aynı işlemi C++ da yerine getirebilmek için, ilişimi
mutlaka extern kullanarak açık biçimde dışa çevirmelidir.
extern const int bufsize ;
//sadece bildirim, C ve C++ da geçerli
Bu satır C dilinde de çalışır.
C++ da, bir const saklama yeri oluşturma gerekliliği yoktur. C dilinde ise
bir const her zaman bir saklama yeri oluşturur. C++ da saklama yeri
oluşturulup oluşturulmayacağı, const nasıl kullanılacağına bağlıdır.
Genellikle eğer const bir ada bir değer ataması yaparsa (yani #define
yerine kullanılırsa), o zaman const için saklama yeri olmaz.Bu şekilde
saklama yeri gerekmediği durumlarda atanan değerler tip denetiminden
sonra, verimi arttırmak için kod içinde dosyalanır, #define daki gibi daha
önce değil.( bu veri tipinin karmaşıklığına ve derleyicinin yeteneklerine de
bağlıdır.).
Bir şekilde const ifadenin adresi alınır veya, const ifade extern olarak
tanımlanırsa ( ya da bilmeden const bir işlev içinde kullanılıp referans
alınırsa) o zaman const için saklama yeri oluşturulur.
C++ da bütün işlev dışında bulunan const'lar dosya görünümündedirler
(yani dosya dışında görünemezler).Bunun öbür anlamı; iç ilişimin
varsayılmasıdır. Bu da C++ daki bütün diğer belirteçlerden ayrılan en
büyük farklılıktır, zira onlar dış ilişimi varsayar.(aslında bu C deki
const'tan gelir!) .Bu nedenle iki farklı dosya da aynı adı const olarak
bildirirseniz, adresi istenmez veya extern olarak tanımlanmazsa, ideal bir
C++ derleyici bu const için saklama yeri oluşturmaz ve onu kod içinde
dosyalar. C++ da const ifadeleri başlık dosyaları bölgesine koyabilirsiniz,
zira onlar dosya görünümü ima ederler ve ilişim sırasında herhangi bir
çatışma olmaz.
C++ da const ifadeler iç ilişimi varsaydığı için, bir başka dosyada extern
olarak tanımlanmış olan referansı başka bir dosyada hemen const olarak
tanımlayamazsınız. Bir const'a dış ilişim vermek için başka bir dosyadan
dayanak almalıdır, ve aşağıdaki gibi açık biçimde extern olarak
tanımlanmak zorunluluğu vardır.
extern const int x=10 ;
Farkettiğiniz gibi ilk değer ataması ile birlikte extern tanımlaması yapıldı,
böylece saklama yeri oluşumu mecburiyeti doğdu (derleyicinin burada hala
const dosyalama seçeneği var). İlk değer ataması bunu tanım olarak yapar,
bildirim değil. Bildirim ise;
extern const int x ;
Bu C++ da tanımın bir yerlerde bulunduğunu anlatır. ( Bunun C de doğru
olması gerekmez). C++ da şimdi neden bir const tanımın ilk değer ataması
gerektirdiğini anlamışsınızdır; tanımla bildirim arasındaki fark. extern
const bildiriminde derleyici değeri bilemediği için sabit dosyalama
yapamaz.
C dilinin const'a yaklaşımı pek yararlı değildir. Ve eğer bir sabit ifade
içinde değer atanmış bir isim kullanmak isterseniz, (derleme sırasında
mutlaka değeri bilinmelidir), C dili sizi hemen önişlemleyicide #define
kullanmaya zorlar.
Göstergeçler
Göstergeçler const yapılabilir. Derleyici büyük bir azimle const göstergeç
kullandığınızda saklama yeri oluşturmayı engelleyip yerine sabit
dosyalama için uğraşır. Fakat bu özellikler böyle durumlarda daha az
yararlı görünürler. Daha da önemlisi, eğer sabit bir göstergeçi değiştirmek
isterseniz derleyicinin size söylediği güvenlikle ilgili olan kısımdır.
Göstergeçlerle birlikte kullanılan const ifade için iki seçenek vardır; ilki
göstergeçin gösterdiğini const yapmak, ikincisi ise göstergeçin kendisinin
gösterdiği adresi const yapmak. Bunların yazım şekilleri ile ilgili
başlangıçta biraz şaşırabilirsiniz, fakat pratik kazanınca ne kadar yararlı
olduğunu göreceksiniz.
const'a göstergeç
Karmaşık bir tanımda göstergeç tanımları ile ilgili ince nokta; belirteçten
okumaya başlamak, ve çalışmayı dışarı doğru sürdürmektir. const anahtar
kelimeside en yakınındakine bağlanır. Göstergeçin gösterdiği yerdeki
herhangi bir değerin değişmesini istemiyorsanız, tanımları aşağıdaki gibi
yazmalısınız;
const int* u ;
Belirteçten okumaya başlayalım, u değeri adresi verir ve bu adresteki
değerin const int olmasını sağlar. Burada ilk değer ataması gerekmez, zira
u adresi herhangi bir yeri gösterir, siz u adresini değiştirebilirsiniz, fakat u
adresindeki değer sabit ve int tipindedir bunu değiştiremezsiniz.
Şimdi gelelim biraz şaşırtıcı olan kısıma. Göstergeçin kendisini sabit
yapmayı düşünebilirsiniz, yani u belirteçinin kendisinin gösterdiği değerin
değişimini engellemek isteyebilirsiniz. O zaman const anahtar kelimesini
int'in öbür tarafına taşımalıdır.
int const* z ;
Burada "z int'e sabit bir göstergeçtir" demek çılgınca bir şey değildir.
Aslında aynı ifadeye "z int'e normal bir göstergeçtir ve bu da sabittir"
denilebilir. Yani const yine kendini int'e bağlamıştır. Ve yukarıdaki ilk
tanımla aynı
etkiye sahiptir.Yukarıdaki her iki tanımın aynı anlama gelmesi şaşırtıcı
olan noktadır; bu şaşkınlığa düşmemek için ilk biçim seçilmelidir.
const göstergeç
Göstergeçin kendisini sabit yapmak için * işaretinin sağına const
eklemelidir. Yani aşağıdaki gibi;
int d=10 ;
int* const k=&d ;
Şimdi bu "k sabit bir göstergeçtir, ve gösterdiği adreste int değer taşır"
demektir. Burada bizzat göstergeçin kendisi sabit olduğu için, derleyici
göstergeçin bütün ömrünce değişmeyen değerini bilmek ister, yani ilk
değer ataması yapılmalıdır. Tamam, gösterdiği değeri değiştirmek için
yazılabilecek olan;
* k=2 ;
Aşağıdaki geçerli yollardan birini tercih ederek const bir nesneye const bir
göstergeç koyabilirsiniz;
int d=1 ;
const int* const x=&d ; //(1)
int const* const x2=&d ; //(2)
Burada ne göstergeç ne de nesne değiştirilebilir.
Bazıları yukarıdaki tanımlardan ikincisini daha uygun bulmalarına rağmen
siz kendinize daha uygun olan kodlamayı tercih etmelisiniz. Şimdi
derlenebilecek tarzdaki yukarıdaki kod örnekleri, aşağıda bir program
parçasında verilecek;
//:B08:ConstPointer.cpp
const int* u ;
int const* v ;
int d=1 ;
int* const w=&d ;
const int* const x=&d ; //(1)
int const* const x2=&d ; //(2)
int main( ){ } ///:~
Biçimlendirme (formatting)
Bu kitapta bir satırda tek göstergeç kullanılmış ve mümkünse tanımın
yapıldığı yerde başlatması yapıldı.Bu nedenle veri tipinin yanına (*) işareti
konulması yolu seçilmiştir:
int* u=&i ;
Görüntü sanki int* yekpare ifadeymiş gibi geliyor fakat öyle değil.
Aslında kodun daha anlaşılabilir olmasını sağlamak için bu yol seçildi.
Gerçekte (*) işareti tipe değil belirteçe, yani yukarıdaki örnekte u ya
bağlıdır. Aslında (*) işareti tip ile belirteç arasında herhangi bir yere
konabilir.Yani aşağıdaki gibi de yazabilirsiniz:
int *u= &i, v=0 ;
Burada önce *u göstergeçi oluşturulur daha sonra göstergeç olmayan int
v=0 tipi oluşturulur. Bu yol okuyucuya biraz karışık gelebilir, bu nedenle
kitabımızda kullanılan biçimlerin tercih edilmesi size kolaylık sağlar.
Atamalar ve tip denetimi
C++ değer atamaları konusunda birçok yeteneğe sahiptir, buna göstergeç
atamaları da dahil. Sabit olmayan bir nesnenin adresini sabit bir göstergeçe
atayabilirsiniz, zira burada siz birşeyin değişeceğini söylerken bir
başkasının değişmeyeceğine söz veriyorsunuz. Bir şekilde de sabit bir
nesnenin adresini sabit olmayan göstergeçe de atayabilirsiniz. Burada
söylenen nesne ancak göstergeç ile değiştirilebilir. Tabiki bu da böyle bir
atamada döküm yapma gereksinimi ortaya çıkarır. Aslında bu kötü bir
programlama örneğidir, zira siz nesnenin const olmasını bölüyorsunuz, ve
böylece const tarafından sağlanan güvenlik ortadan kalkıyor. Örneğin;
//:B08:PointerAssignment.cpp
int d=1 ;
const int e=2 ;
int* u=&d ; //tamam.. d sabit değil
//! int* v=&e ; // geçersiz zira e sabit
int* w=(int*) &e ; //geçerli fakat güzel değil
int main( ){ }///:~
C++ hata yapmayı engellemeye yardım etmesine rağmen, eğer siz güvenlik
çemberini kırarsanız, sizi sizden koruyamaz.
Karakter dizi işaretleri
Karakter dizi işaret kullanımları keskin sabitliğin zorlanmadığı bir yerdir.
Aşağıdaki gibi yazabilirsiniz;
char* cp="nassin" ;
ve derleyici yukarıdaki yazımı sorunsuz işlemler. Bu teknik olarak bir
hatadır, zira "nassin" karakter dizisi derleyici tarafından sabit karakter
dizisi olarak kabul edilir, bu nedenle çift tırnaklı karakter dizisinin neticesi
bellekteki başlangıç adresidir. Bütün derleyiciler bunu doğru şekilde
yapmamasına rağmen, dizideki ögelerden birinin değişimi, çalışma zamanı
hatasına neden olur.
Karakter dizi işaretleri aslında sabit karakter dizileridir. Doğaldır ki çok
sayıda C kodları bulunmasından dolayı derleyici sizin bunları sabit
değilmiş gibi işlemlemenizi düşünmenize neden olabilir. Bir şekilde
karakter dizisindeki ögelerden birini değiştirirseniz,birçok makine de
çalışabilmesine rağmen davranışı tanımsız olur. Kelimeyi değiştirilebilir
kılmak için aşağıdaki yazım biçimi tercih edilmelidir:
char []cp="nassin" ;
Derleyiciler çoğu kez farkı uygulamadığı için bu kullanım biçimi size
hatırlatılmaz, buradaki ayrım çok incedir.
İşlev değişkenleri ve geridönüş değerleri
const'ın kafa karıştıran uygulamalarından biri de işlev değişkenleri ve
geridönüş değerleridir. Eğer nesneleri değerleri ile aktarırsanız, const
ilavesi müşteri için bir anlam taşımaz. (bunun anlamı işlev içindeki
aktarılan değişken değiştirilemez demektir.) . Kullanıcı tanımlı bir tipin
nesnesini const olarak geri döndürürseniz, bunun anlamı geri dönen değer
değiştirlemez demektir. Eğer adresleri aktarıp geri döndürürseniz, const'ın
taşıdığı anlam varış adresinin değişmeyeceğidir.
Sabit değerle aktarım
Bir işlevin değişkenlerine değer aktarırken bunları const olarak
belirtebilirsiniz. Şöyle ki:
void f1(const int i){
i++ ; //derleme zamanı hatası ......
}
Şimdi bu ne demek? . f1 işlevinde siz böyle yazarak,f1 işlev değişkeninin
orijinal değerinin değişmeyeceğine söz veriyorsunuz. Bir şekilde değişken
bir değerle aktarıldığı için, hemen orijinal değişken kopyası oluşturulur,
böylece müşteriye verilen söz yerine getirilmiş olur.
İşlev içinde bulunan const, değişkenin değerinin sabit olduğu anlamını
taşır. Aslında const burada işlevi yazan için bir araçtır, işlevi çağıran için
değil.
İşlevi çağıranın kafasını karıştırmamak için const anahtar kelimesi işlevin
değişken listesinde değil, işlevin içinde ki değişken ile birlikte belirtilir.
Bunu göstergeçle yapabilirsiniz, fakat daha tercih edileni ve güzel olanı
referans ile olanıdır. İlerleyen bölümlerden birinde, bu konu ayrıntırları ile
incelenecek. Kısaca; referans sabit bir göstergeçtir, otomatik olarak
referansından kurtulur, böylece sanki bir nesne zarfı gibi olur. Bir referans
oluşturmak için tanımda & işareti kullanılır. Böylece temiz bir referans
aşağıdaki gibi görünür;
void f2(int ic){
const int& i=ic ;
i++ ;
//geçersiz -- derleme zamanı hatası
}
Yine hata iletisi alındı, fakat bu sefer yörel nesnenin const'lığı işlevin
kendi parçası değildir. Sadece işlev uygulamasının içinde anlam taşır.Bu
nedenle kullanıcıdan saklanmıştır.
const değerle geri dönüş
Benzer bir durum geri dönüş değerlerinde de ortaya çıkar.Bir işlevin geri
dönüş değeri const ise;
const int g( ) ;
Burada orijinal değişken (işlev çerçevesinde) sabit olarak bildiriliyor. Ve
yine bir değerle geri dönüş olduğu için, orijinal değer kopyalanır, böylece
geri dönüşten kaynaklanan değişim hiçbir zaman olmaz.
Başlangıçta const belirtimi anlamsız gibi görünebilir. Aşağıdaki örnekte
const'ların bir değerle geri dönüşlerindeki etki eksikliğini açıkça
göreceksiniz.
//:B08:Constval.cpp
//constın değerle geri dönüşü
//yerleşik tipler için anlamı yok
int f3( ){return 1;}
const int f4( ){return 1;}
int main( ){
const int j=f3( ) ; //iyi çalışır
int k=f4( ) ;
//bu da iyi çalışır
}///:~
Yerleşik tipler için const değerle geri dönüşün herhangi anlamı yoktur, bu
nedenle müşteri programcının aklını bununla karıştırmamalısınız, ve bu
nedenle yerleşik tiplerin geri dönüşlerini değerle yaparken, const
kullanmaktan sakınmalısınız.
Değerle const geri dönüş kullanıcı tiplerinde kullanırken önemlidir. Bir
işlev sınıf nesnesini const değerle geri döndürürse, o işlevin geri dönüş
değeri soltaraf değeri olamaz.(yani atama yapılamaz veya başka şekilde
değiştirilemez)
Örnek;
//:B08:ConstReturnValues.cpp
//değerle sabit geri dönüş
//sonuç soltaraf değeri olarak kullanılamaz
Class X{
int i;
public:
X(int ii=0) ;
void modify( ) ;
};
X::X(int ii){i=ii;}
void X::modify( ){i++;}
X f5( ){
return X( );
}
const X f6( ){
return X( );
}
void f7(X& x){ //sabit olamayan referansla aktarım
x.modify( ) ;
}
int main( ){
f5( )=X(1) ;
//tamam
f5( ).modify( ) ; //tamam
//derleme zamanı hatası
//! f7(f5( )) ;
//! f6( )=X(1) ;
//! f6.modify( ) ;
//! f7(f6( )) ;
}///:~
f5( ) işlevi constX nesnesinin sabit olmayanını geri döndürür, bu sırada f6(
), constX nesnesini geri döndürür. Sadece sabit olmayan geri dönüş,
soltaraf değeri taşır.Bu nedenle const kullanımı geri dönüş değerinin sol
taraf değeri olarak kullanılmını engellemek amacını taşır. Kullanıcı tanımlı
tiplerin nesne geri dönüşünü sadece değerle yaparsanız onu bir unsur
yaparsınız.
f7( ) işlevinin değişkeni const olmayan bir referanstır. ( C++ da adresleri
kullanmanın bir yolu olup ilerleyen bölümlerden birinde ayrıntıları ile
anlatılacaktır.). Bunun etkisi const olmayan göstergeçle aynıdır, sadece
yazım biçimi farklıdır. Bunun nedeni C++ da geçici ortam oluşumu
dolayısı ile derleyici onu derlemez.
Geçici nesneler
Bazı durumlarda, ifadenin değeri hesaplanırken derleyici mutlaka geçici
nesneler üretmek zorundadır. Bu nesneler de diğer nesneler gibidir;
onlarda bellekte yer kaplarlar, ve mutlaka hem yapıcı hem de yıkıcı işlev
kullanırlar. Siz onları görmezsiniz, farklılık da buradan kaynaklanır,
derleyici onların varlıklarının ayrıntılarına ve gereksinimine karar
verilmesinden sorumludur. Fakat geçiciler hakkında bilinmesi gereken bir
şey daha var; onlar otomatik olarak const'tır.
Genellikle geçici nesnelere ellerinizi değdiremeyeceğiniz için, geçicileri
değiştirecek bir işlem yapmaya kalktığınızda
mutlaka bir hata olacaktır, zira o bilgi kullanılamaz. Böyle bir hata
yaptığınız da, bütün geçiciler otomatik olarak const
yapıldığı için, derleyici hata hakkında sizi bilgilendirir.
Yukarıdak örnekte f5( ), constX in sabit olamayan nesnesini geri
döndürür .Fakat aşağıdaki ifadede
f7(f5( )) ;
Derleyici burada f5( ) geri dönüş değerini tutmak için geçici bir nesne
üretmek zorundadır, böylece bu f7( ) işlevine aktarılmış olur. f7( ) işlevi,
değişkeni değerle alsaydı çok daha iyi olurdu; daha sonra geçici nesne f7( )
ye kopyalanacaktı, ve geçici X e ne olduğu önemli olmayacaktı. Bir
şekilde f7( ) işlevi değişkeni referansla alır, bu örnekteki anlamı geçici X
in adresini almaktır. Madem ki f7( ) değişkenini const referansla almadı,
geçici nesneyi değiştirme izni olur. Fakat derleyici bilir ki ifadenin değeri
hesaplanır hesaplanmaz, geçici hemen ortadan kalkar, ve böylece geçici X
e yapılan herhangi bir değişiklik kaybolur. Bütün geçicilerin otomatik
olarak const yapılmasından dolayı bu durum derleyici zamanı hatası
oluşturur, ve tarafınızdan bulunması çok zor hataları sisteme yerleştirmiş
olabilir.
Aşağıdaki geçerli ifadeleri dikkatle inceleyeniz;
f5( )=X(1) ;
f5( ).modify( ) ;
Derleyici için bu geçişler toplanmasına rağmen, aslında sorun
kaynağıdırlar. f5( ) X nesnesi geri döndürür, ve derleyici için yukarıdaki
ifadeleri işlemlemek amacı ile, mutlaka geçici nesne oluşturulur ve burada
geri dönüş değeri saklanır. Böylece her iki ifadede geçici nesne değiştirilir,
ifadenin işlemi tamamlanır tamamlanmaz geçici nesne derhal silinir. Sonuç
olarak, değişiklikler ortadan kalktığı için geride kalan kodlar büyük
ihtimalle hatalıdır. Ve derleyici bu hata ile ilgili herhangi bir bilgi vermez.
Yukarıdaki ifadeler benzerlerine rastladığınız da sorun kaynağını saptamak
sizin için yeteri kadar basittirler, fakat daha karmaşık ifadeler olduğunda
bu hataları görmek oldukça zordur ve bu hatalar programın çatlakları
olarak kalır. Sınıf nesnelerinin const'lığını korumanın yolu bölümün
ilerleyen kısımlarında anlatılacak.
Adreslerin aktarımı ve geri dönüşü
Bir adresi aktarır ya da geri döndürürken (ya göstergeç ya da referans
olarak), müşteri programcının onu alması ve orijinal değerini değiştirmesi
mümkündür. Bir göstergeç ya da referans const yapılırsa bunu
engellemelisiniz, zira bazı üzüntülü durumlar yaşanabilir. Aslında bir
şekilde bir işleve bir adres aktarılması hiç mümkün değilse, onu const
yapmanız lazımdır.
Göstergeç ya da referansı const olarak geri döndürme seçimi, müşteri
programcının onunla yapacaklarına verilen izine bağlıdır. Aşağıdaki
örnekte const göstergeç, işlev değişkeni ve geri dönüş değeri olarak
kullanılırken görülecektir.
//:B08:ConstPointer.cpp
//değişken ve geri dönüş olarak sabit göstergeç kullanımı
void t(int*){ }
void u(const int* cip){
//!cip=2 ; //geçersiz-- değeri değiştiriyor
int i=*cip ; //geçerli -- kopyalama yapar
//! int* ip2=cip ; //geçersiz-- sabit değil
}
const char* v( ){
//statik karakter dizi adresini geri döndürür
return "return result of function v( )" ;
}
const int* const w( ){
static int i ;
return &i ;
}
int main( ){
int x=0 ;
int* ip=&x ;
t(ip) ; //tamam
//! t(cip) ; //geçersiz
u(ip) ; //tamam
u(cip) ; //tamam
//! char* cp=v( ) ; //geçersiz
const char* cpp=v( ) ; //tamam
//! int* ip2=w( ) ; //geçersiz
const int* const ccip=w( ) ; //geçerli
const int* cip2=w( ) ;
// geçerli
//! *w( )=1 ;
//geçersiz
}///:~
t( ) işlevi sabit olmayan normal göstergeçi değişken olarak alır, ve u( ) ise
const göstergeçi değiken olarak alır.u( ) işlevinin içinde yer alan
değişkenin hedefini değiştirmeye kalkmak geçersiz bir işlemdir, fakat tabii
o sabit olmayan bir değişkene kopyalanabilir. Derleyici sabit bir
göstergeçin gösterdiği yerdeki adresi kullanarak, sabit olmayan bir
göstergeç oluşturmanızı engeller.
v( ) ve w( ) işlevleri geri dönüş değerlerini sınar, v( ) işlevi karakter dizisi
ögelerinden üretilen const char* geri döndürür. Bu deyim aslında karakter
dizisi ögesinin adresini üretir, derleyici onu oluşturduktan sonra statik
bellek alanında saklar. Daha önce anlatıldığı gibi, bu karakter dizisi teknik
olarak sabittir, ve v( ) nin geri dönüş değeri ile uygun şekilde açıklanır. w
( ) işlevinin geri dönüş değeri, hem göstergeçi hemde sabit olarak
işaretlemek zorunda olduğu bilgiyi ister. v( ) ile birlikte, w( ) işlevinden
geri dönen değerin geçerliliği sadece statik olmasından kaynaklanır.Hiç
bir zaman göstergeçler yörel yığıt değişkenlerine döndürülmek istenmez,
zira onlar işlev geri döndürüldükten ve yığıt silindikten sonra, geçersiz
olurlar. (ikinci ortak göstergeçte, kümede yerleştirilmiş bellek yeri adresi
geri döndürülür, ki bu hala işlev geri dönüşünden sonra da geçerlidir.)
main( ) de işlevler farklı değişkenlerle sınanırlar. Gördüğünüz üzere t( )
const olmayan göstergeçi değişken kabul eder,
fakat eğer siz onu göstergeç olarak bir const'a aktarmaya çalışırsanız, t( )
işlevi göstergeçi varış yerinde yalnız bırakmaya söz vermez, böylece
derleyici hata iletisi üretir, u( ) işlevi const göstergeç alır, böylece her iki
tip değişken kabul edilir. Bundan dolayı const göstergeç alan bir işlev
almayanlardan daha geneldirler.
Beklendiği gibi, v( ) işlevinin geri dönüş değeri sadece const olmuş bir
göstergeçe atanabilir. Ayrıca beklediğiniz gibi,
derleyici w( ) işlevinin geri dönüş değerini const olmayan göstergeçe
atamayı kabul etmez., ve const int* constı kabul eder, fakat biraz şaşırtıcı
olan bir şey de const int* i de kabul etmesidir, ki bu geri dönüş tipine tam
uygun değildir. Yine değerin kopyalanmasından ötürü söz verilip
iliştirilmemiş olan orijinal değişken korunur. Böylece const int* const taki
ikinci const solyan değeri olarak kullanmaya çalışırsanız sadece
anlamlıdır, bu derleyicinin sizi engellediği durumdur.
Standart değişken aktarımı
C dilinde değerle aktarım oldukça yaygındır, ama bir adres aktarmak
isterseniz, tek seçenek göstergeç kullanmaktır. C++ dilin de ise, bu
yaklaşımların hiçbiri tercih edilmez. Bunların yerine ilk tercih bir
değişkeni aktarmak istediğiniz de referans kullanılır, ve orada referans
const yapılır. Müşteri programcıya göre buradaki durum; yazım biçimi
olarak değerle aktarımın aynıdır, bu sayede göstergeçlerle ilgili karışıklık
olmaz,- hatta göstergeçleri düşünmek zorunda kalmazlar. İşlev geliştiricisi
için adresin sanal olarak aktarımı, sınıf nesnesinin tamamının aktarımından
her zaman çok daha verimlidir. Ve eğer const referansla aktarırsanız,
bunun anlamı; işlev o adresin varış yerini değiştirmeyecek demektir, ve
böylece müşteri programcının bakış açısından etki, değerle aktarımın
birebir aynı olur.( sadece daha verimli)
Referansların yazım biçimi nedeni ile (çağırana değerle aktarıma benzer),
const referans alan bir işleve geçici nesne aktarmak mümkündür. Böyle bir
işlev de göstergeç bulunuyosa bilindiği gibi hiç bir zaman geçici nesne
aktarılamaz, ki göstergeç olduğunda adres mutlaka açık bir şekilde
bildirilmek zorundadır. Böylece referansla aktarım C de hiçbir zaman
olmayan bir durum ortaya çıkarır; geçici durum, her zaman sabit olan
işleve aktarılacak adrese sahip olabilir. Bunun nedeni işleve referansla
aktarılacak geçicilere izin vermek içindir, değişken mutlaka const referans
olamak zorundadır, bu unutulmamalıdır. Aşağıdaki örnek bunu
göstermektedir;
//:B08:ConstTemporary.cpp
//sabit olan geçiciler
class X{ }
X f( ){return X( );}
//değerle geri dönüş
void g1(X&){ }
//sabit olmayan referansla aktarım
void g2(const X&){ } //sabit referansla aktarım
int main( ){
//Hata: f( ) işlevi tarafından sabit geçici oluşturuldu
//! g1(f( )) ;
//doğru işlem: g2 tarafından sabit geçici oluşturuldu
g2(f( )) ;
}///:~
f( ) class X in bir nesnesini değerle geri döndürür. Bunun anlamı; f( ) in
geri dönüş değerini hemen aldığınız da, ikinci bir işleve aktarırsınız, g1( )
ve g2( ) çağrılarına yerleştirmek gibi, bir geçici oluşur ve bu geçici const
dır. Böylece
g1( ) çağrısı hatalıdır, zira g1( ) const referans almamaktadır, fakat g2( ) ye
çağrı doğrudur, zira o const referans almaktadır.
Sınıflar
Bu bölümde sınıflarla const kullanımını göstereceğiz. Sınıf içinde, derleme
sırasında hesaplanacak sabit açıklamaların içinde kullanılacak yörel const
oluşturabilirsiniz. Sınıf içindeki const'ların anlamı bir şekilde farklıdır, bu
nedenle const sınıf veri üyeleri oluştururken seçenekeleri mutlaka iyi
kavramalısınız.
Ayrıca nesnenin tamamını da const yapabilirsiniz.( biraz önce gördüğünüz
gibi derleyici geçici nesneleri de const yapar) .Fakat nesnenin sabitliğini
sürdürmek oldukça karmaşıktır. Yerleşik veri tiplerinin sabitliğini derleyici
emniyete almıştır, fakat bunlar bir sınıfın giriftliğini göstermezler. Bir sınıf
nesnesinin sabitliğini emniyete almak için, const üye işlev tanıtılır, const
nesne için sadece const üye işlev çağrılır.
Sınıflardaki const'lar
Sabit ifadelerde const'ların kullanılacağı yerlerden biri, sınıfların içidir.
Tipik örneklerden biri; sınıf içinde bir dizi oluşturmak istediğiniz zaman
dizi büyüklüğünü ve dizi ile ilgili hesaplamalarda #define yerine const
kullanımı tercih edilir. Dizi büyüklüğü genelde sınıf içinde saklı tutulmak
istenen bir bilgidir, böylece örneğin size gibi bir adı başka bir sınıfta
değişken adı olarak kullansaydınız, isim çatışması olmazdı. Önişlemleyici
ise bütün #define ları tanımlandıkları yerden itibaren global olarak
işlemler, dolayısı ile bu istenen etkiyi yaratmaz.
Sınıf içini const kullanımı için mantıklı tercih olarak kabul edebilirsiniz.
Bu istenen neticeyi sağlamaz.Bir sınıfın içinde kullanılan const, kısmen C
dilindeki anlamına döner. Her nesnede saklama yapılır, ilk değer ataması
bir kez yapılır ve bir daha değiştirilemez. Sınıf içinde kullanılan const'ın
taşıdığı anlam; "nesnenin ömrü boyunca bu sabittir". Her farklı nesne için
sabitin değeri farklı olabilir.
Böylece bir sınıfın içinde normal (statik olmayan) bir const
oluşturduğunuz zaman, ilk değer atamasını yapamazsınız.
İlk değer atama işlemi yapıcı işlev tarafından yerine getirilmek zorundadır,
tabiiki yapıcı işlevin özel bir bölgesinde.
Zira bir const, mutlaka oluşturulduğu yerde ilk değer atamasının yapılmak
zorunluluğundan ötürü, yapıcı işlevin ana gövdesinde const ilk değer
ataması olmalıdır. Akis takdirde yapıcı işlevin ilerleyen bölümlerinde ilk
değer ataması gerçeklenirse bir süre const, değeri bulunmayan durumunda
kalır. Burada bir şey daha belirtelim; yapıcı işlevin değişik bölümlerinde,
const değeri farklı değerler olabilir, bunda bir sakınca yoktur.
Yapıcı işlevin ilk değer atama listesi
İlk değer atamasının özel yeri, yapıcı işlevin ilk değer atama listesidir,ve
bu işlem özellikle kalıtım da kullanmak için geliştirilmişti.(14. bölümde
irdelenecek). Yapıcı ilk değer atama listesi-adında anlaşılacağı gibi sadece
yapıcı tanımının içinde bulunur- "yapıcı çağrımlarıdır" ve işlev değişken
listesi ve iki nokta üstüsteden sonra sonra fakat yapıcı işlev ilk zincirli
ayracından önce olur. Bu size şunu hatırlatmalı; listedeki ilk değer ataması,
ana yapıcı kodlardan herhangi biri işlemlenmeden önce yerine getirilir.
Burası bütün const ilk değer atamalarının yapıldığı yerdir. Bir sınıf içinde
uygun const yerleşim örneği aşağıda verilmiştir.
//:B08:ConstInitialization.cpp
//sınıflarda const ilk değer atamaları
#include <iostream>
using namespace std ;
class Fred{
const int size ;
public:
Fred(int sz) ;
void print( );
};
Fred::Fred(int sz) : size(sz){ }
void Fred::print( ){cout<<size<<endl ;}
int main( ){
Fred a(1),b(2),c(3) ;
a.print( ),b.print( ),c.print( ) ;
}///:~
Yapıcı işlev ilk değer atama listesinin yukarıda gösterilen biçimi
başlangıçta kafa karıştırabilir, zira yapıcıya sahipmiş gibi yerleşik veri
tiplerin işlemlenmesi henüz gösterilmedi.
Yerleşik veri tiplerindeki "yapıcılar"
Dil geliştirilirken kullanıcı tanımlı veri tiplerini yerleşik veri tiplerine
benzetmek için çok çaba sarfedildi, bazende yerleşik veri tiplerini kullanıcı
tanımlılara benzer yapmak daha yararlıydı. Yapıcı ilk değer atama
listesinde yerleşik veri tiplerini sanki yapıcıları varmış gibi
işlemleyebilirsiniz, aşağıda bir örnek verilmiştir;
//:B08:BuiltInTypeConstructors.cpp
#include <iostream>
using namespace std ;
class B{
int i ;
public:
B(int ii) ;
void print( ) ;
};
B::B(int ii) : i(ii){}
void B::print( ) {cout<< i <<endl ;}
int main( ){
B a(1),b(2) ;
float pi(3.14159) ;
a.print( ); b.print( ) ;
cout<<pi<<endl ;
}///:~
Veri üyelerinin const olanlarına ilk değer ataması yapıldığı zaman bu özel
önem taşır zira, onların ilk değer atamaları mutlaka işlev gövdesine
girilmeden önce yerine getirilmelidir.
Yapıcı işlev yerleşik tipede uygulanarak genelleştirildi (basit anlamda
atama işlemi), böylece float pi(3.14159) tanımı yukarıdaki kodda
çalışabilir hale geldi.
Çoğu zaman yerleşik veri tiplerini bir sınıfa sarmalayarak, o sınıfın yapıcı
işlevi ile ilk değer atamasını gerçeklemek daha kullanışlıdır. Integer sınıfı
ile yapılmış bir örnek aşağıda verilmiştir,
//:B08:EncapsulatingType.cpp
#include <iostream>
using namespace std ;
class Integer{
int i ;
public:
Integer(int ii=0) ;
void print( ) ;
};
Integer::Integer(int ii) : i(ii){}
void Integer::print( ){cout<<i<<' ' ; }
int main( ){
Integer i[100] ;
for(int j=0;j<100;j++)
i[j].print( ) ;
}///:~
main( ) işlevinde bulunan Integer dizisine tamamen otomatik olarak sıfır
ilk değer ataması yapılır. İlk değer ataması işlem maliyetinin for
döngüsünden veya memset( ) işlevinden fazla olması gerekmez. Birçok
derleyici bu işlemi hızlı süreçlerle kolaylıkla iyileştirmektedir.
Sınıflarda ki derleme zamanı sabitleri
Yukarıdaki biçimde const kullanımı hem ilginç hemde bir çok durumda
oldukça kullanışlıdır. Fakat "derleme sırasında kullanılan sabitler sınıfa
nasıl yerleşecek" ona yanıt vermemektedir. Yanıt aslında ilerleyen
bölümlerde ayrıntıları anlatılacak olan anahtar kelime; static. static
anahtar kelimesi böyle durumlarda, sınıfın kaç tane nesne oluşturduğuna
bakılmaksızın, sadece bir hal var demektir.Tamamen gereksinim
duyulmasından kaynaklanır, ve sınıfın bir üyesi bir nesneden diğerine
değişmeyecek demektir.static anahtar kelimesi sınıfın bir üyesinin, o
sınıfın nesneleri için sabit olduğu anlamını taşır. Bu sayede yerleşik veri
tipinin static const olanları derleme zamanı sabitleri olarak işlemlenebilir.
static const sınıf içinde kullanılırken bir özelliği biraz alışılmadık
gelebilir; static const tanımının yapıldığı yerde mutlaka ilk değer atama
işlemi yapılmak zorundadır. Bunlar sadece istenildiği kadar kullanılan
durumlarda ortaya çıkar zira başka durumlarda çalışmaz, çünkü bütün öbür
veri üyelerinin mutlaka ilk değerleri, ya yapıcı işlev ya da diğer veri
üyelerinin içinde atanmalıdır.
Şimdi size bir sınıf içinde size adı verilen static const oluşumu ve
kullanımını, ki string göstergeç yığıtını temsil eder- bir örnek vereceğiz .
(bunu bazı derleyiciler desteklemeyebilir)
//:B08:StringStack.cpp
//sınıf içinde static const kullanarak derleme zamanı sabiti oluşturmak
#include <string>
#include <iostream>
using namespace std ;
class StringStack{
static const int size=100 ;
const string* stack[size] ;
int index ;
public:
StringStack( ) ;
void push(const string* s) ;
const string* pop( ) ;
};
StringStack::StringStack( ) : index(0){
memset(stack,0,size * sizeof(string*));
}
void StringStack::push(const string* s){
if(index<size)
stack[index++]=s ;
}
const string* StringStack::pop( ){
if(index>0){
const string* rv=stack[--index] ;
stack[index]=0 ;
return rv ;
}
return 0 ;
}
string iceCream[ ]={
"pralines & cream ",
"fudge ripple ",
"jamodgga almodge fudge",
"wild mountain blacberry",
"rasperry sorbet",
"lemon swirl " ,
"rocky road" ,
"deep choclate fudge"
};
const int iCsz=sizeof iceCream/sizeof *iceCream ;
int main( ){
StringStack ss;
for(int i=0;i<iCsz;i++)
ss.push(&iceCream[i]) ;
const string* cp ;
while((cp=ss.pop( ))!=0)
cout<<*cp<<endl ;
}///:~
stack dizisinin büyüklüğü size tarafından belirlendiği için, size derleme
zamanı sabiti olarak tanımlanmıştır, fakat sınıf içinde saklanmıştır.
Gördüğünüz üzere push( ) işlevi const string* i değişken olarak almış,
pop( ) işlevi bir const string* geri döndürmüş ve StringStack,const
string* tutmuştur. Bu doğru olmasaydı, iceCream de bulunan
göstergeçleri StringStack ile tutamazdınız. Bunun yanında ayrıca,
StringStack te kılıflanmış nesnelerin değişmesine yolaçacak eylemler
yapılmasınıda önler. Tabii doğaldır ki bütün kılıflar aynı sınırlamalara
sahip değildirler.
Eski kodlarda "enum yakalama"
C++ dilinin ilk sürümlerinde, static const sınıf içi kullanımı
desteklenmemişti. Bunun taşıdığı anlam; sınıf içinde sabit açıklamalarda
const kullanımı yararsız demektir. Bunun yanında bir çok kişi bu işi hala
tipik bir çözümle ( genellikle "enum enseleme" denilir), uzantısız enum
kullanarak hallediyordu. Numaralandırma işleminde değerler mutlaka
derleme sırasında yerleştirilmek zorunluluğundadır, sınıfa göre yöreldirler,
ve değerleri sabit açıklamalar için sağlanırlar. Şimdi yaygın kullanım
örneklerinden birini görelim;
//:B08:EnumHack.cpp
#include <iostream>
using namespace std ;
class Bunch{
enum{size=1000};
int i[size] ;
};
int main( ){
cout<<"sizeof(Bunch)= "<<sizeof(Bunch) ;
<<", sizeof(i[1000])= "
<<sizeof(int[1000])<<endl ;
}///:~
Burada enum kullanımı nesnede herhangi bir yer kaplamamayı güvence
altına alıyor, ve numaralayıcılar her zaman derleme sırasında değer atama
işlemini gerçeklemektedirler. Bunun yanında ayrıca numaralandırmayı
açık bir şekilde de yapabilme olanağı vardır;
enum{bir=1,iki=2,uc};
Tümleşik enum tiplerde derleyici son değerden itibaren saymaktadır, bu
nedenle uc için aldığı değer 3 tür.Yukarıdaki
StringStack.cpp örneğinde aşağıdaki satır;
static const int size=100 ;
aşağıdaki gibi olabilirdi;
enum{size=100};
Geçerli kodların bir çoğunda enum tipleri görecek olmanıza rağmen static
const özelliğinin dile eklenmesi sadece bazı sorunları ortadan kaldırmak
içindi. Bunun başka bilinmeyen daha üstün özellikleri de yoktur, bu
nedenle biz static const yerine enum uygulamasını tercih edeceğiz, zira bu
kitabın yazıldığı süreçte çok sayıda derleyici enum tipleri
desteklemekteydi.
Sabit nesneler ve üye işlevler
Sınıf üye işlevlerini const yapabiliriz. Peki bunun anlamı ne?. Bunu
anlamak için önce const nesneleri kavramalısınız.
Kullanıcı tanımlı nesnelerin const olanları aynı yerleşik veri tiplerindeki
gibidir.;
const int i=1 ;
const blob b(2) ;
Burada b, blob tipinde sabit bir nesnedir, ve ilk değer ataması 2 ile yapıcı
işlev tarafından yerine getirilir. Derleyici sabitliği güçlendirmek için
nesnenin hiçbir veri üyesini, nesnenin ömrü boyunca değiştirilmemelidir.
public veriler kolaylıkla değiştirilmeden bırakılabilirler.Peki o zaman
hangi üye işlevlerin veri değişimini sağlayacağı nasıl bilinecek,
ve hangileri const nesneler için "güvenli" olacaklar.
Bir üye işlevi const olarak bildirdiğinizde, derleyiciye siz o işlevi sadece
const nesne için çağıracağınızı söylüyorsunuz.
Bir üye işlev özellikle const olarak bildirilmemişse, siz o işlevi const
olmayan herhangi bir nesnede ki veri üyelerini değiştirmekte
kullanabilirsiniz, fakat siz onu const nesne için çağıramazsınız.
Bu iş burada bitmeyecek. Bir şekilde üye işlevin const olmasını istemek
onun hemen o şekilde davranmasını sağlamaz.
İşlevi tanımlarken de, derleyici sizden tekrar özelliği yinelemenizi mecbur
bırakır, (öyleki const işlevin imzası olur, ve hem derleyici hem de ilişimci
sabitliği denetler). Daha sonra işlev tanımı esnasında sabitlik daha
güçlendirilir, ve eğer nesnenin üyelerinden herhangi biri değiştirilmeye
çalışılırsa ya da sabit olmayan işlev çağrılırsa, hata iletisi üretilir. Böylece
const olarak bildirilmiş üye işlev davranışlarını tanımda da sürdürürür.
Üye işlevlerin const bildirimlerindeki yazım biçimin de, önce const üye
işlev bildiriminin başına konur, bunun anlamı geri dönüş değeri sabit
demektir, böylece istenen sonuçlar elde edilmez. Onun yerine, const
belirteci değişken listesinden sonra konulmalıdır.Örnek;
//:B08:ConstMember.cpp
class X{
int i ;
public:
X(int ii) ;
int f( ) const ;
};
X::X(int ii) : i(ii){ }
int X::f( ) const{return i;}
int main( ){
X x1(10) ;
const X x2(20) ;
x1.f( ) ;
x2.f( ) ;
}///:~
Burada hemen belirtelim const, tanımda da mutlaka yinelenmelidir, aksi
takdirde derleyici farklı bir işlev olarak işlemler. f( ) işlevi const üye işlevi
olduğu için, herhangi bir şekilde i değeri değiştirilecek olursa, ya da const
olmayan başka bir üye çağrılacak olursa, derleyici bunu hata olarak bayrak
ile bildirir.
Gördüğünüz gibi const üye işlev çağrımı, hem const hem de const
olmayan nesnelerle güvenlidir. Böylece bunun, üye işlevlerin en yaygın
biçimi olduğunu kabul edebilirsiniz. (bu nedenle üye işlevler maalesef
otomatik olarak const olarak varsayılmamıştır.). Üye verileri
değiştirmeyen herhangi bir işlev, const olarak bildirilmesi lazımdır,
böylece kolaylıkla const nesnelerle kullanılabilirler.
Şimdi const olan ve const olmayan üye işlevlerin farklarını aktaran bir
örnek verilecek;
//:B08:Quoter.cpp
//gelişigüzel tırnaklayıcı seçici
#include <iostream>
#include <cstdlib>
//gelişigüzel sayı üreteci
#include <ctime>
//gelişigüzel sayı kökü
using namespace std ;
class Quoter{
int lastquote ;
public:
Quoter( ) ;
int lastQuote( ) const ;
const char* quote( ) ;
};
Quoter::Quoter( ){
lastquote=-1 ;
srand(time(0)) ;
//sayı kökü
}
int Quoter::lastQuote( ) const{
return lastquote;
}
const char* Quoter::quote( ){
static const char* quotes[]={
"henüz eglence varmı?",
"doktorlar en iyisinimi bilir?",
"atomik boyuttami?",
"korku bellidir",
"bilimsel delil yok",
"fikir desteklenecek",
"ki hayat ciddidir",
"mutlu edenler akıllı da yaparlar",
};
const int qsize=sizeof quotes/sizeof *quotes;
int qnum=rand( )%qsize;
while(lastquote>=0&&qnum==lastquote)
qnum=rand( )%qsize;
return quotes[lastquote=qnum];
}
int main( ){
Quoter q;
const Quoter cq;
cq.lastQuote( ); //tamam
//! cq.quote( );
//geçerli değil zira const işlev değil
for(int i=0;i<20;i++)
cout<<q.quote( )<<endl;
}///:~~
Ne yapıcı ne de yıkıcı işlev const üye işlev olamazlar. Bunun sebebi nesne
üzerinde ilk değer ataması ve silinmesi sırasında, her zaman sanal olarak
bazı değişiklikler yapmalarıdır. quote( ) üye işlevi lastquote veri üyesi
üzerinde değişiklik yaptığı için const olamaz (return deyimine bakınız).
lastQuote( ) üye işlevi ise herhangi bir değişiklik yapmadığı için const
olabilir ve const nesne cq tarafından güvenle çağrılabilir.
sesi kesilenler: bit düzenine karşılık mantık sabitleri
Nesnelerdeki bazı verileri değiştirmeye izin verecek bir const üye işlev
oluşturulmak istenirse ne olur?. Bu biraz bitwise const ile logical const
(memberwise const olarakta adlandırılır) arasındaki farka benzer. Bit
düzenindeki sabitte nesnedeki her bit kalıcı ve değişmezdir, ve nesnenin
bit görüntüsü hiçbir zaman değişmez. Logical const'ta ise nesnenin
bütününde fikirsel olarak değişmezlik vardır fakat üyeden üyeye temelinde
değiştirilebilir özellikleri vardır.
Bunun yanında derleyiciye bir nesnenin sabit olduğu söylenirse, derleyici
onu kıskançlıkla bitwise const olarak korumaya alır. Mantık
değişmezliğini sağlarken ise, const üye işlevin içinden veri üyeyi
değiştirmek için iki yol vardır.
İlk yaklaşım en eski olan değişmezliği uzaklaştırmaktır. Ve daha ziyade
tek bir yolla yerine getirilir. this anahtar kelimesini alıp bunu o anki nesne
adresini belirleme de kullanmak için o anki tipin nesnesine göstergeç
olarak atanır. this bir göstergeç olarak görünmektedir. Sabit bir üye işlevin
içindeki this, gerçekten sabit bir göstergeçtir. Ve eğer siz onu normal bir
göstergeçe dökerseniz, o işlem için sabitliğini kaldırmış olursunuz. İşte bir
örnek;
//:B08:Castaway.cpp
//"değişmezliği uzaklaştırmak"
class Y{
int i;
pubic:
Y( );
void f( ) const ;
};
Y::Y( ){i=0;}
void Y::f( ) const{
//! i++ ;
//hata-- sabit üye işlev
((Y*)this)->i++; //tamam, değişmezlik uzaklaştırıldı
//daha iyi C++yazım biçimi kullanılabilir.
(const_cast<Y*>(this))->i++ ;
}
int main( ){
const Y yy;
yy.f( ) ;
///gerçekten değişir.
}///:~
Yukarıdaki yaklaşım çalışır kod yazmayı sağlayabilir, ve siz onları geçerli
kodlar arasında görebilirsiniz, fakat genellikle pek tercih edilen bir teknik
değildir. Sorun; const eksikliğinin, üye işlev tanımında saklanarak
uzaklaştırılmasıdır, ve kaynak koda erişemediğiniz de nesne verileri
gerçekten değiştiğince sınıf arayüzünden herhangi bir ipucu elde
edemezsiniz. (ve mutlaka const uzaklaşmasından şüphelenmelisiniz ve
yeni dökümü aramalısınız.).
Her şeyi aleni yapmak için de mutable anahtar kelimesini sınıf
bildiriminde kullanmalısınız. Böylece const bir nesne içinde belli bir veri
üyesinin değişimine izin verilebilir.
//:B08:Mutable.cpp
//"mutable" anahtar kelimesi
class Z{
int i ;
mutable int j ;
public:
Z( ) ;
void f( ) const ;
};
Z::Z( ) : i(0),j(0){ }
void Z::f( ) const{
//!i++; // hata const üye işlev
j++ ;
//mutable
}
int main( ){
const Z zz ;
zz.f( ) ; //gerçekten değişir
}///:~
Bu yöntemle sınıf kullanıcısı const üye işlev içinde neleri
değiştirebileceğini bildirimden görebilir.
ROMlanabilirlik
Bir nesne const olarak tanımlanırsa o, sadece oku belleğine (ROM)
yerleşmeye aday demektir. Bu uygulama tekleşik dizge
programlamalarında önemlidir. (embedded system programming). Aslında
bir nesneyi basit bir şekilde sadece const yapmakta ROMlamak için yeterli
değildir, bunun oldukça katı kuralları vardır. Tabii nesne, mantık
yönlenmeli const yerine mutlaka bit yönlenmeli const olmalıdır. Eğer
mantık yönlenmeli const mutable ile sağlanırsa bunu anlamak oldukça
kolaydır, fakat eğer const üye işlev içine döküm yapılarak sabitlik
uzaklaştırılırsa büyük olasılıkla derleyici tarafından algılanamazlar.
İlaveten;
1-class ya da struct kullanıcı tanımlı yapıcı ve yıkıcı işlevlere sahip
olmamalıdır.
2-Ana sınıflar ya da, kullanıcı tanımlı yapıcı ve yıkıcı işlevli üye nesneler
olmamalıdır.
ROMlanabilir tip bir sabit nesnenin herhangi bir parçası üzerine yazma
eyleminin etkisi tanımsızdır. Uygun şekile sokulmuş nesne ROM a
yerleştirilebilir olmasına rağmen, hiçbir nesne ROMa sonsuza kadar
yerleştirilmek istenmez.
Uçucu (volatile)
Volatile anahtar kelimesinin yazım biçimi const gibidir, fakat anlamı "bu
veri derleyicinin bilgisi dışında değişebilir" demektir. Bir şekilde çevredeki
bir unsur önceden varolan veriyi değiştiriyor (çoklu görevlendirme, çoklu
akış, ve kesimlerle), volatile derleyiciye, özellikle iyileştirme esnasında
veri ile ilgili herhangi bir kabul yapmamasını bildirir.
Eğer derleyici " daha önceden bu veriyi yazmaçtan okudum ve bu yazmaca
dokunmadım" derse normal olarak bu veriyi tekrar okumaya gerek yoktur.
Ama eğer veri volatile ise derleyici böyle bir kabul yapamaz, zira veri
başka bir süreç tarafından değiştirilebilir, veri mutlaka yeniden
okunmalıdır burada kod iyileştirilmesinden ziyade doğru okuma dikkate
alınmıştır.
volatile nesneler aynı const nesneler oluşturulurken kullanılan yazım
biçimi ile oluşturular. Bunların yanında const volatile nesneler de
oluşturulabilir. const volatile nesneler müşteri programcılar tarafından
değiştirilemez fakat dışarıdaki bazı birimler tarafından değiştirilebilir.
Aşağıdaki örnek iletişim donanımı ile ilintili bir sınıfı göstermektedir;
//:B0:Volatile.cpp
//volatile anahtar kelimesi
class Comm{
const volatile unsigned char byte ;
volatile unsigned char flag ;
enum{bufsize=100};
unsigned char buf[bufsize] ;
int index ;
public:
Comm( ) ;
void isr( ) volatile ;
char read(int index) const ;
};
Comm::Comm( ) : index(0), byte(0), flag(0){}
//sadece bir örnek uygulamada çalışmaz
//kesim servisi akışı
void Comm::isr( ) volatile {
flag=0 ;
buf[index++]=byte ;
//tamponun başlangıcına katla
if(index>=bufsize)index=0 ;
}
char Comm::read(int index) const{
if(index<0|| index>=bufsize)
return 0;
return buf[index];
}
int main( ){
volatile Comm Port ;
Port isr( ); //tamam
//! Port.read(0) ; //hata, zira read( ) volatile değil
}///:~
const uygulamalarında olduğu gibi volatile anahtar kelimesi de; veri
üyeler, üye işlevler ve nesnelerde de kullanılırlar. Volatile üye işlevler
sadece volatile nesneler için çağrılabilirler. Bu kurala uygulamalarda çok
dikkat edilmelidir.
isr( ) işlevinin gerçek bir kesim hizmet uygulaması olarak
kullanılamamasının nedeni, ki üye işlev içindedir, o anki nesne adresinin
(this) gizlice aktarılma zorunluluğudur, bir ISR (interrupt service routine)
genellikle hiçbir zaman bir değişken kullanmaz. Bu sorunu çözmek için isr
( ), static üye işlev haline getirilir. Bu konu 10. bölümde ayrıntıları ile
incelenecek.
volatile ile const her ikisi de aynı yazım biçimlerine sahip oldukları için
birlikte incelenirler. Ve her ikisi birlikte c-v belirteçleri olarak anılırlar.
Volatile anahtar kelimesi hakkında biraz daha ayrıntı verelim. Bugün 2004
lerin sonunda, yarıiletken teknolojilerinde kullanılan silikon
yarıiletkeninde hız sınırı ortaya çıktı. Bu yüzden tek kırmık üzerine birden
fazla işlemci çekirdeği yerleştirerek programları hızlandırma yolu
seçilecektir. Bu yüzden şimdiye kadar genellikle tek denetim akışı olan
programlar (single-threaded), koşut denetim akışlı (multi-threaded)
programlar halinde tasarlanması gerekecektir. İşte aslında tek denetim
akışlı yapı olan C++ diline koşutluk özelliğini kazandıracak olan
deyimlerden biri, belkide en önemlisi volatile anahtar kelimesidir.
Nispeten az bilinen ve az kullanılan anahtar kelimelerden olan volatile
programlama alemine ilk kez ANSI C tip değiştiricisi olarak girdi. Gereken
her sefer veri unsurunu yeniden yüklemesi için derleyicinin kod üretmesini
güvenceye aldı. Volatile mutlaka asenkron olarak değişen değişkenlerle
kullanılmalıdır. Örnek olarak işaret işlemleyicileri veya bellek alanları
belirlenmiş donanımlardan gelen veriler gibi. İşaret/kesim işleyiciler,
denetim akış kodları, araç sürücülerini de içeren çekirdek (kernel) kodların
içinde volatile yaygın olarak kullanılır. Genel olarak bir veri önceden
belirlenmemiş şekilde asenkron olarak değişebiliyorsa, onu volatile olarak
bildirmek lazımdır. Volatile'nin derleyiciye anlattığı; veri derleyicinin
bilgisi dışında değişebilir.
Birkaç volatile gösterim örneği verelim;
volatile int *s1;
//s1 volatile int'e göstergeç olarak
bildirilmiş
int* volatile s2;
//s2 int'e volatile göstergeç olarak
bildirilmiş
volatile int* volatile s3;
//s3 volatile int'e volatile göstergeç
olarak bildirilmiş
const volatile int * volatile s4;
//s4 const volatile int'e volatile
göstergeç olarak bildirilmiş
volatile int * (*f)(volatile int *); //f volatile int'e göstergeç döndüren
işleve (volatile int'e göstergeç) göstergeç bildirmiş
Özet
Nesnelerin, işlev değişkenlerinin, geri dönüş değerlerinin ve üye işlevlerin
sabit olarak tanımlanması sırasında const anahtar kelimesi kullanılır.
Böylece önişlemleyici değer ataması görevinden alınmış olur ve daha
yararlı yerlerde kullanılabilir. const işlemlerinin tamamı tip denetimini de
sağladığı için programlarda güvenlik üst düzeye çıkmış olur. const
doğrulayıcılığı (const, istenen her yerde kullanılabilir) projelerde hayat
kurtarıcı olabilir.
const anahtar kelimesini yeni anlamı dışında, C dili tarzında yani eski
biçimde de kullanabilirsiniz. 11. bölümde dayançla (referansla) atamalar
örneklerle incelenecektir. Anılan bölümde const anahtar kelimesinin işlev
değişkenleri ile kullanımınının ne kadar hassas olduğunu göreceksiniz.
Ayrıca istikbali en parlak anahtar kelimelerden volatile da kısaca anlatıldı.
9. BÖLÜM
Satıriçi işlevler
C++ diline C dilinden gelen en önemli özelliklerinden biri, verimliliktir. C++ dilinin
verimliliği C dilinden az
olsaydı programcılar bu dili kullanmazlardı. C dilinde verimliliği sağlamanın
yollarından biri, makro kullanımıydı. Makro kullanımı, bilinen işlev çağrımı
yapmadan işlev çağrımını andıran bir özelliktir. Makro uygulaması derleyici yerine,
önişlemleyici tarafından yerine getirilen bir uygulamadır. Önişlemleyici, bütün
makro çağrılarını doğrudan makro kodları ile değiştirir, böylece aktarılan
değişkenlerin ek maliyeti olmaz, yani assembly dili ile CALL yapılır, değişkenler
geri döndürülür, ve assembly dili ile RETURN işlemlenir. Bütün işlemler
önişlemleyici tarafından yerine getirilir. Böylece hiçbir maliyeti olmayan işlev
çağrılarının okunabilirliği ve uyumu sağlanır.
C++ da ise önişlemleyici makroların kullanımı ile ilgili iki sorun ortaya çıkar.
Birinci sorun C makroları içinde geçerlidir; makro uygulaması işlev çağrımını
andırır, ama her zaman o biçimde davranmaz. Bu da ayıklanması zor olan hatalara
yolaçar. İkinci sorun ise, sadece C++ diline özgüdür; önişlemleyici hiçbir zaman
sınıf üye verilerine erişim iznine sahip değildir. Bundan dolayı önişlemleyici
makroları, hiçbir zaman sınıf üye işlevleri olarak kullanılamazlar.
Önişlemleyici makro işlemlerinin gerçeklenmesi ve işlevlerde güvenilir bir şekilde
sınıf yaklaşımının uygulanabilmesi için, C++ dilinde satıriçi (inline) işlevler
bulunur. Şimdi C++ dilinde önişlemleyici makro sorunlarını inceleyeceğiz. Bu
sorunların satıriçi işlevler ile giderilmesi ve satıriçi işlev çalışma usullerini
göstereceğiz. Konunun ayrıntılarına girmeden önce hemen belirtelim; inline anahtar
kelimesi bir komut değil, bir istemdir. inline kullanıldığında derleyici istenen
etkinliği yapmak zorunda değildir. Derleyici inline işlevin boyutuna bakar onun
inline olup olamayacağına kendisi karar verir. Sınıfların içinde yer alan üye işlevler
ise otomatik olarak inline'dır. Bundan dolayı sınıf üye işlev başına inline anahtar
kelimesini yerleştirmek gereksizdir. Ama fazla yazı yazmayı seviyorsanız,
yazabilirsiniz(!).
Önişlemleme tuzakları
Önişlemleme makro sorunlarını anlamanın anahtarı, önişlemleyici davranışlarının
derleyicinin davranışlarına benzediğini düşünmektir. Elbette makro, işlev çağrımına
benzer ve onun gibi davranır. Bu nedenle yukarıdaki benzetmeyi düşünmek
doğaldır. Zorluklar, ince farklılıklar olduğu zaman ortaya çıkar.
Basit bir örnek olarak, aşağıdaki örneği inceleyelim;
#define F (x)(x+1)
Şimdi F için bir çağrı yapılsın
F(1)
Önişlemleyici çağrıyı açar, aşağıda gösterildiği gibi beklenmeyen bir şey olur.
(x)(x+1)(1)
Sorun F ten sonra gelen boşluktan kaynaklanmaktadır. Makro tanımı boşluktan
sonra yapılmıştır. Bu boşluk ortadan kaldırıldığı takdirde makroyu boşlukla dahi
çağırabilirsiniz.
F (1)
yine de makro doğru açılır.
(1+1)
Yukarıdaki örnek çok basittir, fakat ilgilendiğimiz sorunu açıkça göstermektedir.
Makro çağrılarında gerçek zorluklar, değişken olarak açıklamalar kullanıldığı
zaman ortaya çıkar.
Burada iki sorun var. Bunlardan ilki, makrodaki açıklamalar açılırken hesaplama
sıralarının beklenenden farklı olduğu görülür. Örnek olarak;
#define FLOOR(x,b) x>=b?0:1
Ve şimdi değişkenler için açıklamalar kullanılırsa
if(FLOOR(a&0x0f,0x07)) ///...
makro aşağıdaki gibi açılır
if(a&0x0f>=0x07?0:1)
& işaretinin işlem önceliği >= den daha düşük seviyede olduğu için makro
işleminden beklenmeyen bir sonuç elde edilir. Sorunu farkettiğiniz zaman, makro
tanımındaki her ögenin etrafına ayraçlar koyarak bunun üstesinden gelebilirsiniz.
(önişlemleyici makrosu oluşturmada iyi bir çalışma olur). Böylece;
#define FLOOR(x,b) ((x)>=(b)?0:1)
Sorunu fark etmek zor olabilir, uygun makro davranışı elde edilene kadar fark
edilemez . Önceki örnekte ayraçsız kullanım da, açıklamaların çoğu düzgün çalışır,
zira >= işlecinin önceliği öteki işleçlerin çoğundan (+,/,-- ve hatta bit kaydırma )
daha düşük düzeydedir. Bunun sonucun da, kolaylıkla bütün açıklamalarla -bit
mantık işleçleri dahil olmak üzere- çalışabileceğini düşünebilirsiniz.
Öncelik sorunu dikkatli bir programlama uygulaması ile çözülebilir; makro daki
herşeyi ayraç içine alarak. İkinci zorluk ise daha ince bir ayrıntıdır. Normal
işlevdekilere benzemeyen şekilde, makrodaki değişkenler kullanıldığı her sefer
hesaplanır.
Normal bir değişkenle makro çağrılır çağrılmaz, bu değerlendirme uygun olabilir,
ama eğer değişkenin hesaplanması yan etkilere sahipse, o zaman sonuç şaşırtıcı
olabilir, kesinlikle işlev taklidi değildir.
Örnek olarak aşağıdaki makro, belli bir aralıkta değişken değerlerinin
kullanılamayacağını göstermektedir.
#define BAND(x) (((x)>5 && (x)<10) ? (x) :0)
normal bir değişken kullanılır kullanılmaz, makro daha çok bir işlev gibi çalışır.
Ama sakinleşip gerçek bir işlev olduğuna inandığınız da, sorun başlar. Böylece ;
//:B09:MacroSideEffects.cpp
#include ".../require.h"
#include <fstream>
using namespace std ;
#define BAND(((x)>5&&(x)<10)?(x):0)
int main( ){
ofstream out("macro.out") ;
assure(out,"macro.out") ;
for(int i=4;i<11;i++){
int a=i ;
out<<"a= "<<a<<endl<<'\t' ;
out<<"BAND(++a)= "<<BAND(++a)<<endl ;
out<<"\t a= "<<a<<endl ;
}
}///:~
Yukarıda bir şeyi daha farketmiş olmalısınız, makro adı büyük harflerle yazılıyor.
Büyük harf tercihi oldukça yararlıdır, zira onun bir işlev değil bir makro olduğunu
anlamayı sağlıyor. Sorun çıktığında makro olduğu hatırlanır. Yukarıdaki programın
çıktısı aşağıda verilmiştir, bunların tamamı gerçek bir işlevden beklenenler
değildir.;
a=4
BAND(++a)=0
a=5
a=5
BAND(++a)=8
a=8
a=6
BAND(++a)=9
a=9
a=7
BAND(++a)=10
a=10
a=8
BAND(++a)=0
a=10
a=9
BAND(++a)=0
a=11
a=10
BAND(++a)=0
a=12
a' nın değeri 4 olduğu zaman, koşulların ilk kısmı yerine getirilir. Böylece açıklama
sadece bir kez hesaplanır, makro çağrımının yan etkileri a'yı 5 yapar. Bu aslında
aynı durumda işlev çağrımı yapılırsa, olması beklenilendir. Sayı ilgili aralıkta ise
her iki koşul da sınanır, bunların neticesinde iki değer artışı elde edilir. Sonuç
değişken yeniden hesaplanarak bulunur, bu da üçüncü artışı sağlar. Sayı aralık
dışına çıktığı zaman, her iki koşul yine de sınanmaya devam eder, böylece iki artış
elde edilir.
Doğal olarak bir işlev çağrısı gibi düşünülen makro çağrısının, yukarıda çıkan
neticeleri istenenler olmadığı açıktır. Kesin çözüm, hepsini gerçek işlev durumuna
getirerek sağlanır. Doğal olarak bu da işlevlerin her çağrımında ortaya çıkan ek
işlemler nedeni ile verimliliği azaltır. Maalesef sorun bu kadar berrak
görünmeyebilir, ve bilmeden aldığınız kütüphane, birlikte hem işlev hem de
makro'lar içerebilir, o zaman böyle durumlarda bu tür sorunlar bulunması zor
hatalara yolaçar. Örnek olarak cstdio da bulunan putc( ) makrosu ikinci değişkeni
iki kez hesaplar. Standart C de bu belirtilmiştir. Bunun yanında toupper( ) ın
makro olarak kullanımı dikkatsiz bir şekilde uygulanırsa, değişken birden fazla
hesaplanır, ve toupper(*p++) neticesi beklenen gibi olmaz.
Makrolar ve erişim
C dilinde önişlemleyici makro kodlaması ve kullanımı dikkat gerektirir, ve doğal
olarak aynı şeyler C++ dilinde de geçerlidir. Bir çözüm getirmek için olmasaydı, o
zaman makro üye işlevlerle birlikte aynı görüntüye sahip olurdu. Önişlemleme basit
bir şekilde metin yerleştirimidir, o nedenle aşağıdaki gibi yazamazsınız;
class X{
int i ;
public:
#define VAL(X::i) //hata
veya benzer olsa bile. Ek olarak hangi nesnenin tanık alınacağına dair de bir işaret
yoktu. Makrolarda sınıf görüntüsünü
açıklayacak basit bir yol bulunmaz. Önişlemleyici makrolarına seçenek olmayacak
şekilde bazı programcılar verimliliği sağlamak için bazı veri üyelerini public
yaparlar. Böylece uygulama açığa çıkar ve private tarafından sağlanan korumaların
ortadan kaldırılması ile değişikliklerin engellenmesi mümkün olmaz .
Satıriçi işlevler
C++ dilinde, makroların sınıfın private üyelerine erişimi ile ilgili sorunu
çözülürken, önişlemleyici makroları ile ilgili bütün sorunlar ortadan kalkar. Bunu da
gerçekleştirmek; makro uygulamasını derleyicinin denetimine bırakarak sağlanır.
C++ dili makro'yu satıriçi işlevi olarak yerleştirir ve uygular. Satıriçi işlevi her
anlamda gerçek bir işlevdir. Normal bir işlevden beklenen her türlü davranışı
satıriçi işlevi de gösterir. Tek fark; satıriçi işlev aynı yerde açılır, yani aynı
makro'lardaki gibi, böylece fazladan işlev çağrımı ortadan kalkar. Bundan dolayı
hemen hemen hiçbir zaman makro kullanılmaz, sadece satıriçi işlev kullanılır.
Sınıf gövdesinde tanımlanmış herhangi bir işlev otomatik olarak satıriçidir. Sınıf
gövdesinde bulunmayan bir işlevi satıriçi yapmak için ise, başına inline anahtar
kelimesini getirmek gerekir.
Bunun etkin olabilmesi için, işlev bildirimi ile birlikte işlev gövdesi de
bulunmalıdır, aksi takdirde derleyici onu sadece işlev bildirimi olarak alır. Böylece;
inline int plusOne(int x) ;
bunun işlev bildiriminden başka anlamı yoktur, zira işlev gövdesi yok. (satıriçi
tanımı bir süre sonra olabilir de, olmayabilir de). Başarılı yaklaşım ise, işlev
gövdesini de yanında bulundurur. Bunun örneği aşağıda verilmiştir.
inline int plusOne(int x){return ++x;}
Derleyici, işlev değişken listesini ve geri dönüş değerinin (her zamanki gibi)
kullanım uygunluğunu, sınayarak denetler.
(gerekli çevrimleri yaparak), bunlar önişlemleyici için gerçeklemesi mümkün
olmayan işlemlerdir. Ayrıca yukarıdaki işlev makro olarak yazılsaydı, istenmeyen
yan etkiler ortaya çıkabilirdi.
Hemen hemen her zaman satıriçi tanımlar başlık dosyalarına yerleştirilmek istenir.
Derleyici böyle bir tanımı gördüğünde onun işlev tipini (geri dönüş değeri ile
birlikte ) ve işlev gövdesini simge tablosuna yerleştirir. İşlev kullanıldığı zaman
derleyici çağrının doğruluğunu denetler ve geri dönüşün doğrulukla kullanılmasını
sağlar. Daha sonra işlev gövdesini çağrının bulunduğu yere yerleştirir, böylece fazla
işlemler ortadan kaldırılmış olur. Satıriçi kodlar bir bellek alanı kaplar, ama işlev
küçükse normal bir işlevin çağrımı ile üretilen koddan daha az bellek alanı kaplar.
(değişkenleri yığıta göndermek ve oradan CALL yapmak)
Başlık dosyasındaki satıriçi işlevin özel bir durumu bulunur, zira mutlaka işlevi
içeren başlık dosyasını koymalısınız, ve işlevin kullanıldığı her dosyada işlev
tanımını yerleştirmelisiniz. Ama burada aynı işlevi farklı tanımlarla vererek
hatalara neden olunmamalıdır. (satıriçi işlevlerinin bulunduğu her yerde işlev
tanımları mutlaka aynı olmalıdır).
Sınıf satıriçileri
Satıriçi işlevi tanımlamak için işlev tanımının başına inline anahtar kelimesini
koymak gerekir. Ama bu sınıf tanımı içerisinde gerekmez (İstenirse yazılabilir ama
yazmamak akıllıca olur). Sınıf tanımı içerisindeki herhangi bir işlev, otomatik
olarak inline sayılır. Örnek;
//:B09:Inline.cpp
//sınıfların içindeki satıriçi işlevler
#include <iostream>
#include <string>
using namespace std ;
class Point{
int i,j,k ;
public:
Point( ): i(0),j(0),k(0) { }
Point(int ii,int jj, int kk)
: i(ii),j(jj),k(kk) {}
void print(const string& msg=" ") const{
if(msg.size( )!=0)cout<<msg<<endl ;
cout<<"i= "<<i<<endl ;
<<"j= "<<j<<endl ;
<<"k= "<<k<<endl;
}
};
int main( ){
Point p,q(1,2,3) ;
p.print("p nin degeri") ;
q.print("q nun degeri");
}///:~
Burada varsayım olarak iki yapıcı işlev ile birlikte print( ) işlevi satıriçidir.
Farkedildiği üzere main( ) işlevinde kullanılan satıriçi işlevler olması gerektiği gibi
saydamdır. Bir işlevin mantıki davranışı mutlaka, satıriçi olup olmadığına
bakılmaksızın, aynı olmalıdır. (aksi takdirde derleyici çalışmaz). Tek fark başarım
derecesinden gelir.
Sınıf bildirimlerinin her yerinde satıriçi kullanma eğiliminin kaynağı, dışarıda üye
işlev tanımının gerekmemesidir.
Satıriçilerin ardında yatan fikir, derleyiciler tarafından yapılan iyileştirmeler için iyi
fırsatlar oluşturmaktır. Fakat büyük bir işlevi satıriçi yapmak, işlevin çağrıldığı
heryerde kodların yinelenmesine ve kodun şişmesine, daha sonra da program
hızının düşmesine neden olur. (tek güvenilir usul derleyicinizde satıriçilerinizin
davranışlarını araştırmaktır.)
Erişim işlevleri
Satıriçilerin sınıf içinde en önemli kullanımlarından biri erişim işlevidir. Erişim
işlevi küçük bir işlev olup nesnenin (iç değişken veya değişkenlerin) durumunu
okumaya veya değiştirmeye yarar. Erişim işlevinde satıriçilerin önemi çok
büyüktür, bunu aşağıdaki örnekte göreceksiniz;
//:B09:Acces.cpp
//satıriçi erişim işlevi
class Access{
int i ;
public:
int read( ) const{return i;}
void set(int ii){i=ii;}
};
int main( ){
Access A ;
A.set(100) ;
int x=A.read( ) ;
}///:~
Burada kullanıcı sınıf içindeki durum değişkenleri ile doğrudan hiç bir zaman ilişki
kuramaz. Bu değişkenler sınıf tasarımcısı tarafından private olarak belirlenmiştir.
Bütün private veri üyelerine erişim, üye işlev arayüzü aracılığı ile sağlanır. Ek
olarak şunu belirtelim ; erişim verimlilik açısından oldukça yararlıdır. Örnek olarak
read( ) işlevini ele alalım. read( ) işlevinin çağrımında satıriçi olmaksızın üretilen
kodda this'in yığıta gönderilir ve assembly dili ile CALL yapılır. Birçok işlemcide
bu kodun boyutu, satıriçi tarafından üretilenden daha büyüktür. Bundan dolayı
çalışma süresi de daha uzundur.
Verimliliğe duyarlı sınıf tasarımcısı, satıriçi işlevleri kullanmaksızın i değişkenini
public üye yapmaya zorlanır, böylece i ye kullanıcının doğrudan erişimine izin
verilerek ek uğraşılardan kurtulunur. Tasarım açısından bu bir felakettir. Zira i
değişkeni o zaman public arayüzün bir parçası olur, bundan dolayı tasarımcı i de
hiçbir zaman değişiklik yapamaz. i adı verilen int tipini biriktirirsiniz. Bu bir
sorundur, ilerleyen bölümlerde göreceğiniz üzere durum bilgisini int yerine float
göstermek çok daha yararlıdır. int i, public arayüzün bir parçası olduğu için
değiştirmek olası değildir. Veya i nin okunması ve değerinin belirlenmesi sürecinde
bazı ek hesaplama işlemleri istenebilir, eğer i public ise bunu gerçekleyemezsiniz.
Öte yandan üye işlevleri her zaman bir nesnenin durum bilgisini okumak ve
değiştirmek için kullanabilirsiniz, içinizden nesnenin vurgulanan gösterimini de
değiştirebilirsiniz.
Bunlara ek olarak, veri üyelerini denetlemek için üye işlevlerin kullanımı,
değiştirilen veriyi algılamak amacı ile üye işleve kod eklenmesine izin verir. Bu
uygulama hata ayıklama sırasında çok yararlıdır. Bir veri üyesi public ise, herhangi
biri onu herhangi bir anda sizin haberiniz olmadan değiştirebilir. Bunu unutmayın!.
Erişiciler ve değiştiriciler
Konu ile ilgili kişiler erişim işlevlerini daha ayrıntılı incelemek için iki kısma
ayırırlar; erişiciler (nesneden durum bilgisini okumak için) ve değiştiriciler
(nesnenin durumunu değiştirmek için). Ek olarak, işlev bindirimi hem erişici hem de
değiştirici için aynı ad kullanarak sağlanır. Durum bilgisinin okunmak içinmi yoksa
değiştirilmek içinmi olduğunu, işlevin çağrılma biçimi belirler. Böylece;
//:B09:Rectangle.cpp
//erişiciler ve değiştiriciler (accessors &mutators)
class Rectangle{
int wide,high ;
public:
Rectangle(int w=0,int h=0)
:width(w),height(h){}
int getWidth( ) const {return width;}
void setWidth(int w){width=w;}
int getHeight( ) const{return height;}
void setHeight(int h){height=h;}
};
int main( ){
Rectangle r(19,48) ;
//genişlik ve yüksekliği değiştir
r.setHeight(2*r.getWidth( )) ;
r.setWidth(2*r.getHeight( )) ;
}///:~
Doğal olarak erişiciler ve değiştiriciler iç değişkenlere bağlantı unsuru olmak
zorunda değildirler. Bazı durumlarda çok daha ayrıntılı hesaplamalarda kullanılırlar.
Aşağıdaki örnek standart C kütüphanesinde ki time işlevlerini kullanarak bir Time
sınıfı oluşturur;
//:B09:Cpptime.h
//basit bir zaman sınıfı
#ifndef CPPTIME_H
#define CPPTIME_H
#include <ctime>
#include <cstring>
class Time{
std::time_t t ;
std::tm local ;
char asciiRep[26] ;
unsigned char lflag, aflag ;
void updateLocal( ) {
if(!lflag){
updateLocal( );
std::strcpy(asciiRep,std::asctime(&local)) ;
aflag++;
}
}
public:
Time( ){mark( );}
void mark( ){
lflag=aflag=0 ;
std::time(&t) ;
}
const char* ascii( ){
updateAscii( ) ;
return asciiRep( ) ;
}
//saniyelerdeki fark
int delta(Time* dt) const{
return int(std::difftime(t,dt->t)) ;
}
int daylightSavings( ){
updateLocal( ) ;
return local.tm_isdst ;
}
int dayOfYear( ){ //ocak 1 den itibaren
updateLocal( ) ;
return local.tm_yday ;
}
int dayOfWeek( ){//pazardan itibaren
upodateLocal( ) ;
return local.tm_wday ;
}
int since1900( ){//1900 yılından itibaren
updateLocal( ) ;
return local.tm_year ;
}
int month( ){//ocaktan itibaren
updateLocal( ) ;
return local.tm_mon ;
}
int dayOfMonth( ){
updateLocal( ) ;
return local.tm_mday ;
}
int hour( ){//öğleden itibaren, 24-saatlik aralık
updateLocal( ) ;
return local.tm_hour ;
}
int minute( ){
updateLocal( ) ;
return local.tm_min ;
}
int second( ){
updateLocal( ) ;
return local.tm_sec ;
}
};
#endif //CPPTIME_H///:~
Standart C kütüphanesinde bulunan işlevlerin zaman için, çok değişik gösterimleri
vardır. Bunların hepsi Time sınıfının içinde bulunur. Hepsini de güncellemek
gerekmez, bunun yerine time_t ana gösterim olarak alınabilir, ve tm local ile ascii
gösterimi asciiRep, sahip oldukları bayraklarla, o anki time_t ye güncellemeyi
gösterirler. updateLocal( ) ve updateAscii( ) private işlevleri, bayrakları denetler
ve güncellemeyi koşullara göre yaptırır.
Yapıcı mark( ) işlevini çağırır (kullanıcı ayrıca o anki zamanı göstermesini
sağlamak için çağırabilir) , ve bu iki bayrağı da yöresel zamanı göstermek için
sıfırlar, ve artık şimdi ASCII gösterim geçersizdir. ascii( ) işlevi updateAscii( )
işlevini çağırarak standart C kütühane işlevi asctime( ) sonucunu yörel tampona
kopyalar, zira asctime( ) belirli bir bellek alanını kullandığı için herhangi bir yerde
çağrıldığı zaman o yerin üzerine yazılır. ascii( ) işlevinin geri dönüş değeri yörel
tamponun adresidir. daylightSavings( ) işleviyle başlayan bütün işlevler
updateLocal( ) işlevini kullanır.
Tabii bu durum birleşik satıriçilerin boyutunu oldukça büyütür. Bu durum özellikle
işlevler çok fazla sayıda çağrılmadığın da, uğraşmaya değmeyecek kıymettedir.
Fakat bu, tabii ki bütün işlevler satıriçi olmamalıdır demek değildir. Bütün işlevleri
satıriçi olmaktan çıkarsanız, bile hiç olmazsa en azından updateLocal( ) işlevini
satıriçi olarak bırakmalısınız, böylece onun kodları satıriçi olmayan işlevlerde
yinelenir, ve ek işlev çağrım maliyeti ortadan kalkmış olur. Şimdi küçük bir sınama
programı örneği verelim;
//:B09:Cpptime.cpp
//basit bir zaman sınıfının sınanması
#include "Cpptime.h"
#include <iostream>
using namespace std ;
int main( ){
Time start ;
for(int i=1;i<1000;i++){
cout<<i<<' ' ;
if(i%10==0) cout<<endl ;
}
Time end ;
cout<<endl ;
cout<<"start= "<<start.ascii( ) ;
cout<<"end= "<<end.ascii( ) ;
cout<<"delta= "<<end.delta(&start) ;
}///:~
Bir Time nesnesi oluşturulur, daha sonra biraz vakit alan işlemler olur, daha sonra
süreyi sonlandıran ikinci Time nesnesi oluşturulur, Bunlar, zamanı başlatma,
sonlandırma ve geçen süreyi hesaplamada kullanılır.
Satıriçileri kullanarak Stash ve Stack kullanımı
Stash ve Stack sınıflarında satıriçilerini kullanarak çok daha verimli hale
getirebiliriz.
//:B09:Stash4.h
//satıriçi işlevler
#ifndef STASH_H
#define STASH_H
#include "../require.h"
class Stash{
int size ;
//her bellek alanının büyüklüğü
int quantity ; //bellek alanı sayısı
int next ;
//bir sonraki boş bellek
//dinamik olarak yerleştirilen bayt dizisi
unsigned char* storage ;
void inflate(int increase) ;
public:
Stash(int sz):size(sz),quantity(0),
next(0),storage(0){ }
Stash(int sz,int initQuantity):size(sz),
quantity(0),next(0),storage(0){
inflate(initQuantity);
}
Stash::~Stash( ){
if(storage!=0)
delete []storage ;
}
int add(void* element);
void* fetch(int index) const{
require(0<=index, "Stash::fetch (-)index");
if(index>=next)
return 0 ;
//sona gelindiğini gösteriyor
//istenen ögenin göstergeçini elde etmek için
return &(storage[index*size]) ;
}
int count( ) const{return next;}
};
#endif //STASH4_H//
Küçük boyutlu işlevler satıriçilerle daha iyi çalışır, fakat yukarıdaki iki büyük
boyutlu işlev hala satıriçi değildir. Eğer bunlarda satıriçi işlev haline getirilecek
olursa, herhalde fazla bir kazançlı olmaz.
//:B09:Stash4.cpp {O}
#include "Stash4.h"
#include <iostream>
#include <cassert>
using namespace std ;
const int increment=100 ;
int Stash::add(void* element){
if(next>=quantity) //yeterli bellek alanı varmı?
inflate(increment) ;
//ögeyi belleğe kopyala
//bir sonraki boş alandan başlayarak
int startBytes=next*size ;
unsigned char* e=(unsigned char*)element ;
for(int i=0;i<size;i++)
storage[startBytes+i]=e[i] ;
next++ ;
return(next-1) ; //dizin numarası
}
void Stash::inflate(int increase){
assert(increase>=0) ;
if(increase==0)return ;
int newQuantity=quantity+increase ;
int newBytes=newQuantity*size ;
int oldBytes=quantity*size ;
unsigned char* b=new unsigned char[newBytes] ;
for(int i=0;i<oldBytes;i++)
b[i]=storage[i] ; //eskiyi yeniye kopyala
delete [] (storage) ; //eskiyi boşalt
storage=b ; //yeni bellek yerini işaretle
quantity=newQuantity ; //boyutu ayarla
}///:~
Bir kez daha sınama programı herşeyin doğru çalıştığını gösterecektir.
//B09:Stash4Test.cpp
//{L} Stash4
#include "Stash4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std ;
int main( ){
Stash intStash(sizeof(int)) ;
for(int i=0;i<100;i++)
intStash.add(&i) ;
for(int j=0;j<intStash.count( );j++)
cout<<"intStash.fetch("<<j<<")= "
<<*(int*)intStash.fetch(j)
<<endl ;
const int bufsize=80 ;
Stash stringStash(sizeof(char)*bufsize,100) ;
ifstream in("Stash4Test.cpp") ;
assure(in,"Stash4Test.cpp") ;
string line ;
while (getline(in,line))
stringStash.add((char*)line.c_str( ));
int k=0 ;
char* cp ;
while((cp=(char*)stringStash.fetch(k++))!=0)
cout<<"stringStash.fetch("<<k<<")= "
<<cp<<endl ;
}///:~
Bu sınama programı daha önce kullanılan sınama programı ile aynıdır, ve bundan
dolayı çıktısı temel olarak benzerdir.
Stack sınıfı satıriçi kullanımının daha uygun olduğu bir uygulamadır.
//:B09:Stack4.h
//satıriçilerle birlikte
#ifndef STACK4_H
#define STACK4_H
#include "../require.h"
class Stack{
struck Link{
void* data ;
Link* next ;
Link(void* dat,Link* nxt):
data(dat),next(nxt){}
}* head ;
public:
Stack( ) : head(0){}
~Stack( ){
require(head==0,"Stack not empty") ;
}
void push(void* dat){
head=new Link(dat,head) ;
}
void* peek( ) const{
return head ? head->data:0 ;
}
void* pop( ){
if(head==0)return 0;
void* result=head->data ;
Link* oldHead=head;
head=head->next ;
delete oldHead ;
return result ;
}
};
#endif //STACK4_H///:~
Gördüğünüz gibi Stack'in önceki sürümünde boş olarak bulunan Link yıkıcı işlevi
burada bulunmamaktadır .pop( ) işlevi ise Link tarafından kullanılan bellek
boşaltımını yapmaktadır.(Link tarafından gösterilen data (veri) nesnesi ortadan
kaldırılmamaktadır)
Satıriçi işlevlerin büyük çoğunluğu belli ve açık biçimde farkedilir, özellikle Link .
pop( ) işlevi geçerli görünse de
koşullu durumlar ve yörel değişkenler bulunduğu durumlarda satıriçi işlevler o
kadar yararlı değildir. Burada ki örnekte, satıriçi işlevler büyük boyutlu olmadıkları
için çoğunlukla herhangi bir zararları olmaz. Bütün işlevler satıriçi hale getirilirse,
kütüphane kullanımı oldukça basitleşir, zira ilişimler gerekmez. Aşağıdaki sınama
örneğinde bunları göreceksiniz. (Stack4.cpp olmadığına dikkat edin!)
//:B09:Stack4Test.cpp
//{T} Stack4Test.cpp
#include "Stack4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namspace std ;
int main(int argc,char* argv[]){
requireArgs(argc,1); //dosya adı değişken olarak verildi
ifstream in(argv[1]) ;
assure(in,argv[1]) ;
Stack textlines ;
string line ;
//dosyayı oku ve yığıta yükle
while(getline(in,line))
textlines.push(new string(line)) ;
//satırları yığıttan al ve yazdır
string* s ;
while((s=(string*)textlines.pop( ))!=0){
cout<<*s<<endl ;
delete s ;
}
}///:~
Bazıları bütün işlevleri satıriçi olan sınıflar yazarlar, o zaman sınıfın tamamı başlık
dosyasında bulunur. Program geliştirme süreci boyunca derleme süresini
uzatmasına rağmen, bunun büyük ihtimalle zararı olmaz. Program bir küçük parçayı
kararlı hale getirdiğinde herhalde geriye dönüp uygun yerlerde işlevleri
satıriçilikten çıkarmak istersiniz.
Satıriçiler ve derleyiciler
Satıriçilerin etkisini anlamanın yolu, derleyici satıriçi ile karşılaştığında ne yapıyor
bunu bilmekten geçer. Bilindiği gibi herhangi bir işlevle karşılaşan derleyici, işlevin
tipini simge tablosunda saklar. (burada tipten kastedilen isim, değişken adları ve
işlev geri dönüş değeridir). Ek olarak, derleyici satıriçi işlev tipini gördüğü zaman,
-ve işlev gövdesi kısımlara hatasız bir şekilde bölmelere ayrılır (parsing)- işlev
gövdesinin kodları simge tablosuna yerleştirilir. Kodlar kaynak biçiminde mi
saklanacak, assembly komutları olarak mı derlenecek, ya da derleyiciye başka bir
görüntüde mi aktarılacak bunlar ayarlanır.
Bir satıriçi işleve çağrı yapıldığında, derleyici önce çağrının doğruluğunu onaylar.
Yani bütün değişken tipleri mutlaka ya işlevin değişken listesindekilerle tıpatıp aynı
olmalıdır, ya da derleyici mutlaka uygun tip çevirisini yapabilmelidir, ve geri dönüş
tipi mutlaka varış açıklamasında doğru tip olmak zorundadır.( ya da doğru tipe
çevrilebilmelidir). Bu doğal olarak derleycinin bir işlev için tıpıtıpına yapması
gerekenlerdir, ve bu işlemler önişlemleyicilerin işlevler için yaptıklarından tamamen
farklıdır, zira önişlemleyiciler tip denetimi ve tip çevrimi işlemleri yapmazlar.
Bütün işlev tip bilgileri çağrı ile ilintili olanlara uygunsa, satıriçi işlev kodu
doğrudan çağrıya yerleştirilir. Ek çağrı uğraşı ortadan kaldırılır, ve böylece derleyici
tarafından daha ileri iyileştirme yapılmasına izin verilir. Ayrıca satıriçi eğer üye
işlev ise, nesne adresi (this) uygun yere (ya da yerlere) konur, bu işlemde keza
önişlemleyicilerin yerine getiremeyecekleri işlemlerdendir.
Sınırlamalar
Derleyicilerin satıriçilerle ilgili işlem yapamayacakları iki durum bulunur.Bu
durumlarda satıriçi tanımını alarak işlevi
normal işlev haline döndürür ve sanki satıriçi değilmiş gibi işlev için bellekte yer
oluşturur. Bu işlem çoklu çevrimle yerine getirilmek zorunda kalınırsa (çok fazla
tanım hatasına yolaçar), ilişimciye, farklı çok sayıda ki tanımı gözardı etmesi
iletilir.İşlev çok fazla karmaşık olduğunda ise, derleyici satıriçi işlemlerini yerine
getiremez. Bu aslında, derleyiciye özellikle bağlı bir durumdur, fakat derleyicilerin
çoğunluğu işlemi terkederler, ve o zaman satıriçi size herhangi bir yarar sağlamaz.
Genellikle döngü çeşitleri satıriçi açılımları olarak çok karmaşık yapılar olarak
düşünülür.
Döngüler işlev içinde işlev çağrılarına göre işlemlenirken çok daha fazla vakit
harcarlar. İşlev sadece basit deyimlerden ibaretse, satıriçi yapılmasında derleyici
tarafında herhangi bir sorun ortaya çıkma olasılığı çok azdır. Fakat çok sayıda
deyim olduğunda, işlev çağrımının masrafı işlev gövdesinin işlemlenmesinden çok
daha azdır. Şimdi hatırlayalım; büyük bir satıriçi işlevin her çağrılışında, bütün işlev
gövdesi çağrı yerine aktarılır, böylece herhangi başarım ilerlemesi olamadan kod
şişmelerine yol açabilirsiniz. (bu kitapta ekranın tamamında görebilmeniz için, bazı
satıriçilerin kabul edilebilirden daha büyük olduklarını farkedeceksiniz, bunların
anlaşılmak amacı için olduğunu düşünmelisiniz)
İşlevin adresi içeriden ya da dışarıdan alındığı durumlarda, derleyici satıriçi
işlemlerini gerçekleyemez. Derleyici mutlaka adres üretmek zorunda olursa, o
zaman derleyici işlev kodları için saklama yeri belirler ve sonuç adresini kullanır.
Bir şekilde adres gerekmezse, derleyici büyük olasılıkla kodları satıriçi olarak tutar.
Şimdiye kadar anlatılanlardan anlaşılması gereken; satıriçiler derleyiciye sadece
birer önerilerdir. Derleyiciler herhangi bir unsurun satıriçi olması için hiçbir zaman
zorlanamazlar. İyi bir derleyici küçük basit işlevleri satıriçi olarak, çok karmaşık
olanları ise otomatik olarak satıriçi yapmayarak satıriçilikten çıkarır. Bu da bize
istenen neticeyi verir, işlev çağrısının gerçek anlamını yani macro verimliliğinin
elde edilmesini sağlar.
İlerdeki tanıklar (forward references)
Derleyicinin satıriçileri işlemlerken ne yaptığını düşündüğünüz de, gerçekte
varolandan çok daha fazla sınırlamayla karşılaşmanız sizi şaşırtabilir. Özellikle bir
satıriçi, sınıf içinde bildirilmemiş bir işleve ileride tanıklık edeceğini söylerse (o
işlev ister satıriçi olsun ya da olmasın), derleyici o işlevi kullanamaz gibi görünür.
//:B09:EvaluationOrder.cpp
//satıriçi hesaplama sırası
class Forward{
int i;
public:
Forward( ):i(0){}
//bildirilmemiş işlev çağrısı
int f( ) const {return g( )+1;}
int g( ) const {return i;}
};
int main( ){
Forward frwd ;
frwd.f( ) ;
}///:~
f( ) işlevinde henüz bildirilmemiş olmasına rağmen g( ) işlevine bir çağrı var. Bu
çalışır, zira programlama dilinin tanımına göre; sınıf içindeki hiçbir satıriçi işlev,
sınıf bildiriminin kapatan zincirli ayracına rastlayana kadar hesaplanmaması
lazımdır.
Tabii eğer g( ) işlevi f( ) işlevini çağırsaydı kendini çağırmanın sonsuz döngüsü
nedeni ile sonlandırma yapmak zorunluluğu doğardı. Böyle bir durum satıriçileri
derleyici için çok karmaşık hale getirirdi. (ayrıca gerek f( ) gerekse
g( ) işlevlerinden birine hesaplamaları bitirme zorlaması yapılır, zira aksi taktirde
kendini çağırma sonsuza giderdi.)
Yapıcı ve yıkıcı işlevlerin görünmeyen etkinlikleri
Yapıcı ve yıkıcıların bulunduğu yerler insana, satıriçilerin aslında çok daha fazla
verimli olduğu düşüncesini oluşturabilir. Yapıcılar ve yıkıcılar görünmeyen
etkinliklere sahiptirler, zira sınıflar yapıcı ve yıkıcı işlevlerin çağrılmasını gerekli
kılan alt nesneler bulundurabilirler. Bu alt nesneler ya üye nesnelerdir, ya da kalıtım
nedeni ile oradadırlar. Üye nesnelerin bulunduğu bir örneği aşağıda veriyoruz;
//:B09:Hidden.cpp
//satıriçilerdeki saklı etkinlikler
#include <iostream>
using namespace std;
class Member{
int i,j,k ;
public:
Member(int x=0):i(x),j(x),k(x){}
~Member( ){cout<<"~Member"<<endl ;}
};
class WithMembers{
Member q,r,s ; //yapıcıları var
int i;
public:
WithMembers(int ii):i(ii){} //çok basit
~WithMembers( ){
cout<<"~WithMembers( )"<<endl ;
}
};
int main( ){
WithMembers wm(1) ;
}///:~
Member için yapıcı işlevin satıriçi olması çok basittir, zira ek bir işlem yok, kalıtım
ya da üye nesneler gibi ek görünmeyen etkinlikler oluşturan unsurlar da ortada
bulunmamaktadır. Fakat WithMembers sınıfında gözün göremediği etkinlikler
olur. Üye nesnelerden q,r,s için yapıcı ve yıkıcılar otomatik olarak çağrılır. Bu
yapıcı ile yıkıcılar satıriçidirler. Böylece normal üye işlevlerden farklılıkları anlamlı
kılınır. Bu her zaman yapıcı ve yıkıcı işlevlerinizin tanımları satıriçi yapmamanız
lazım anlamını taşıyacak demek değildir.Aksi durumları anlamlı kılan durumlar
vardır.Ayrıca programınızı hızla yazmaya koyulurken işlevleri satıriçi olarak
kullanmak daha uygundur. Fakat eğer verimlilikle ilgili beklentileriniz varsa
gözönünde bulundurmanız gereken yerlerdesiniz demek.
Dağınıklığı azaltma
Bu tür kitaplarda, sınıflara yerleştirilen satıriçi tanımlarının kısalığı ve basitliği çok
yararlıdır, zira ekrandaki ya da sayfada ki daha çok unsur birbiri ile uyumludur.
Bazılarına göre ise bu durum, sınıf arayüzlerinin gereksiz derecede dağınıklığına
yolaçar, ve bundan dolayı sınıfların kullanımı zorlaşır. Bu kişiler, sınıfların içinde
yerine göre tanımlanmış üye işlevlerin, arayüzü berrak kılmak için bütün
tanımlarını, sınıf dışında yapmayı sürdürürler. İyileştirme ise, bu kişilere göre ayrı
bir konudur. İyileştirme yapmak isterseniz, o zaman inline anahtar kelimesini
kullanın. Bu yaklaşıma göre daha önce geliştirilen Rectangle.cpp aşağıdaki biçime
dönüşür;
//:B09:Noinsitu.cpp
//in situ(in place=yerine göre) işlevleri kaldırma
class Rectangle{
int width,height ;
public:
Rectangle(int w=0,int h=0) ;
int getWidth( ) const ;
void setWidth(int w) ;
int getHeight( ) const ;
void setHeight(int h) ;
};
inline Rectangle::Rectangle(int w,int h)
:width(w),height(h){}
inline int Rectangle::getWidth( ) const{
return width ;
}
inline void Rectangle::setWidth(int w){
width=w ;
}
inline int Rectangle::getHeight( ) const{
return height ;
}
inline void Rectangle::setHeight(int h){
height=h ;
}
int main( ){
Rectangle r(20,40) ;
//en ile boyu yerdeğiştir
int iHeight=r.getHeight( ) ;
r.setHeight(r.getWidth( ) ) ;
r.setWidth(iHeight) ;
}///:~
Şimdi satıriçi işlevlerle aynı işlevlerin satıriçi olmayan uygulamalarını
karşılaştırmak için, buradaki inline anahtar kelimesini kaldırmak yeterlidir. (satıriçi
işlevler normalde başlık dosyalarına yerleştirilir, satıriçi olmayanlar ise kendi
çevrim birimlerinde bulunmak zorundadırlar). İşlevleri belgelendirmek isterseniz,
basit bir şekilde kes-kopyala işlemini uygulamak yeterlidir.İn situ(yerine uygun)
işlevler daha fazla çalışma istedikleri gibi hata yapılmasına daha çok olanak
verirler. Bu yaklaşımdaki başka bir unsurda her zaman işlev tanımlarında belli
tutarlı bir biçimin uygulanabilmesidir. Bazı şeyler in situ işlevlerde her zaman
yapılamayabilir.
Önişlemleyicilerle ilgili diğer özellikler
Daha önce önişlemleyici macro'larının yerine, hemen hemen her zaman inline
işlevler kullanmak isteyeceğinizi belirtmiştik. Bunun C önişlemleyicisindeki(doğal
olarak C++ önişlemleyicisinde de) üç özel konuda istisnası bulunmaktadır ;string
yapma, string'leri ardarda dizme, ve simge yapıştırma. String yapma daha önce de
anlatıldığı gibi, # yönergesi ile yerine getirilir ve bir taanımlayıcı almasına izin
vererek karakter dizisine çevirir. Ardarda string diziminde ise iki yanyana karakter
dizisi aralarında herhangi noktalama işareti olmaksızın biraraya getirilir. Bu her iki
özellik, hata ayıklama kodları yazımında oldukça yaygın kullanılır. İşte,
#define DEBUG(x) cout<<#x" = "<<x<<endl
Bu herhangi bir değişkenin değerini yazdırır. Bundan başka işlemlenen herhangi bir
deyimin izini de yazdırabilirsiniz.
#define TRACE(s) cerr<<#s<<endl ; s
#s çıktı için deyimi string yaparak yazdırır. İkinci s ise, yeniden yineleyerek s
deyiminin çalışmasını sürdürür. Tabii böyle durumlar bazen sorunlara yolaçar,
özellikle tek satırlık for döngülerinde;
for(int i=0;i<100;i++)
TRACE(f(i)) ;
TRACE( ) macro'sunda gerçekte iki tane deyim bulunmaktadır, yukarıdaki tek
satırlık for döngüsünde sadece ilki işlemlenir. Çözüm macro'da noktalı virgülü,
virgülle değiştirerek bulunur.
Simge yapıştırma
Simge yapıştırma işlemi ## yönergesi ile yerine getirilir, kod üretiminde çok
yararlıdır. İki tane tanımlayıcıyı alıp birarada yapıştırıp, otomatik olarak yeni bir
tanımlayıcı oluşturur. Örnek;
#define FIELD(a) char* a##_string; int a##_size
class Record{
FIELD(one);
FIELD(two);
FIELD(three);
//....
};
FIELD( ) macro'sunun her çağrılışında bir karakter dizisini tutan, bir tanımlayıcı
oluşur, ikincisi ise dizinin uzunluğunu tutar.Bu uygulama sadece okunma kolaylığı
sağlamaz, aynı zamanda kodlama hatalarını ortadan kaldırır ve bakımı da
kolaylaştırır.
Hata denetimini geliştirme
Dikkat ettiyseniz, şimdiye kadar require.h işlevleri tanımlanmadan kullanıldı.
(uygun yerlerde programcının hatalarını saptamak için assert( ) işlevi
kullanılmasına rağmen) . Şimdi artık bu başlık dosyasını tanımlamanın zamanı
geldi. Burası inline satıriçi işlevleri yerleştirmenin tam yeri, zira inline işlevler
herşeyin başlık dosyasına yerleştirilmesine izin verirler, ve böylece paket kullanım
sürecini basitleştirirler. Sadece başlık dosyasını eklersiniz, ve uygulama dosyasının
ilişimi ile ilgili herhangi sorununuz kalmaz.
Bilmeniz gereken bir şeyde, çok sayıdaki hatayı etkin bir şekilde düzelten istisnaları
programı durdurmadan yönetmektir.İstisnalar konusu ikinci kitapta çok daha
ayrıntılı olarak incelenecektir.- özellikle düzeltmek istediğiniz hatalarda- .
require.h işlevinin yönettiği koşullardan biri; programın sürmesini engellemektir,
bunlar dosyanın açılamadığı ya da kullanıcının yeterli komut satırı değişkeni
sağlamadığı durumlardır. Böylece standart C kütüphanesinden exit( ) işlevinin
çağrımı kabul edilir. Aşağıdaki örnek başlık dosyası kitabın kök dizinine
yerleştirilmiş olup diğer tüm dizinlerden erişilebilir.
//:B09:require.h
//programdaki hata koşullarının sınanması
//eski derleyiciler için yörel olarak "using namespace std"
#ifndef REQUIRE_H
#define REQUIRE_H
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
inline void require(bool requirement,
const std::string& msg="istem iptal"){
using namespace std ;
if(!requirement){
fputs(msg.c_str( ),stderr) ;
fputs("\n",stderr) ;
exit(1) ;
}
}
inline void requireArgs(int argc,int args,
const std::string& msg=
"mutlak kullanilan %d arguments"){
using namespace std;
if(argc!=args+1){
fprintf(stderr,msg.c_str( ),args);
fputs("\n",stderr);
exit(1) ;
}
}
inline void requireMinArgs(int argc,int minArgs,
const std::string& msg=
"mutlak kullanilan enaz %d arguments"){
using namespace std ;
if(argc<minArgs+1){
fprintf(stderr,msg.c_str( ),minArgs);
fputs("\n",stderr) ;
exit(1) ;
}
}
inline void assure(std::ifstream& in,
const std::string& filename=" "){
using namespace std ;
if(!in){
fprintf(stderr,"Dosya acilmadi %s\n",
filename.c_str( ));
exit(1) ;
}
}
inline void assure(std::ofstream& out,
const std::string& filename=" "){
using namespace std ;
if(!out){
fprintf(stderr,"Dosya acilmadi %s\n",
filename.c_str( )) ;
exit(1) ;
}
}
#endif //REQUIRE_H///:~
Gerekirse yazılan iletileri size uygun hale getirebilirsiniz.
Gördüğünüz gibi char* değişkenlerinin yerine, const string& değişkenleri
kullanılmıştır. Bu hem char* hemde string'lerin işlevlere değişken olarak
kullanılmasını sağlar, çok daha genel amaçlı ve yararlı bir kullanım biçimidir.
(kodlamalarınız da bu yolu takip edebilirsiniz)
requireArgs( ) ve requireMinArgs( ) işlevlerinin tanımında komut satırından
değişkenlerin sayısına bir eklenir, zira argc her zaman program adını sıfır değişken
ile işlemleyerek kullanır, böylece komut satırında aslında olandan bir fazla değere
sahip olur.
Her işlevde "using namespace std" bildiriminin yörel olduğunu belirtelim. Bu
kitabın yazıldığı dönemlerde bazı derleyiciler namespace std'leri, standart C
kütüphanelerinde, doğru olmayan bir şekilde ihtiva etmemektedirler. Bu da derleme
sırasında hatalara yolaçabilmektedir. Yörel bildirim, require.h'i hem doğru hem de
yanlış kütüphanelerle namespace std açılması gerekmeksizin çalışmasına izin verir,
böylece bu başlık dosyasının kullanımında sorun çıkmaz.
Şimdi require.h sınaması yapan basit bir program örneği vereceğiz;
//:B09:ErrTest.cpp
//{T} ErTest.cpp
// require.h sınaması
#include "require.h"
#include <fstream>
using namespace std ;
int main(int argc, char* argv[]){
int i=1 ;
require(i,"deger mutlaka sifirdan farkli olmali") ;
requireArgs(argc,1) ;
requireMinArgs(argc,1) ;
ifstream in(argv[1]) ;
assure(in, argv[1]) ; //dosya adini kullan
ifstream nofile("nofile.xxx") ;
//Fails:
//! assure(nofile) ; //varsayılan değişken
ofstream out("tmp.txt") ;
assure(out) ;
}///:~
Dosyaları açmak için bir adım daha ileri gider, require.h bir macro ilave ederiz.
#define IFOPEN(VAR,NAME) \
ifstream VAR(NAME); \
assure(VAR,NAME);
daha aşağıdaki gibi kullanılır
IFOPEN(in,argv[1])
İlk bakışta çok çekici gelen bu durum için yazılacak çok az şey var. Korkunç
derecede güvensiz değildir, ama yine de en iyisi sakınmak gerekir. Burada yine
belirtelim; bir makro, işlev gibi görünür, ama davranışları farklıdır. Aslında,
oluşturulan nesne (in) makro boyunca varlığını sürdürür. Bu iyi anlaşılmalıdır, zira
programlamaya yeni başlayanlarla kod bakımcılarını şaşırtan en önemli konulardan
birisidir. Yeteri kadar karmaşık olan C++ programlama diline, yeni kafa karıştıran
unsurlar eklenmemelidir. Kendinizi önişlemleyici makro kullanımı ile ilgili
tartışmaların dışında tutmaya çalışın.
Özet
İlerde değiştirebilmek amacı ile bazı temel sınıf unsurlarını gizleyebilmek hassas bir
konudur. Bu unsurların değiştirilme nedeni; bir süre sonra sorun daha iyi
anlaşıldığında verimliliği arttırmak veya, yeni bir sınıf oluşumu çözüme daha fazla
katkı sağladığı içindir. Uygulama özelliğini tehlikeye atan herhangi bir şey, dilin
esnekliğini azaltır. Bundan dolayı satıriçi işlevler çok önemlidir, zira önişlemleyici
makrolarından kaynaklanan sorunları ortadan kaldırır. Satıriçi sayesinde üye işlevler
önişlemleyici makroları kadar verimlidirler. Satıriçi işlevler sınıf tanımlarında
birçok defa kullanılabilirler, bu çok doğaldır zira kullanımları daha kolaydır. Sınıf
tanımlarında inline deyiminin kullanılması gerekmez. Ama ilerde programda boyut
küçültme işlemlerine gidildiği zaman, etkileri değişmeden satıriçi olmayanlarla
değiştirilebilirler. Unutmayın, program geliştirirken uyulacak en önemli kurallardan
biri; "önce çalışan program, daha sonra program iyileştirmesi".
10. BÖLÜM
İsim Denetimi
Programlarda isim oluşturma, temel etkinliklerden birisidir. Büyük bir
proje de kullanılan isimler fazla sayıda olabilir. Bu yüzden programcı aynı
isme değişik yerlerde değişik anlamlar vererek (bindirim ve çokbiçimlilik
kurallarına uymaksızın) dikkatsizce kullanabilir. Böyle bir durum,
derleyicinin o isim hakkında doğru işlem yapmasını engeller. C++ dili,
isimlerin denetimi ve görünürlükleri için yararlı araçlar sağlamaktadır. Bu
araçlarla isimler için bellekte yer ayrılır, ve bunlarla isimler arasında
bağlantılar kurulur.
Bilindiği üzere C dilinde, static anahtar kelimesine bindirim yapılmıştı.
(daha önce bindirimin ne olduğu anlatıldı ) C++ dilinde ise, bindirime
ikinci anlam eklendi. static anahtar kelimesinin bütün kullanımlarda ki
temel anlamı "duran herhangi şey" dir (statik elektrik benzeri). Veya
bellekteki fiziksel yer ya da dosyadaki görünüş anlamına gelir.
Bu bölümde static anahtar kelimesinin bellekte saklama ve görünürlüğü
denetleyiş biçimi, ve C++ da namespace özelliği ile, isimlere erişim
denetiminin gelişmiş halini anlatacağız. Bundan başka C dilinde yazılmış
ve derlenmiş işlevlerin kullanımını da öğreneceksiniz.
C dilinden gelen static ögeler
C ve C++ dillerinin her ikisinde de, static anahtar kelimesinin temel iki
anlamı vardır, ve maalesef her ikisi de çoğunlukla birbirinin arazisine
girer;
1- Sabit bir adrese bir kez yerleştirilir; yani, işlevin her çağrılışında yığıttan
ziyade, özel bir statik veri alanında nesne oluşturulur. Bu, statik saklama
alanıdır.
2- Özel çeviri birimine yöreldir. (C++ da sınıf görüntüsüne yöreldir).
Burada static, isim görünürlüğünü denetler, böylece isim, sınıf veya
çevrim biriminin dışında kullanılamaz. Bu da ilişim konusunu açıklar, yani
ilişimcinin göreceği isimleri belirler.
Bu bölümde static anahtar kelimesinin, C den geçen yukarıdaki anlamları,
ayrıntıları ile açıklanacaktır. Özetlersek C++ da static çok amaçlı bir
anahtar kelimedir. İşlev içinde, sınıf içinde ve global değişken olarak
dosyada kullanılabilir. İşlevde kullanıldığı zaman ilk değer atanmasına bir
kez izin verir, daha sonraki işlev çağrımlarında bir daha ilk değer
atamasına izin vermez. Sınıf içinde kullanıldığında sınıfın değişen
durumlarına göre değişen değişkenlerin (eğer değişkenin başında static
anahtar kelimesi varsa) değişmemesini sağlar. static değişkenleri, o
sınıftan oluşturulacak nesnelerin ana unsurları olarak görmek en akıllıca
yaklaşım olur. Son olarak bir dosyada global değişken static olarak
bildirilirse, o değişkene projenin öbür dosyalarından erişmek mümkün
olmaz. Sadece static global değişkenin bulunduğu dosyanın kodları, bu
değişkene erişebilir.
İşlevlerdeki statik değişkenler
Bir işlev içinde yörel değişken oluşturulduğunda, her işlev çağrılışında
yığıt göstergeçi belli bir miktar azalarak, derleyici gösterilen adrese denk
gelen bellek yerini, ilgili değişken için ayırır. Eğer o değişken için bir ilk
değer atayıcısı varsa, sırası gelince ilk değer atama işlemi yerine getirilir.
Bazen işlev çağrıları arasında bir değeri saklamak istersiniz. Bunu global
değişken yaparak başarabilirsiniz,
ama o zaman o değişken sadece o işlevin denetimi altında kalır. C ve C++
dilleri işlev içerisinde statik nesne oluşumuna izin verirler; nesnelerin
barınma yeri bu durumda yığıt değildir, bunun yerine bellekteki programın
statik veri alanı kullanılır. Bu nesneye sadece bir kez, o da işlevin ilk
çağrılışında ilk değer ataması yapılır, ve daha sonra işlev çağrıları arasında
ilk değer saklanmaya devam eder. İşlevin sonraki çağrımlarında yeniden
ilk değer ataması yapılmaz. Aşağıdaki örnekte işlev, her çağrılışında dizide
bulunan bir sonraki karakteri geri döndürür.
//:C10:StaticVariablesInFunctions.cpp
#include "../require.h"
#include <iostream>
using namespace std ;
char oneChar(const char* charArray=0){
static const char* s ;
if(charArray){
s=charArray ;
return *s ;
}
else
require(s,"un-initialized s");
if(*s=="\0")
return 0;
return *s++;
}
char* a="abcdefghijklmnprqklmnopqrst";
int main( ){
//oneChar( ) ;//require( ) iptal olur
oneChar(a); //s ye a ile ilk değer ataması yapılır
char c;
while((c=oneChar( )!=0)
cout<<c<<endl ;
}///:~
static const char* s, oneChar( ) işlevinin çağrımları arasında değerini
tutar, zira bunun nedeni bu saklama yeri işlev yığıtının bir parçası değildir.
Bununla birlikte programın statik saklama alanında yer alır. oneChar( )
işlevini char* değişkeni ile çağırdığınız zaman, s atanır ve dizinin ilk
karakteri geri döner. oneChar( ) işlevinin değişkensiz her ara çağrılışında
charArray dizisi için, varsayılan sıfır değeri üretilir, bu da işleve s nin
önceden konan ilk değerlerinden karakterlerin elde edildiğini gösterir.
İşlev karakter üretimine, karakter dizisinin sıfır sonlandırıcı ögesine kadar
devam eder. Bu noktada göstergeç değerinin artışı durur, böylece dizi
sonunda çalışma tekrarı engellenmiş olur.
oneChar( ) işlevi, değişken olmadan ve s önceden ilk değer ataması
yapılmadan çağrılırsa ne olur?. s tanımında bir ilk değer atayıcı
belirtilmişti.
static char* s=0 ;
Yerleşik bir tipin static değişkenine ilk değer ataması yapmazsanız,
derleyici programın başlangıcında o değişkenin ilk değerini sıfır (tipe
uygun) yapmayı emniyete alır. oneChar( ) işlevinde, işlevin ilk
çağrımında s sıfırdır. Bu durumda
if(!s) koşulu yeterlidir.
Yukarıda s için ilk değer atama işlemi oldukça basitti. Fakat statik
nesnelerin (diğer bütün nesnelerde olduğu gibi) ilk değer atamaları, sabit
ve önceden bildirilmiş değişkenler ile işlevlerden oluşan gelişigüzel
açıklamalar da olabilir.
Belirtmemiz gereken bir şeyde, yukarıdaki gibi işlevler, koşut denetim akış
(multithreading) sorunlarına oldukça hassastırlar. O nedenle statik
değişkenler içeren işlevleri tasarlarken koşut denetim akış unsurları daima
gözönünde bulundurulmalıdır.
static sınıf nesneleri bulunduran işlevler
Kurallar kullanıcı tanımlı tiplerin statik nesneleri için de aynıdır, ve buna
nesneye ilk değer atama gerçeği de dahildir. Sıfır ataması yerleşik tipler
için geçerlidir, kullanıcı tanımlı tipler de ise, ilk değer ataması mutlaka
yapıcı işlev çağrımı ile gerçeklenmelidir. statik nesneyi tanımladığınız
zaman eğer yapıcı işlevin değişkenlerini belirtmezseniz, sınıf mutlaka
varsayılan bir yapıcı işleve sahip olmalıdır. Örnek;
//:B10:StaticObjectsInFunctions.cpp
#include <iostream>
using namespace std ;
class X{
int i;
public:
X(int ii=0) : i(ii){ } //varsayılan yapıcı
~X( ){cout<<"X::~X( )"<<endl ;}//varsayılan yıkıcı
};
void f( ){
static X x1(44) ;
static X x2 ; //varsayılan yapıcı gerekli
}
int main( ){
f( ) ;
}///:~
f( ) işlevinde bulunan X tipi statik nesnelere, ya yapıcı işlev değişken
listesi ya da, varsayılan yapıcı işlev ile ilk değer ataması yapılır. Bu
uygulamanın denetimi ilk kez tanımlama yapılırken olur, ve sadece ilk
sefer için geçerlidir.
statik nesnelerin yıkıcı işlevleri
statik nesneler (yani örnekteki gibi sadece yörel statik nesneler değil, bütün
statik saklama alanında barınan nesneler) için yıkıcı işlev, ya main( )
işlevinden çıkılırken ya da standart C kütüphane işlevi olan exit( ) işlevi
açık olarak çağrılırsa görev yapar. Uygulamaların çoğunda main( ) exit( )
işlevini, kendisi sonlanırken çağırır. Bunun anlamı; yıkıcı işlevde exit( )
işlevi çağırmak, sonsuz kendini çağırma döngüsüne girileceği için tehlikeli
demektir.
Programdan standart C kütüphane işlevi olan abort( ) işlevini kullanarak
çıkarsanız, statik nesnenin yıkıcı işlevi çağrılmaz.
main( ) işlevini terkederken programcı yapılacak işlemleri belirleyebilir.
(exit( ) işlevi standart C kütüphane işlevi atexit( ) kullanılarak çağrılır. Bu
durumda, atexit( ) tarafından kaydedilmiş işlevler, main( ) işlevi
terkedilmeden evvel oluşturulan nesnelerin yıkıcılarından önce
çağrılabilir. veya exit( ) işlevi çağrılır)
Bilinen yıkımlarda olduğu gibi, statik nesnelerin silinmesi işlemi, başlatma
işleminin tersi sıralamada olur. Sadece oluşturulmuş olan nesneler yıkım
ile silinir. Allahtan, C++ dilinin geliştirme araçları, ilk değer atama sırasına
uyarlar ve nesneler ona göre inşa edilirler. Global nesneler her zaman,
main( ) işlevine girmeden önce inşa edilir ve main( ) işlevinden çıkılırken
imha edilirler, fakat yörel statik nesne ihtiva eden bir işlev hiç bir zaman
çağrılmazsa, o nesnenin yapıcı işlevi hiçbir zaman işlem yapmaz, dolayısı
ile de yıkıcı işlevi de . Örnek olarak ;
//:B10:StaticDestructors.cpp
//Statik nesne yıkıcıları
#include <fstream>
using namespace std ;
ofstream out("statdest.out") ; //izleme dosyası
class Obj{
char c;
//tanımlayıcı
public:
Obj(char cc) : c(cc){
out<<"Obj::Obj( ) icin "<<c<<endl ;
}
~Obj( ){
out<<"Obj::~Obj( ) icin "<<c<<endl ;
}
};
Obj a('a') ;
//global (statik saklama). yapıcı ve yıkıcı herzaman çağrılır
void f( ){
static Obj b('b') ;
}
void g( ){
static Obj c('c') ;
}
int main( ){
out<<"main( ) icinde"<<endl ;
f( ) ;
// b için statik yapıcı işlev çağrımı
//g( ) çağrılmadı
out<<"main( ) islevini terkediyoruz"<<endl ;
}///:~
Obj da char c tanımlayıcı olarak bulunur ve böylece yapıcı ve yıkıcı
işlevler çalışılan nesnenin bilgisini yazdırabilir.
Obj a global nesnedir. Global nesne olmasından ötürü her zaman main( )
işlevine girilmeden önce yapıcı işlevi çağrılır. f( ) işlevinde bulunan static
obj b ve g( ) işlevindeki static obj c için yapıcılar sadece işlevler çağrılırsa
gelir. Sadece
f( ) işlevi çağrıldığında, çağrılan yapıcı ve yıkıcı işlevleri gösterelim.
Programın çıktısı;
Obj::Obj( ) icin a
main( ) icinde
Obj::Obj( ) icin b
main( ) islevini terkediyoruz
Obj::~Obj( ) icin b
Obj::~Obj( ) icin a
a için gereken yapıcı işlev, main( ) işlevine girmeden evvel çağrılır, b için
yapıcı işlev sadece f( ) işlevi çağrılırsa gelir. main( ) işlevi sonlanırken
nesne yıkıcılarının çağrım sıralaması, oluşturulurken kullanılan
sıralamanın tersi düzendedir. Bunun anlamı eğer g( ) işlevi çağrılırsa,
çağrılan b ve c yıkıcılarının sıraları tamamen f( ) ve g( ) işlevlerinden
hangisinin ilk çağrıldığına bağlıdır. İz dosyasından gördüğünüz üzere,
ofstream nesnesi out da statik nesnedir, zira o tüm işlevlerin dışında
tanımlanmıştır, ve bu nedenle statik saklama alanında barınır. out'un olası
kullanımından önce, dosya başlangıcında tanımının (extern bildiriminin
tersine) bulunması oldukça önemlidir. Aksi taktirde uygun ilk değer
ataması yapılmamış nesne kullanıyor olacaksınız.
C++ dilinde global statik nesne yapıcı işlevi, main( ) işlevine girmeden
önce çağrılır. Böylece main( ) işlevine girmeden önce kod çalıştırmanın,
aktarılabilir ve basit yoluna sahip olursunuz. Ve ayrıca main( ) işlevinden
çıktıktan sonra da, yıkıcı işlev kodları çalıştırılır. C dilinde ise, bu her
zaman derleyici üreticisinin assembly başlatma kodlarının etrafından
kaynaklanırdı.
İlişim denetimi
Normal olarak dosya görüntüsünde yeralan (yani işlev veya sınıf içinde
yuvalanmamış) herhangi bir isim, programın bütün çeviri birimlerinde
görünür. Buna çoğu kez dış ilişim denir. İlişimci ilişim sırasında adına her
yerde rastladığı için, çevrim biriminin dışında kalanlarla ilişkisine dış
ilişim denilir. Global değişkenler ve bildik işlevler de dış ilişim bulunur.
İsim görünümüne sınırlamalar getirmek istediğiniz zamanlar da olabilir. O
zaman dosya çalışma alanında, o dosyadaki bütün işlevlerin
kullanabileceği bir değişken olmalıdır. Bunun yanında dosya dışındaki hiç
bir işlev bu değişkene erişmemeli ve görmemelidir, ve ek olarak dosya dışı
tanımlayıcılar ile dikkatsiz isim çatışmaları da engellenmelidir.
Dosya çalışma alanında nesne veya işlev adları çevrim birimine yörel
olarak açık bir biçimde static olarak bildirilir (bu kitapta bildirimlerin
bulunduğu yer .cpp dosyalarıdır). Buna iç ilişim adı verilir. Bunun anlamı;
aynı adı öteki çevrim birimlerinde de isim çatışması olmadan
kullanabilirsiniz demektir.
İçi ilişimin üstünlüğü; başlık dosyasına yerleştirilen bir isim, ilişim
sırasında herhangi bir isim çatışmasına neden olmaz. Başlık dosyalarına
yerleştirilen en yaygın isimler const tanımlar, inline işlevlerdir ve bunların
iç ilişim olduğu baştan varsayılır. (Burada hemen belirtelim; const tanımlar
C++ da iç ilişim, C dilinde ise dış ilişim olarak varsayılır). Burada not
edilmesi gereken bir konu da; ilişim, sadece ilişim/yükleme sırasında
adresleri bulunan ögelere dayanır; sınıf bildirimleri ve yörel değişkenlerin
ilişimi olmaz.
Karışıklık
Şimdi vereceğimiz örnek static'in iki anlamının birbiri ile nasıl çatıştığını
gösterecek. Bütün global nesneler içsel olarak statik saklama sınıfına
sahiptirler, dosya kapsama alanı içinde eğer şöyle yazarsanız;
int a=0 ;
int main( ){}
Bu durumda a için oluşan saklama alanı programın statik veri alanındadır,
ve a için ilk değer atama işlemi main( ) işlevine girmeden önce bir kez
olur. Ek olarakta a görünürlüğü bütün çevrim birimlerinde globaldir.
Görünürlük koşullarına göre static (sadece bu çevrim biriminde görünür)
zıttı olan extern de, isim görünürlüğü açık biçimde bütün çevrim
birimlerinde sağlanır. Böylece yukarıda ki tanıma özdeş açıklama;
extern int a=0 ;
yazımıdır. Yok eğer aşağıdaki gibi yazarsanız;
static int a=0 ;
Burada yapılan bütün iş görünürlüğü değiştirmektir. Böylece a iç ilişime
sahip olur. Saklama sınıfı değişmez, görünürlük static (ya da extern) olsa
bile nesne statik veri saklama alanında kalır.
Yörel değişkenler varsa, static görünürlük seçeneğini durdurur, ve yerine
saklama sınıfı seçeneğini koyar.
Yörel değişken olarak görünen birini extern olarak bildirirseniz, saklama
yeri başka bir yerde anlamına gelir. (böylece değişken aslında işleve
global demektir.) Örnek olarak;
//:B10:LocalExtern.cpp
//{L} LocalExtern2
#include <iostream>
int main( ){
extern int i;
std::cout<<i ;
}///:~
//:B10:LocalExtern2.cpp {O}
int i=5 ;
///:~
İşlev adları ile (üye olamayan işlevler için) static ve extern sadece
görünürlüğü değiştirirler. Şöyle yazarsanız;
extern void f( ) ;
bu aşağıdaki sade yazımla aynı anlamı taşır;
void f( ) ;
ve eğer aşağıdaki gibi yazarsanız;
static void f( ) ;
f( ) işlevi sadece bu çevrim biriminde görünür demektir. Buna bazen statik
dosya da denir.
Öteki saklama sınıf belirteçleri
Gördüğünüz üzere en yaygın kullanılan belirteçler static ve extern'dir.
Bunlardan başka daha az kullanılan iki tane daha belirteç bulunmaktadır.
auto belirteci hemen hemen hiçbir zaman kullanılmaz, zira görevi
derleyiciye değişkenin yörel olduğunu bildirmektir. Aslında auto,
otomatik kelimesinin kısaltılmışıdır. Derleyicinin, değişken için otomatik
olarak yer ayırmasını sağlar. Her zaman derleyici bu uygulamayı değişken
tanımlandığında belirlediği için, auto belirteci düşer, yani kullanılmaz.
register değişkeni ise (auto) yörel bir değişkendir. Verilen bu ipucu ile
derleyici yoğun şekilde kullanılan bu özel değişkeni mümkünse yazmaçta
(register) saklar. Aslında yapılan bu işlem, sistem için bir iyileştirmedir.
Değişik derleyiciler bu ipucu için farklı tepkiler verir; gözardı etme
seçenekleri de vardır. Değişken adresi kullanılacak olursa, register
belirteci mutlaka hemen gözardı edilir. register anahtar kelimesini
kullanmaktan sakınmak gereklidir, zira derleyici genellikle sizin
yaptığınızdan daha iyi iyileştirme yapar.
İsimalanları (namespaces)
İsimler sınıfların içinde yuvalanmış olmalarına rağmen, global işlevlerin,
global değişkenlerin ve sınıfların yeri tek bir global isim alanıdır. Bunun
üzerinde static anahtar kelimesi biraz denetim sağlar. Değişken ve
işlevlere iç ilişim yapmanıza izin verilir (yani, onlar static dosya haline
getirilir). Büyük projelerde global isim alanları üzerindeki denetim
eksiklikleri bazı sorunlara neden olur. Sınıflar da bu sorunları çözmek için,
üreticiler çoğu kez uzun karmaşık isimler oluştururlar ve böylece isim
benzerliğinden kaynaklanan çatışma engellenir. Ama o zaman, isimleri
biraraya toplamalısınız (typedef bunu basitleştirmek amacı ile, yaygın
olarak kullanılmaktadır). Bu uygulama dil tarafından pek desteklenmeyen
ve güzel görünmeyen bir çözümdür.
C++ dilinde global isim alanı, daha iyi yönetmek amacı ile namespace
özelliği kullanılarak altbirimlere bölünür. namespace anahtar kelimesi
aynı class, struct, union ve enum'da olduğu gibi, üyelerin adını ayrı bir
bellek alanına koydurur. Anılan anahtar kelimelerin ek özellikleri de
bulunmasına rağmen, namespace anahtar kelimesinin tek amacı sadece
yeni isim alanı oluşturmaktır.
isim alanı oluşturmak
Bir isim alanı oluşumu aynı class oluşumuna benzer.
//:B10:MyLib.cpp
namespace MyLib{
//Bildirimler
}
int main( ){}///:~
Bu bildirimleri de içeren yeni bir isim alanı üretir. class, struct, union ve
enum'dan belli bazı farklılıkları bulunur. Bunlar:
--->İsim alanı tanımı sadece global görüntüde veya başka bir isim alanında
yuvalanmış olarak bulunur.
--->İsim alanı tanımında kapatan zincirli ayraçtan sonra, noktalı virgül
sonlandırıcı olarak kullanılmaz
.
--->İsim alanı tanımı çok sayıda başlık dosyasında devam ettirilebilir, bu
özel yazım biçimi sayesinde sınıf için yeni bir tanım yapılabilir.
//:B10:Header1.h
#ifndef HEADER1_H
#define HEADER1_H
namespace MyLib{
extern int x;
void f( ) ;
//...
}
#endif //HEADER1_H //:~
//:B10:Header2.h
#ifndef HEADER2_H
#define HEADER2_H
#include "Header1.h"
//MyLib'e daha fazla isim eklenecek
namespace MyLib{//yeni bir tanım değil
extern int y ;
void g( ) ;
//...
}
#endif //HEADER2_H//:~
//:B10:Continuation.cpp
#include "Header2.h"
int main( ){}///:~
---Bir isim alanı adı başka ad için takma ad olabilir, böylece kütüphane
üreticisi tarafından yazılmış hantal isimler kullanmak zorunda kalınmaz.
//:B10:SelamininSuperMuperLibrary.cpp
namespace SelamininSuperMuperLibrary{
class widget{/*... ..*/};
class poppit{/*.. ..... */};
//....
}
//Yazacak çok şey var, onun için takma ad kullan !
namespace Selami=SelamininSuperMuperLibrary;
int main( ){}///:~
---Sınıflarla yapılabilen olaylar isim alanları ile gerçeklenemez.
Adsız isimalanları
Her çeviri biriminde belirteci bulunmayan, "namespace" diyerek
yerleştirebileceğiniz bir isimsiz isim alanı bulunur.
//:B10:UnnamedNamespaces.cpp
namespace{
class Arm{/* */};
class Leg{/* */};
class Head{/* */};
class Robot{
Arm arm[4];
Leg leg[16] ;
Head head[3];
//...
}xanthan ;
int i,j,k ;
}
int main( ){ }///:~
Bu bölgede yeralan isimler çeviri birimine herhangi belirtece gerek
duyulmaksızın otomatik olarak yerleştirilir. Her çeviri birimi için isimsiz
isim alanları tektir ve güvenceye alınmışlardır.Yörel isimler adı konmamış
isim alanına yerleştirilirse, onlara static ile iç ilişim verme ihtiyacı kalmaz.
C++ dili adı konmamış isim alanları kullanarak, statik dosya
kullanımlarına karşı seçenek üretir.
Friend'ler
Kapalı bir class içine friend bildirimini bir isimalanı ile sokabilirsiniz:
//:B10:FriendInjection.cpp
namespace Me{
class Us{
//..
friend void you( );
};
}
int main( ){ }///:~
you( ) işlevi Me isim alanının üyesidir.
Bir sınıfın global isimalanına bir friend yerleştirildiğinde, sokulan friend
global olur.
İsimalanı kullanımı
Bir isim, isimalanı içine üç biçimde yerleştirilebilir; adı görüntü duyarlık
işleci kullanarak belirtme, bütün adları isimalanı içinde using yönergesi
kullanarak tanıtma, veya isimleri using yönergesi kullanarak her sefer bir
adla tanıtmak. (ad ve isim aynı anlama gelir, bilmem belirtmememize gerek
varmı?)
Görüntü (çalışma alanı) duyarlığı
Sınıf içinde yer alan isimlere benzer biçimde, görüntü duyarlık işleci
kullanılarak, isimalanı içinde yeralan adlar açıkça belirtilebilir:
//:B10:ScopeResolution.cpp
namespace X{
class Y{
static int i;
public:
void f( );
};
class Z;
void func( );
}
int X::Y::i=9 ;
class X::Z{
int u,v,w;
public:
Z(int i) ;
int g( );
};
X::Z::Z(int i){u=v=w=i;}
int X::Z::g( ){return u=v=w=0;}
void X::func( ){
X::Z a(1) ;
a.g( );
}
int main( ){}///:~
Farkettiğiniz gibi X::Y::i tanımında i veri üyesi X sınıfında yuvalanmış Y
sınıfının üyesidir, yani X isimalanının değil.
Görüldüğü gibi isimalanları sınıflara çok fazla benzerler.
Using yönergesi
Bir isimalanı içinde bir belirtecin bütün özellikleri ile yazılması oldukça
uğraştırıcı olabilir, bu nedenle using anahtar kelimesini kullanmak, bütün
isimalanının bir kerede elde edilmesine yolaçtığından tercih edilir. using
yönergesi namespace anahtar kelimesi ile birlikte de kullanılabilir. using
yönergesi isimleri sanki en yakın kapalı isim alanı görüntüsüne aitmiş gibi
gösterir. Böylece ayrıntıları verilmemiş isimleri kolayca kullanabilirsiniz.
Basit bir namespace örneğini inceleyelim;
//:B10:NamespaceInt.h
#ifndef NAMESPACEINT_H
#define NAMESPACEINT_H
namespace Int{
enum sign{positive,negative};
class Integer{
int i;
sign s;
public:
Integer(int ii=0)
:i(ii),
s(i>=0? positive : negative)
{}
sign getSign( ) const {return s;}
void setSign(sign sgn){s=sgn;}
//....
};
}
#endif //NAMESPACEINT_H///:~
using yönergesinin başka bir kullanış biçimi de, Int deki bütün isimleri
ikinci bir isimalanı içine yerleştirmek, ve isimleri isimalanı içinde
yuvalamaktır.
//:B10:NamespaceMath.h
#ifndef NAMESPACEMATH_H
#define NAMESPACEMATH_H
#include "NamespaceInt.h"
namespace Math{
using namespace Int;
Integer a,b;
Integer divide(Integer,Integer);
//.....
}
#endif //NAMESPACEMATH_H///:~
Int içindeki bütün isimler bir işlev içerisinde bildirilebilir, işlev içerisinde
yuvalanmış olarak yerleştirilir;
//:B10:Arithmetic.cpp
#include "NamespaceInt.h"
void Arithmetic( ){
using namespace Int;
Integer x;
x.setSign(positive);
}
int main( ){}///:~
Burada using yönergesi kullanılmasaydı, isimalanı içinde yeralan bütün
isimlerin nitelikleri, ayrıntıları ile belirtilmek zorunda olurdu.
using yönergesinin kullanımında başlangıçta sezgiselliğe ters bir duruş var
gibidir. using yönergesi ile tanıtılan isimlerin görünürlük bölgesi,
yönergenin görüntüsünün yapıldığı yerlerdir. Fakat isimleri sanki global
görünüm sahibi gibi using yönergesini kullanarak isimlerin üzerine
bindirebilirsiniz.
//:B10:NamespaceOverriding1.cpp
#include "NamespaceMath.h"
int main( ){
using namespace Math;
Integer a; //Math::a yı saklar;
a.setSign(negative) ;
//Math::a : yı seçmak için görüntü duyarlılığı gerekli
Math::a.setSign(positive) ;
}///:~
Şimdi namespace Math' te yeralan bazı isimlerin, ikinci bir isimalanında
bulunduğunu farzedelim.
//:B10:NamespaceOverriding2.h
#ifndef NAMESPACEOVERRIDING2_H
#define NAMESPACEOVERRIDING2_H
#include "NamespaceInt.h"
namespace Calculation{
using namespace Int;
Integer divide(Integer,Integer);
//......
}
#endif //NAMESPACEOVERRIDING2_H///:~
Mademki bu isimalanı ayrıca using yönergesi ile de tanıtıldı, o zaman isim
çakışması ihtimali var demektir. Bu konudaki müphemlik adın kullanıldığı
noktada ortaya çıkar, using yönergesi sırasında değil.
//:B10:OverridingAmbiguity.cpp
#include "NamespaceMath.h"
#include "NamespaceOverriding2.h"
void s( ){
using namespace Math ;
using namespace Calculation ;
//herşey aşağıdakiler doğru ise olumlu
//!divide(1,2) ; //şüphe!
}
int main( ){}///:~
Böylece birden fazla isimalanı kullanarak çakışma ihtimali ortadan
kaldırılır.
using bildirimi
Bulunulan görüntüde using anahtar kelimesini kullanarak her seferinde bir
kez olmak koşulu ile isimleri sokabiliriz. using bildirimi using
yönergesine benzemeyen bir biçimde, sanki görüntüye global bildirilmiş
gibi isimleri işlemler. Bir using bildirimi o anki görüntüdeki bildirimdir.
sadece bulunulan görüntüde yapılır. Bunun anlamı; using yönergesinden
kaynaklanan isimler de geçersiz kılınır.
//:B10:UsingDeclaration.h
#ifndef USINGDECLARATION_H
#define USINGDECLARATION_H
namespace U{
inline void f( ){}
inline void g( ){}
}
namespace V{
inline void f( ){}
inline void g( ){}
}
#endif //USINGDECLARATION_H///:~
//:B10:UsingDeclaration1.cpp
#include "UsingDeclaration.h"
void h( ){
using namespace U ; //yönerge kullanımı
using V::f ; //bildirim kullanımı
f( ) ; //V::f( ) çağrımı
U::f( ) ; //çağrımın tamamen ayrıntılanması zorunluluğu
}
int main( ){}///:~
using bildirimi tip bilgisi haricinde tanımlayıcının tam bilgisini ayrıntısı ile
verir. Bunun da tam anlamı; isimalanı eğer aynı taşıyan bindirilmiş
işlevlere sahipse, using bildirimi bindirilmiş bütün işlevleri bindirilmiş
kümede bildirir. using bildirimi normal bildirimin yapılabileceği herhangi
bir yerde de yapılabilir. using bildirimi aynı öteki bildirimler gibi çalışır,
bir özelliği hariç;değişken listesi yerleştiremezsiniz. using bildiriminde
işlev bindirimi aynı değişkenlerle gerçeklenir (normal bindirimde bu
mümkün değildir). Bunun müphemliği bildirim noktasından ziyade
kullanım yerine kadar görünmez.
using bildirimi bir isimalanı içinde de bulunabilir. Başka herhangi bir
yerde (adın bildirildiği yer) olduğu gibi, aynı etkiye sahiptir.
//:B10:UsingDeclaration2.cpp
#include "UsingDeclaration.h"
namespace Q{
using U::f ;
using V::g ;
//....
}
void m( ){
using namespace Q;
f( ); //U::f( ) çağrımı
g( ); //V::g( ) çağrımı
}
int main( ){}///:~
using bildirimi aslında bir ad takmaktır, yani takma ad koymaktır. Ve bu
sayede aynı işlev farklı isimalanlarında bildirilebilir.Farklı
isimalanlarından alarak aynı işlevin yeniden bildirimini bitirebilirseniz,
herşey olumlu gelişir, böylece müphemlik ve tekrarlar ortadan kalkar.
İsimalanlarının kullanımı
Yukarıdaki kurallarla ilk kez karşılaşıldığında ve özellikle de sürekli
kullanılacağı düşüncesi, başlangıçta korkutucu gelir. Fakat kullanım biçimi
kavranıldığı zaman, isimalanının basit biçimi ile de kullanım uygulamaya
sokulabilir. Burada hatırlanması gereken anahtar unsur global using
yönergesi (görüntü dışı "using namespace" yönergesi) kullandıldığında, o
dosya için bir isimalanı açmışsınız demektir. Bu genellikle uygulama
dosyaları (.cpp uzantılı dosyalar) için oldukça şık olur, zira using
yönergesi o dosyanın sadece derleme bitişine kadar etkindir. Yani başka
dosyaları etkilemez. Böylece isimalanlarının denetimi, bir seferde bir
uygulama dosyası ile ayarlanır. Örnek olarak; eğer özel bir uygulama
dosyasında çok sayıdaki using yönergesi kullanımından ötürü bir isim
çatışması farkederseniz, o dosyayı belli nitelemelerle veya using
bildirimleri ile değiştirip çatışmayı ortadan kaldırmak oldukça kolaydır.
Böylece başka uygulama dosyalarının değiştirilmesi gerekmez.
Başlık dosyaları ise değişik unsurlardır. Başlık dosyalarına hiçbir zaman
sanal bir global using yönergesi konmak istenmez. Bunun sebebi başka,
ayrıca ekli bulunan başlık dosyalarının da isimalanları taşımasıdır. (başlık
dosyalarında başka başlık dosyaları bulunabilir) .
Bu nedenle başlık dosyalarında ya açık bir biçimde nitelemelerle, ya da
görüntüsü belli using yönergeleri ve bildirimleri kullanmak iyi olur. Bu,
kitabımızdaki uygulamalarda rastlayacağınız pratik usuldür. Böylece
global isimalanlarını kirletmemiş olacak, C++ dünyasını kendi temiz
isimalanları ile başbaşa bırakacağız.
C++ static üyeleri
Bir sınıfın bütün nesneleri tarafından kullanılabilen, bellekte tek bir
saklama alanı gerektiği zamanlar olabilir.
C dilinde global değişken kullanılırdı, ama çok güvenli olduğu
söylenemez. Global değişken herhangi bir kişi tarafından kolaylıkla işleme
tabi tutulabilir ve büyük projelerde benzer isimler yüzünden isim
çatışmaları olabilir. Bir verinin sanki globalmiş gibi bellekte tutulup sınıf
içinde gizlenmesi, ve açık biçimde o sınıfla ilintili olması en ideal durum
olurdu.
Böyle bir durum sınıf içinde, static veri üyeleri ile başarılır. static veri
üyesi için bellekte tek bir yer bulunur ve sınıfla üretilen nesne sayısı bu
durumu hiç bir şekilde etkilemez. Nesne sayısı kaç tane olursa olsun.
Bütün nesneler o veri üyesi için, aynı static saklama alanını paylaşırlar,
yani kullanırlar. Böylece bir bakıma birbirleri ile iletişim yolu da bulurlar.
Burada bilinmesi gerekli en önemli husus static veri sınıfa aittir, adı sınıf
içinde görünür, ve public, private veya protected olabilirler.
static veri üyelerinin bellek yerlerinin tanımlanması
Yukarıda belirttiğimiz gibi üretilen nesne sayısı kaç tane olursa olsun,
static veri üyeler için tek bir bellek alanı bulunur, bu nedenle bu tek yer
mutlaka açıkça tanımlanmalıdır. Derleyici bellek alanını belirleme işini
bizim için yapmaz. Eğer static veri üyesi bildirilmiş fakat
tanımlanmamışsa yani yeri belirtilmemişse, ilişimci bildirim yapıldığı ama
tanım yapılmadığı hata iletisini aktarır. Tanım mutlaka sınıf dışında
yapılmalıdır (satıriçine izin verilmez) ve tek bir tanım yapılmalıdır.
Böylece sınıfın uygulama dosyasına ortak olarak koymak mümkün olur.
Yazım biçimine bakıldığında bazılarına biraz zor görünebilir, ama aslında
tamamen mantıklı bir yapısı vardır. Örnek olarak; sınıf içinde static veri
üyesi oluşturalım
class A{
static int i;
public:
//....
};
Daha sonra tanım dosyasında mutlaka aşağıdakine benzer static veri üyesi
bellek yerini tanımlamalısınız.
int A::i=1 ;
Eğer normal bir global değişken tanımlanmış olsaydı aşağıdaki gibi
olurdu;
int i=1 ;
Fakat burada belirleme için görüntü duyarlık işleci ve sınıf adı kullanıldı
A::i .
Bazıları A::i nin private olması ile ilgili olarak zihinlerinde ikirciklenirler.
Ve sanki bazı şeylerin yanlış gideceğine dair düşünceler oluşur. Bu yapı,
koruma mekanizmasını yıkıyormu?. Aslında iki nedenle bu yapı tamamı
ile güvenlidir. Birincisi, tanımdaki tek uygun ilk değer atama yeri
burasıdır. Gerçekten, static veri, eğer yapıcısı olan bir nesne olsa idi, (=)
işlecini kullanmak yerine yapıcı işlev çağrılırdı. İkinci neden ise, tanım bir
kez yapıldığında, son kullanıcı ikinci bir tanım yapamaz, zira o zaman
ilişimci hata iletisi üretir. Ve sınıfı geliştiren kişi tanım yapmaya mecbur
bırakılır, veya kod sınama esnasında ilişimden uzak tutulur. Bütün bunlar,
tanımın sadece bir kez yapılmasını sağlar ve bu da tamamen sınıf
geliştircisinin ellerindedir.
static veri üyesinin ilk değer atama açıklamasının tamamı sınıf
görüntüsünün içinde bulunur. Örnek;
//:B10:Statinit.cpp
//statik ilk değer atayıcısının görüntüsü
#include <iostream>
using namespace std ;
int x=100;
class WithStatic{
static int x;
static int y;
public:
void print( ) const {
cout<<"WithStatic::x= "<<x<<endl ;
cout<<"WithStatic::y= "<<y<<endl ;
}
};
int WithStatic::x=1 ;
int WithStatic::y=x+1 ;
//WithStatic::x NOT ::x
int main( ){
WithStatic ws ;
ws.print( ) ;
}///:~
Burada WithStatic:: belirlemesi ile WithStatic görüntüsü tanımın
tamamını kapsar.
static dizilere ilk değer atanması
8. bölümde static const anlatılmıştı. Orada sınıf gövdesine sabit bir
değerin yerleşimine, nasıl izin verildiği gösterilmişti. static nesne
dizilerini, ister sabit (const) ister değişken (non-const) olsun, oluşturmak
mümkündür. Yazım biçimi kendi içinde oldukça tutarlıdır.
//:B10:StaticArray.cpp
//sınıflarda bulunan static dizilere ilk değer atama
class Values{
//static sabitlerin ilk değer atamaları yerinde yapılır
static const int scSize=100;
static const long scLong=100;
//static dizilerde otomatik sayma çalışır
//Dizilerin (tümleşik olmayanlar ve static olmayanlar)
//mutlaka ilk değer atamaları dışarıda yapılmalıdır.
static const int scInts[] ;
static const long scLongs[] ;
static const float scTable[] ;
static const char scLetters[] ;
static int size ;
static const float scFloat ;
static float Table[] ;
static char Letters[];
};
int Values::size=100 ;
const float Values::scFloat=1.1 ;
const int Values::scInts[]={
88,48,18,45,1
};
const long Values::scLongs[]={
88,48,18,45,1
};
const float Values::scTable[]={
1.1,2.2,3.3,4.4
};
const char Values::scLetters[]={
'a','b','c','d','e','f','g','h','i','j'
};
float Values::table[4]={
1.1,2.2,3.3,4.4
};
char Values::Letters[10]={
'a','b','c','d','e','f'',
'g','h','i','j'
};
int main( ){Values v; }///:~
static const özelliği taşıyan bütün veri tiplerinin tanımları sınıf içinde
yapılır. Fakat bundan başka herşeyi o üye için tek dış tanımla yapmalısınız
(static const olsalar bile, her tip dizilerde dahil). Bu tanımlar da iç ilişim
bulunduğu için, başlık dosyalarında yer alabilirler. Statik dizilerin ilk değer
atama yazım biçimi, otomatik saymayı da kapsayan herhangi bir birikintide
olduğunun aynısıdır.
Ayrıca şunu da belirtelim, sınıf tiplerinden static const nesneler ve bu
nesnelerin dizilerini de üretebiliriz. Ama bunlara, yerleşik static const veri
tiplerinde olduğu gibi, ilk değer atama işlemi satıriçi işlemi ile yapılamaz.
Yani sınıf tiplerinin static const nesnelerine ilk değer ataması aynı satırda
yapılamaz.
//:B10:StaticObjectArray.cpp
//sınıf nesnelerinin statik dizileri
class X{
int i;
public:
X(int ii):i(ii){}
};
class Stat{
//buradaki çalışmaz bunu isteseniz bile
//!static const X x(100) ;
//hem const hemde const olmayan sınıf nesnelerinin
//ilk değer atamaları mutlaka dışarıda yapılmalıdır.
static X x2 ;
static X xTable2[] ;
static const X x3 ;
static const X xTable3[] ;
};
X Stat::x2(100) ;
X Stat::xTable2[]={
X(1),X(2),X(3),X(4)
};
const X Stat::x3(100) ;
const X Stat::xTable3[]={
X(1),X(2),X(3),X(4)
};
int main( ){Stat v; }///:~
Sınıf nesnelerinin hem const hem de const olmayan static dizilerinin ilk
değer atama işlemleri, static tanım yazımını takipeden aşama da, benzer
biçimde yapılmalıdır.
Yuvalanmış ve yörel sınıflar
Başka sınıfların içine yuvalanmış bulunan sınıflara da kolaylıkla static veri
üyeleri yerleştirebilirsiniz. Bu tür üyelerin tanımları zekice ve açık şekilde
geliştirilmiş eklerdir. Aslında görünüm duyarlığının başka bir seviyesini
kullanmaktan ibarettir. Burada hemen şunu belirtelim; yörel sınıfların
içinde static veri üyeleri bulundurulamaz (yörel sınıf; işlevlerin içinde
tanımlanmış sınıf demektir) . Böylece;
//:B10:Local.cpp
//static üyeler ve yörel sınıflar
#include <iostream>
using namespace std ;
//CAN yuvalanmış sınıfı static veri üyelere sahip
class Outer{
class Inner{
static int i ; //tamam
};
};
int Outer::Inner::i=47 ;
//yörel sınıfta static veri üyeleri bulunamaz
void f( ){
class Local{
public:
//! static int i ; ///hata!!!!!!
//Nasıl tanımlayacaksınız?
} x;
}
int main( ){Outer x;f( );}///:~
Yörel bir sınıfta static üye ile ortaya çıkan sorunu hemen gördünüz. Dosya
görünümünde bu veri üyesini nasıl tanımlarsınız. Yörel sınıflar nadiren
kullanılırlar bunu hatırlayın!.
static üye işlevler
Static veri üyeleri gibi static üye işlevlerde oluşturulabilir. Bu işlevler
sınıfın belli bir nesnesinden ziyade tamamı için çalışır. Global veya yörel
isimalanını kirleten global işlevler yerine, işlevler sınıf içine konarak bu
işlemler yapılır. Static üye işlev oluşturulduğunda belli bir sınıfla bağlantı
açıklamış olursunuz.
Static üye işlev çağrımı bilinen normal işlev çağrımı gibi yani nokta (.)
veya ok (->) ile yapılır. Fakat daha da tipik static üye işlev çağrımı kendisi
tarafından yapılandır, belli bir nesne bulunmaz, ve görüntü duyarlık işleci
kullanılır.
Yani static üye işlevi sadece kendi sınıfının nesneleri çağırmaz, tek
başlarına da kullanılabilirler. Aşağıda bir örnek verilmiştir;
//:B10:SimpleStaticMemberFunction.cpp
class X{
public:
static void f( ){ };
};
int main( ){
X::f( ) ;
}///:~
Bir sınıfın içinde static üye işlevler gördüğünüz de, tasarımcının o üye
işlevi sınıfın tamamına bağlantılı tasarımladığını düşünmelisiniz.
Static üye işlev normal veri üyelerine erişemez, sadece static veri
üyelerine erişir. Sadece başka static üye işlevleri çağırabilir. Normalde
herhangi bir nesnenin adresi (this), bir üye işlev çağırdığında hemen
aktarılır, fakat static üyeler de, bu (this) adresi bulunmaz. İşte normal
üyelere erişememe nedeni budur. Bu nedenle de global işlevlere göre çok
az bir işlem hızlanması olur, zira static üye işlevde, this adresi aktarımı
gibi ek bir görevi yoktur. Aynı zamanda üye işlevin sınıf içinde bulunma
üstünlüklerinden de yararlanılır.
Veri üyeleri için static kelimesi, sınıfın bütün nesneleri için o veri üyesine
bellekte tek bir parça ayrıldığının işaretidir. Buna paralel olarak, işlev
içindeki nesneleri tanımlayan static kullanımında, işlevin tüm
çağrımlarında yörel değişkenin sadece tek bir kopyası kullanılır.
Şimdi static veri üyeleri ve static üye işlevlerin birlikte kullanıldığı bir
örnek verilecek;
//:B10:StaticMemberFunctions.cpp
Class X{
int i ;
static int j ;
public:
X(int ii=0):i(ii){
//static olmayan üye işlev static olan
//üye işlev veya veriye erişebilir
j=i ;
}
int value( ) const{return i;}
static int incr( ){
//!i++ ; //hata static üye işlev
//static olmayan veri üyeye erişemez
return ++j ;
}
static int f( ){
//!val( ) ; //hata satic üye işlev
//static olmayan üye işleve erişemez
return incr( ) ; //tamam çağrı static
}
};
int X::j=0 ;
int main( ){
Xx;
X* xp=&x ;
x.f() ;
xp->f( ) ;
X::f( ) ; //sadece static üyelerle çalışır
}///:~
Static üye işlevler this göstergecine sahip olmadıkları için, ne static
olmayan veri üyelerine erişebilirler ne de static olmayan üye işlevleri
çağırabilirler.
Yukarıdaki örnekte main( ) işlevinde static üye seçimi nokta (.) ve ok (->)
işareti ile yapılır, böylece işlev nesneye bağlanır.Bunların yanında nesne
olmadan da sınıf adı ve görüntü duyarlık işleci kullanılarak seçim
yapılabilir. (buradaki incelik; static üyenin belli bir nesneye değil
tamamen sınıfa ait olmasından kaynaklanmaktadır.)
Şimdi ilginç bir özelliğe geleceğiz; bu da static üye nesnelere ilk değer
atama biçiminden gelmektedir, aynı sınıfın static veri üyesini o sınıfın
içine yerleştirebilirsiniz.Şimdi bununla ilgili bir örnek verelim; burada
yapıcı işlevi private yaparak Egg tipinin tek bir nesnesi olacaktır. Bu
nesneye erişebileceksiniz, fakat yeni benzeri bir nesne ya da nesneler
oluşturamayacaksınız.
//:B10:Singleton.cpp
//Aynı tipin static üyesi bu tipin
//tek bir nesnesi olmasını güvenceye alır.
//Buna tekleyici kalıbı denilir.
#include <iostream>
using namespace std ;
class Egg{
static Egg e ;
int i ;
Egg(int ii):i(ii){}
Egg(const Egg&) ; //kopya-inşaatını engeller
public:
static Egg* instance( ){return &e;}
int val( ) const {return i;}
};
Egg Egg::e(47) ;
int main( ){
//! Egg x(1) ; //hata! Egg oluşturamaz
//tek bir oluşuma erişilebilir :
cout<<Egg::instance( )->val( )<<endl ;
}///:~
e için ilk değer atama işlemi sınıf bildirimi tamamlandıktan olur.Bildirim
tamamlandıktan sonra, derleyici bellek yerleşimi ve yapıcı çağrımı için
gereken bilgilere sahip olur.
Başka nesnelerin oluşumunu tamamı ile engellemek için ise bazı başka
eklere ihtiyaç vardır; kopya yapıcısı (copy-constructor) denilen ikinci bir
private yapıcı gibi. Kitabın bu noktasında neden böyle bir şeye gerek
duyulduğunu anlayamayacaksınız, zira bu konu kitabın bir sonraki
bölümünde ayrıntıları ile anlatılacak. Yine de kısaca not olarak bazı
bilgileri aktaralım; yukarıdaki örnekte tanımlanmış kopya-yapıcıyı
kaldırmış olsaydınız, aşağıda gösterilen biçimde yeni bir nesne
oluşturabilirdiniz
Egg e=*Egg::instance( ) ;
Egg e2(*Egg:instance( )) ;
Bunların her ikisi de kopya-yapıcı kullanır, bu nedenle olasılıkları ortadan
kaldırmak için kopya-yapıcı private olarak bildirilir. (tanım gerekmez, zira
kendisine herhangi bir çağrı yapılmaz). Bir sonraki bölümün büyük bir
kısmı kopya-yapıcıların incelenmesi ile geçeceği için, zihinlerde bu konu
ile ilgili bulanık hiç bir şey kalmayacaktır.
Bağımlı Static başlatma
Belli bir çevrim biriminin içinde nesne tanımlarının göründüğü sıralamada
static nesnelerin ilk değer atamaları güvence altındadır. Silinme (yıkıcı
işlev çağrımı) işlemi ise ilk değer atama sıralamasının tersi düzeninde
yerine getirilir.
Çevrim birimlerinin kendi aralarında static nesnelerin ilk değer
atamalarının sırası hakkında kesinlik yoktur, ve dil bunun yolunu
güvenceye alan bir ipucu vermez. Bu sıralamanın dil tarafından
belirlenmemiş olması bazı sorunlara yolaçar. Bu anlık talihsizliğe örnek
olarak, (ilkel bir işletim sistemi durdurulur ve süreçlerden karmaşık olanı
ortadan kaldırılır) eğer bir dosyanın içeriği;
//İlk dosya
#include <fstream>
std::ofstream out("out.txt") ;
ve ikinci bir dosya ilk değer atayıcılardan birinde out nesnesini kullanır
//ikinci dosya
#include <fstream>
extern std::ofstream out ;
class Oof{
public:
Oof( ){std::out<<"ohh" ;}
}Oof ;
Program çalışabilir, veya çalışmayabilir. Programlama ortamı, ilk dosyayı
ikinciden önce inşa edip ilk değer atama işlemini de önce yerine getirirse,
program çalışması ile ilgili bir sorun olmaz. Fakat bir şekilde ikinci dosya
birinciden evvel inşa edilip ilk değer ataması yapılırsa, Oof( ) yapıcısı out
varlığını gerektirdiğinden karmaşa ortaya çıkar, program çalışmaz.
Bu sorun sadece birbirine bağımlı static nesne ilk değer atayıcıları varsa
ortaya çıkar. Bir çevrim biriminde bulunan static'lere, o çevrim
birimindeki bir işlevin ilk uyarımından önce ilk değerleri atanır.- fakat bu
main( ) işlevinden sonra olabilir. Burada bir konuyu daha belirtelim; eğer
static nesneler farklı dosyalarda ise ilk değer atama sıralarından emin
olunamaz.
Başka ince özellikli bir örnek daha verelim; bir dosyada global görünümde
extern int y ;
int x=y+1 ;
ve ikinci bir dosyada yine global görünümde
extern int x ;
int y=x+1 ;
Bütün static nesneler için, programcı tarafından belirlenen dinamik ilk
değer atama işlemlerinden önce, ilişim-yükleme
mekanizmaları static nesnelere ilk değer atama işlemini, sıfır yerleştirerek
güvenceye alır. Önceki örnekte fstream out nesnesinin kapladığı bellek
yerinin sıfırlanması özel anlam taşımaz, aslında yapıcı işlev çağrılana
kadar tanımsız sayılır.Yerleşik tipler de ise, ilk değer atamalarının
sıfırlanması bir anlam taşımaktadır. Eğer dosyalara ilk değer atama
sıralaması yukarıda gösterildiği gibi yapılırsa, y başlangıçta ilkdeğer
ataması ile statik olarak sıfır değeri alır, böylece x'in değeri 1 (bir) olur ve
y dinamik olarak ilk değer atanması ile 2 (iki) olur. Bir şekilde dosyaların
ilk değer atama sıraları değiştirilirse, x static olarak ilk değer ataması ile
sıfır olur, y dinamik olarak olarak 1 (bir) ilk değerini alır, ve daha sonra
x'in değeri iki (2) olur.
Programcıların mutlaka dikkat etmeleri gereken ; static ilk değer atama
bağımlılıkları bulunan programların bir işletim sisteminde çalışırken, başka
bir derleme ortamına aktarıldığında çalışmadıkları ve bazı acayip
davranışlar göstermesi hususudur.
Ne yapılmalı
Bu sorunla ilgili üç türlü yaklaşım vardır;
1-Yapma!.Static ilk değer atama bağımlılığından sakınmak en iyisidir.
2-Eğer yapmak zorundaysanız, kritik static nesne tanımlarını tek bir
dosyaya yerleştirin, böylece ilk değer atama işlemlerini doğru sıralamada
gerçeklersiniz.
3-Eğer static nesnelerin çevrim birimlerinde bulunması zorunluluğuna
ikna edilirseniz, -kütüphanelerde olduğu gibi, yani sizin
denetleyemediğiniz programcının kullandıkları- sorunu çözmek için iki
programlama tekniği bulunmaktadır.
Birinci yöntem
Bu teknik Jerry Schwarz tarafından iostream (cin,cout ve cerr farklı bir
dosyada static olarak bulunur) kütüphanesi geliştirilirken bulunmuştur.
Aslında ikinci tekniğe göre daha alt düzeyde bir yaklaşım olmasına rağmen
çok uzun bir süredir bilinmektedir ve bunu kullanan kodlara çok rastlanır.
Bu yüzden çalışma biçimini anlamak önemlidir.
Bu teknikte kütüphane başlık dosyalarında ek bir sınıfa gereksinim vardır.
Bu sınıf, kütüphanenin statik nesnelerinin, dinamik ilk değer
atamalarından sorumludur. İşte basit bir örnek;
//:B10:Initializer.h
//static ilk değer atama tekniği
#ifndef INITIALIZER_H
#define INITIALIZER_H
#include <iostream>
extern int x ; //bildirim, tanım değil
extern int y ;
class Initializer{
static int initCount ;
public:
Initializer( ){
std::cout<<"Initializer( )"<<std::endl ;
//ilk seferde başlatma olur
if(initCount++=0){
std::cout<<"ilk değer atamasını gerçekle"
<<std::endl;
x=100 ;
y=200 ;
}
}
~Initializer( ){
std::cout<<"~Initializer( )"<<std::endl ;
//son kez silme yap
if(--initCount==0){
std::cout<<"silme işlemi tamam"
<<std::endl ;
//başka silme işlemi varsa yapılır
}
}
};
//aşağıdaki Initializer.h bulunan her dosyada bir nesne oluşturur
//nesne sadece o dosyada görünür;
static Initializer init ;
#endif //INITIALIZER_H///:~
x ve y bildirimleri sadece bu nesnelerin varlığını duyurur, bellekte yer
ayırımında bulunmaz. Initializer init tanımı ise, başlık dosyasının
bulunduğu dosyalardaki nesneler için bellekte yer ayarlar. Static adından
ötürü (bu sefer görünürlüğü denetler, bellek yerleşim türüne bakmaz;
bellek yerleşimi dosya görüntüsündedir), sadece çevrim biriminde görünür,
bu yüzden ilişimci çok sayıdaki tanım hatasına dikkat etmez.
Şimdi x,y ve initCount tanımları içeren bir dosya verelim;
//:B10:InitializerDefs.cpp {O}
//Initializer.h dosyasının tanımları
#include "Initializer.h"
//static ilk değer atayıcılar bütün değerleri sıfır yapar
int x ;
int y ;
int Initializer::initCount ;
///:~
(tabii bu dosyaya başlık dosyası varsa init'in static durumunu da eklemek
mümkündür.Şimdi kütüphane kullanıcısı tarafından iki başka dosya
oluşturulduğunu kabul edelim;
//:B10:Initializer.cpp {O}
//static ilk değer atanması
#include "Initializer.h"
///:~
ve
//:B10:Initializer2.cpp
//{L} InitializerDefs ilk değer atayıcısı
//static ilk değer atayıcıları
#include "Initializ+ dilinin er.h"
using namespace std ;
int main( ){
cout<<"main( ) içinde"<<endl ;
cout<<"main( ) terkediliyor"<<endl ;
}///:~
Hangi çevrim biriminde ilk değer atamasının yapıldığı artık önemli değil.
İlk defa Initializer.h dosyasını taşıyan çevrim birimi ilk değer atamasını
yapar. initCount değeri sıfırlanarak ilk değer ataması gerçeklenir.(buarada
ağırlıklı olarak dinamik ilk değer atamalarından evvel static bellek
yerlerinin sıfırlanma işlemi gerçeklenir). Geri kalan bütün çevrim
birimlerinde, initCount değeri sıfırdan farklı olur ve ilk değer atama işlemi
atlanır. Silme işlemi sırası ise ilk değer atama sırasına göre tersi sıralamada
yapılır, ~Initializer( ) yıkıcı işlevi bu görevi bir kez yerine getirerek,
işlemin yapılmasını güvenceye alır.
Yerleşik veri tiplerinin kullanıldığı bu örnekte tipler global static
nesnelerdir. Aynı tekniği sınıf tipleri içinde uygulayıp çalıştırmak
mümkündür, fakat o zaman nesnelere ilk değer atamaları mutlaka dinamik
olarak Initializer sınıfı tarafından yapılmalıdır. Bunu gerçeklemenin bir
yolu; sınıfları yapıcı ve yıkıcı işlevleri olmadan oluşturmaktır, bunun
yerine ilk değer atama ve silme işlemlerini başka isimlerle yapmaktır. Daha
yaygın kullanılan yaklaşım ise nesneleri gösteren göstergeçler
kullanmaktır, ve nesneleri oluşturmak için Initializer( ) işlevi içinde new
anahtar kelimesi ile kullanmaktır.
İkinci teknik
Uzun süre kullanılan yukarıdaki teknikten sonra günün birinde birisi gelip
şimdi anlatacağımız tekniği öğretti. Kim olduğunu hala bilmiyoruz. Yeni
teknik hem daha basit hem daha açıktı. Aslında çok uzun süre sonra böyle
bir yöntemin bulunmuş olması C++ dilinin zorluğuna saygınlık getirdi.
Bu teknik, işlevlerin içindeki statik nesnelere ilk değer ataması işlemi
sadece işlev çağrıldığında yapılır gerçeğine dayanır. Bunu aklınızda
tutarsanız, çözülmeye çalışılan sorunun, aslında static nesnelere ilk değer
ataması işleminin ne zaman yapılacağı olmadığıdır. Yapılmak istenen ilk
değer atamalarının doğru sıralamada olmasıdır. Yani çözülmesi gereken iş
doğru sıralama.
Bu teknik oldukça düzgün ve zekicedir. Herhangi bir ilk değer atama
bağımlılığı için, işlev içine bir static nesne yerleştirilir, bu işlev ilgili
nesneye dayanak (reference) olan değeri geri döndürür. Bu yol, static
nesneye erişebilmek için işlev çağrımını gerektirir, ve nesneye erişimin tek
yolu budur, ve eğer nesnenin bağımlı olmasından dolayı öbür static
nesnelere erişim ihtiyacı varsa, mutlaka öbür işlevleri çağırması gerekir.
Ve işlevin ilk çağrılmasında ilk değer ataması yapılması zorunluluğu
doğar. Kod tasarımından dolayı static ilk değer atama sıralaması mutlaka
doğru düzende olur, yani ilişimcinin gelişigüzel ilk değer ataması
sıralamasında değil.
Örnek olarak birbirine bağımlı iki sınıf geliştirelim. İlkinde bool olsun ve
ilk değer ataması yapıcı işlev tarafından gerçeklensin, bu sayede sınıfın
static unsuru için yapıcı işlevin çağrılırsa gerçekleneceği söylenebilir
(Programın başlangıcında static saklama alanı ilk değer ataması olarak
sıfıra eşitlenir, eğer yapıcı işlev çağrılmazsa o zaman bool değeri false
olur)
//:B10:Dependency1.h
#ifndef DEPENDENCY1_H
#define DEPENDENCY1_H
#include <iostream>
class Dependency1{
bool init ;
public:
Dependency1( ):init(true){
std::cout<<"Dependency1 construction: "
<<std::endl ;
}
void print( ) const{
std::cout<<"Dependency1 init : "
<<init<<std::endl ;
}
};
#endif //DEPENDENCY1_H///:~
Yapıcı işlev çağrıldığında ilaveten, ilk değer ataması yapılırsa nesnenin
durumunu bulmak için print( ) işlevini çalıştırır.
İkinci sınıfın ilk değer ataması, bağımlılık dolayısı ile ilk sınıfın nesnesi
tarafından yapılır.
//:B10:Dependency2.h
#ifndef DEPENDENCY2_H
#define DEPENDENCY2_H
#include "Dependency1.h"
class Dependency2{
Dependency1 d1 ;
public:
Dependency2(const Dependency1& dep1):d1(dep1){
std::cout<<"Dependency2 construction: " ;
print( ) ;
}
void print( ) const{d1.print( );}
};
#endif //DEPENDENCY2_H ///:~
Yapıcı işlev kendini duyurur, ve çağrıldığında ilk değer atamasını görmek
için d1 nesnesinin durumunu print( ) ettirir.
Nelerin yanlış gidebileceğini görmek için aşağıdaki dosyada static nesne
tanımları yanlış sıralamada yerleştirilir, yani sanki ilişimci Dependency2
nesnelerine ilk değer atamalarını Dependency1 nesnelerinden önce yapar.
Daha sonra sıralama terslenerek doğru çalışmanın nasıl olacağı gösterilir.
Böylece sonunda ikinci teknik gösterilmiş olur.
Çıktıyı daha okunabilir yapmak için separator( ) işlevi oluşturulur.
Buradaki incelik; eğer bir işlev bir değişkenin ilk değer atamasını
gerçekleştirmiyorsa, o işlev global olarak çağrılamayacağı için, böylece
separator( ) işlevi global değişkenlerin bir çiftinin ilk değer atamasında
kullanılan boş değer üretir.
//:B10:Tecnique2.cpp
#include "Dependency2.h"
using namespace std ;
//döndürülecek değer global ilk değer atayıcısı olarak kullanılacaktır
int separator( ){
cout<<"........................"<<endl ;
return 1 ;
}
//bağımlılık sorununu benzekini oluştur
extern Dependency1 dep1 ;
Dependency2 dep2(dep1) ;
Dependency1 dep1 ;
int x1=separator( ) ;
//bu sıralamada olursa çalışma tamam
Dependency1 dep1b ;
Dependency2 dep2b(dep1) ;
int x2=separator( ) ;
//işlevlerin içine static nesneleri yerleştirme başarıldı
Dependency1& d1( ){
static Dependency1 dep1 ;
return dep1 ;
}
Dependency2& d2( ){
static Dependency2 dep2(d1( )) ;
return dep2 ;
}
int main( ){
Dependency2& dep2=d2( ) ;
}///:~
d1( ) ve d2( ) işlevleri Dependency1 ve Dependency2 nesnelerinin static
durumlarının sarmalanmasını sağlar. Şimdi static nesnelere işlevleri
çağırarak ve ilk işlev çağrımında static ilk değer atamasını mecbur
kılmanın tek yolu elde edilir. Bunun anlamı; ilk değer atamalarının doğru
yapılacağı güvenceye alınmış demektir. Bunu programı çalıştırdığınızda,
çıktılara bakarak görebilirsiniz.
Şimdi tekniği doğru uygulamak için kodları nasıl düzenleyeceğinizi
görelim. Normalde static nesneler ayrı dosyalarda tanımlanmalıdır (zira
buna bazı nedenlerden zorlanırsınız; static nesnelerin farklı dosyalarda
tanımlanmasının sorun nedeni olduğunu hatırlayınız), böylece onun yerine
farklı dosyalarda sarmalama işlevlerini tanımlarsınız. Ayrıca bunlar başlık
dosyalarında bildirilmeleri gerekir:
//:B10:Dependency1StatFun.h
#ifndef DEPENDENCY1STATFUN_H
#define DEPENDENCY1STUTFUN_H
#include "Dependency1.h"
extern Dependency1& d1( ) ;
#endif //DEPENDENCY1STUTFUN_H ///:~
Aslında işlev bildiriminde extern anahtar kelimesi çıkarılabilir.İşte ikinci
başlık dosyası;
//:B10:Dependency2StatFun.h
#ifndef DEPENDENCY2STATFUN_H
#define DEPENDENCY2STUTFUN_H
#include "Dependency2.h"
extern Dependency2& d2( ) ;
#endif //DEPENDENCY2STUTFUN_H//:~
Şimdi daha önceden static nesne tanımlarını yerleştirdiğiniz uygulama
dosyasında ki yerlere sarmalama işlevlerinin tanımlarını yerleştiririz:
//:B10:Dependency1StatFun.cpp {O}
#include "Dependency1StutFun.h"
Dependency1& d1( ){
static Dependency1 dep1 ;
return dep1 ;
}///:~
Büyük ihtimalle başka kodlar da dosyalara yerleştirilir. İşte başka bir
dosya;
//:B10:Dependency2StutFun.cpp {O}
#include "Dependency1StutFun.h"
#include "Dependency2StutFun.h"
Dependency2& d2( ){
static Dependency2 dep2(d1( )) ;
return dep2 ;
}///:~
Böylece şimdi farklı sıralamada ilişimlenebilecek iki dosyamız var. Ve
eğer varsa normal static nesneler ilk değer atamaları uygular. Hatalı ilk
değer atama tehlikesi bulunmamaktadır:
//B10:Technique2b.cpp
//{L} Dependency1StutFun Dependency2StutFun
#include "Dependency2StutFun.h"
int main( ){d2( );}///:~
Yukarıdaki programı çalıştırdığınız zaman Dependency1 static
nesnelerinin ilk değer atamaları Dependency2 static nesnelerinden daha
önce gerçeklenir. Ayrıca farkedeceğiniz gibi 2. teknik, 1. teknikten çok
daha basittir.
d1( ) ve d2( ) işlevlerini satıriçi işlev olarak ilgili başlık dosyaları ile,
yazmayı arzu etmek çok doğaldır. Fakat bu açık şekilde yapılması zorunlu
birşey değildir. Satıriçi işlevler göründüğü her dosyada ikilenirler. Ve bu
ikilenme static nesne tanımlarını da içerir. Satıriçi işlevlerin otomatik
olarak iç ilişiminin olması nedeni ile, değişik çevrim birimlerinde çok
sayıda static nesnelerin bulunmasına yolaçar ve bu da doğal olarak sorun
kaynağıdır. Yanlız burada önemle belirteceğimiz husus; sarmalama
işlevlerinin sadece tek tanımlarının olması zorunluluğudur. Bu nedenle
sarmalama işlevleri satıriçi işlev olamazlar. Sarmalama işlevi satıriçi işlev
olamaz.
Diğer ilişim özellikleri
Bir C++ programı yazarken, C kütüphanesi kullanırsanız ne olur?. C işlev
bildirimi yaparsanız:
float f(int a,char b) ;
C++ derleyicisi bu adı _f_int_char benzeri biçimde ve işlev bindirimini
destekleyecek şekilde düzenler (ve güvenli tip ilişimi). C kütüphanenizi
derleyen C derleyicisi ise aynı adı açık biçimde değil, iç adı olarak _f
biçiminde düzenler. Bu yüzden C++ daki f( ) işlev çağrımı ilişimci
tarafından çözümlenemez.
C++ da bulunan kaçış mekanizmaları diğer ilişim özellikleridir, extern
anahtar kelimesine bindirim yapılarak sağlanır. extern anahtar kelimesi bir
string tarafından takip edilir ve bu string bildirimini istediğiniz ilişimi
belirler, bildirim tarafından takip edilir.
extern "C" float f(int a, char b) ;
Bu derleyiciye f( ) işlevine C ilişimi sağlar, böylece ayrıntılı düzenlemeye
gerek kalmaz. Standart tarafından desteklenen sadece iki tip ilişim vardır
ve bunlar; C ve C++ dır. Derleyici üreticilerinin kullanım kılavuzlarında
belirttikleri, benzer şekilde destekledikleri öteki diller bulunmaktadır.
Eğer diğer ilişim özelliklerine sahip bildirim grupları varsa, bunlar zincirli
ayraçlara konulmalıdır, aşağıdaki gibi;
extern "C"{
float f(int a, char b) ;
double d(int a, char b) ;
}
Veya başlık dosyası için;
extern "C"{
#include "Mybas.h"
}
C++ derleyicisi üretenlerin büyük çoğunluğu diğer ilişim özelliklerini C ve
C++ ile çalışan başlık dosyalarının içinde kullanmaktadırlar. Bu nedenle
kullanırken şüphe duymaya gerek bulunmaz.
ÖZET
static anahtar kelimesi bazen bellek yerinin denetiminde, bazen de bir adın
ilişim ve görünürlüğünde kullanıldığı için kafa karıştırıcı olabilmektedir.
C++ dilinde isim alanlarının kullanımı ile, büyük projelerde isimlerin
denetimi hem daha esnek olmuş, hem de daha gelişmiştir ve böylece
isimlerle ilgili herşey daha kolaylaşmıştır .
Sınıfların içindeki static, isimleri programlarda denetlerken birden fazla
usul kullanılabilir. Global isimlerle diğer isimler çatışmaz, erişim ve
görünürlük program içinde korunur, kodların bakımında daha fazla
denetim sağlanır.
Örnekler
-------------1#include <iostream>
class CDummy {
public:
static int n;
CDummy () { n++; };
~CDummy () { n--; };
};
int CDummy::n=0;
int main () {
CDummy a;
CDummy b[5];
CDummy * c = new CDummy;
cout << a.n << endl;
delete c;
cout << CDummy::n << endl;
}
// n=0+1=1 bir sınıf oluştu
// n=n+5=6 beş sınıf oluştu
// n=n+1=7 bir sınıf oluştu
// 7 yaz
// n=n-1 bir sınıf silindi
// 6 yaz
2#include <iostream>
namespace first
{
int var = 5;
}
namespace second
{
double var = 3.1416;
}
int main () {
{
using namespace first;
cout << var << endl;
}
{
using namespace second;
cout << var << endl;
}
}
3#include <iostream>
namespace first
{
int var = 5;
}
namespace second
{
double var = 3.1416;
}
int main () {
using namespace second;
cout << var << endl;
cout << (var*2) << endl;
}
4#include <iostream>
namespace first
{
int var = 5;
}
namespace second
{
double var = 3.1416;
}
int main () {
cout << first::var << endl;
cout << second::var << endl;
}
11. BÖLÜM
Dayançlar (&) ve Kopya yapıcı işlevler
Dayançlar (references) sabit göstergeçlere benzerler ve derleyici tarafından
otomatik olarak dayançlarından arındırılırlar, yani bellekteki yerleri tespit
edilir. Dayançlar Pascal dilinde de bulunmasına rağmen, C++ dilindeki
özellikleri bakımından, Algol dilinden alınmışlardır. Bunlar C++ dilinde,
işleç bindirim yazımının esaslarını oluştururlar (bir sonraki bölüme
bakınız). Ayrıca işlevlere giren ve çıkan değişkenlerin denetimini de
sağlarlar.
Bu bölümde kısaca C ve C++ dillerindeki göstergeçler arasındaki farkları
göreceğiz, daha sonra dayançları tanıtacağız. Ama bölümün büyük
kısmında, C++ programlamaya yeni başlayanlara çok şaşırtıcı gelen, özel
bir yapıcı işlev olan kopya yapıcı işlevleri inceleyeceğiz. Bu özel yapıcı
işlev (dayançlar gerektirir) aynı tipin var olan bir nesnesinden yeni bir
nesne oluşturur. Kopya yapıcı işlevleri kullanan derleyiciler, işlevlere giren
ve çıkan nesnelerin değerle girmesini ve geri dönmesini sağlarlar.
Sonuç olarak C++ dilindeki, pek açık olmayan üyeye-göstergeç konusu
aydınlanmış olur.
C++ dilinde göstergeçler
C ve C++ dillerindeki göstergeçler arasındaki en önemli fark; C++ dilinin
çok daha güçlü bir şekilde tip denetlemesine sahip olmasıdır. Bu en fazla
void* tiplemesinde ortaya çıkar. C dili, bir tipin göstergecinin diğer tipe
atanmasına izin vermez, ama bunu void* kullanımı ile başarabilirsiniz.
Böylece;
bird* b ;
rock* r ;
void* v ;
v=r ;
b=v ;
C dilinde bulunan bu özellikten dolayı, bir tip başka bir tipmiş gibi işleme
tabi tutulabilir. Ve bu da C dilinde, tip denetimi açısından büyük bir boşluk
yaratır. (Bütün krakerlerin, hakedicilerin ve limonerlerin kulakları
çınlasın). C++ dilinde buna izin verilmez. C++ dilinde bir tip başka bir tip
yerine işlemlemeye kalkılırsa, derleyici hata iletisi verir. Eğer böyle bir şey
gerçeklenmek, yani bir tip başka bir tip olarak işlemlenmek istenirse, o
zaman bu açık şekilde derleyiciye ve okura belirtilmelidir. Tip değişimine
C++ dilinde döküm (casting) denir. (3. bölümde C++ dilinde gelişmiş
"açık"döküm yazım biçimleri incelenmişti.)
C++ dayançları (&)
Bir dayanç (=reference &) sabit göstergece benzer, ve derleyici tarafından
otomatik olarak dayancından ayrılıp adresi tespit edilir. Genellikle işlev
değişken listeleri ve işlev geri dönüş değerleri için kullanılır. Ayrıca
istenirse serbest dayanç olarakta kullanılabilirler. Örnek;
//B11:FreeStandingReferences.cpp
#include <iostream>
using namespace std ;
//normal serbest dayanç
int y ;
int& r=y ;
//bir dayanç oluşturulduğunda mutlaka canlı bir nesnenin
//ilk değer atamasına karşılık olmalıdır
//bu şekilde yazabilirsiniz
const int& q=12 ; //
//dayançlar başkalarının bellek yerine bağlanabilir
int x=0 ; //
int& a=x ; //
int main( ){
cout<<"x= "<<x<<", a= "<<a <<endl ;
a++ ;
cout<<"x= "<<x<<", a= "<<a<<endl ;
}///:~
(1)
(2)
(3)
Satır (1) de derleyici bir miktar bellek yerini ayırır, 12 sayısını ilk değer
ataması olarak kullanır ve dayancı da bu bellek yerine bağlar. Buradaki
temel nokta; dayançlar mutlaka başka başka bellek alan parçalarına
bağlanmak zorundadır. Bir dayança eriştiğinizde, aslında onun kendi
bellek alanına erişiyorsunuz demektir.
Bu yüzden (2). ve (3). satırları yazınca, daha sonra a değerini bir
arttırdığınız da aslında x değerini arttırmış olursunuz. Bunu main( )
işlevinin içinde açıkça görürüsünüz. Yine dayançlar hakkında bir fikir
edinmenin en kolay yolu, onları hayali göstergeçler olarak düşünmektir.
Bu "göstergecin" bir üstünlüğü; onların ilk değer atamaları hakkında hiçbir
kuşku duyulmamasıdır (derleyici bunu zorla yapar), ve dayancından
arındırılması işlemi de yine derleyici tarafından yerine getirilir.
Dayançları kullanırken belli kurallar bulunmaktadır;
1- Bir dayanç oluşturulduğunda mutlaka ilk değer atama işlemi yapılmak
zorundadır. (Göstergeçlerin ilk değer atama işlemi ise gereken herhangi bir
anda yapılabilir)
2- Bir dayanç bir nesneye bir kez ilk değer olarak atandığında, artık bir
daha başka bir nesneye dayanç olamaz. (Göstergeçler ise başka bir nesneye
istendiği ve gerektiği zaman gösterge olabilirler)
3- Hiçbir zaman dayançlar BOŞ (NULL) olamaz. Mutlaka ve her zaman
bir dayanç geçerli bir bellek alanına bağlanmak zorundadır.
İşlevlerdeki dayançlar
Dayançların en yaygın görüldüğü ve kullanıldığı yerler işlev değişkenleri
ve geri dönüş değerleridir. İşlev değişkeni olarak dayanç kullanılırsa, işlev
içindeki dayançta yapılan değişiklik, işlev dışındaki değişkenlerde
değişikliklere neden olur. Aynı işlemleri göstergeçlerle de yerine getirmek
mümkündür, fakat dayançlarla yazım daha anlaşılabilir ve açıktır.
(Dilerseniz dayançları aslında bir yazım kolaylığı olarak düşünebilirsiniz).
Bir işlevden bir dayancı geri döndürürseniz, işlevden bir göstergeçi
döndürmüş gibi, dikkatli olmak zorunluluğu vardır. İşlev geri dönüşünde,
dayanç ait olduğundan başkasına gitmemelidir, aksi taktirde kendisini
bilinmeyen bellek alanında bulur.
İşte bir örnek ;
//:B11:Reference.cpp
//Basit C++ dayançları
int* f(int* x){
(*x)++ ;
return x ; //güvenli, x bu görüntünün dışında
}
int& g(int& x){
x++ ; //f( ) işlevinin aynı etkiyi oluşturur
return x ; //güvenli, x bu görüntünün dışında
}
int& h( ){
int q ;
//!return q ; //hata
static int x ;
return x ;
//güvenli, x bu görüntünün dışında yaşar
}
int main( ){
int a=0 ;
f(&a) ; //berbat fakat belli
g(a) ; //temiz
}///:~
İşlevlerden f( ) in çağrımı hem uygun hemde açık değil ama buna rağmen
aktarılan adresin kesinliği yeterli. g( ) işlevinin çağrımında ise adres
aktarımı, dayanç yolu ile yapılır ama bu görülemez.
sabit dayançlar
Reference.cpp de bulunan dayanç değişkeni, sadece değişken sabit
olmayan nesne olduğu zaman çalışır. Eğer nesne sabit olursa, g( ) işlevi
değişkeni kabul etmez. Bu aslında iyi bir şeydir, zira işlev dışarıdaki
değişkeni değiştirmektedir.Eğer bir işlevin nesnenin sabitliğine uyduğunu
bilirseniz, değişkeni sabit dayanç yapmak işlevin tüm koşullarda
kullanılmasını sağlar. Bunun anlamı, yerleşik tipler için; işlev değişkeni
değiştiremez, kullanıcı tipler için ise işlev sadece sabit (const) üye işlevleri
çağırır, ve herhangi public veri üyesini değiştiremez.
Sabit (const) dayançların işlev değişkenlerinde kullanımı özellikle
önemlidir zira işlev geçici bir nesne almaktadır. Bu ikinci bir işlevin geri
dönüş değeri olarak oluşturulur veya bu işlevin kullanımından açıkça
oluşturulur. Geçici nesneler her zaman sabittir (const) ve hatta sabit
(const) dayanç kullanmazsanız, derleyici o değişkeni kabul etmez. Basit
bir örnek verelim ;
//:B11:ConstReferenceArguments.cpp
//Dayançları sabit olarak aktarmak
void f(int&){}
void g(const int&){}
int main( ){
//!f(1) ; //hata
g(1) ;
}///:~
f(1) işlevinin çağrımı derleme sırasında hata oluşturur zira derleyici önce
dayanç oluşturmak zorunda. Bu işlemi de önce int değişkeni için bellekte
yer ayırarak yapar, sonra ilk değer atamasını 1 ile gerçekler ve daha sonra
oranın adresini dayanca bağlayarak işlemi tamamlar. Saklama yeri mutlaka
sabit(const) olmak zorundadır zira değişmesi onu anlamsız kılar, bir daha
elde edemezsiniz. Bütün geçici nesneleri gözönüne aldığınızda aynı şeyi
aklınızdan çıkarmayın; geçici nesnelere erişilemez. Derleyici için değerli
olan bu yaklaşımın bize söylediği; böyle bir veri değiştirildiğinde
kaybolmuş bilgi demektir.
Göstergeç dayançları
C dilinde bir göstergecin içeriğini, işaret ettiğinden başka bir değerle
değiştirmek için, işlev bildirimi aşağıdaki gibi yapılır;
void f(int**) ;
Ve aktarım sırasında göstergeçin adresini almak gerekir,
int i=48 ;
int* ip=&i ;
f(&ip) ;
C++ da dayançlar ile yazım daha belirgin ve açıktır. İşlev değişkeni
göstergece dayanç olur, ve artık göstergecin adresini almak zorunluluğu
kalmaz.
//:B11:ReferenceToPointer.cpp
include <iostream>
using namespace std ;
void increment(int*& i){i++;}
int main( ){
int* i=0 ;
cout<<"i= "<<i<<endl ;
increment(i) ;
cout<<"i= "<<i<<endl ;
}///:~
Programı çalıştırdığınızda göstergecin değerinin arttığını göreceksiniz yani
göstergecin işaretlediği yerdeki değer artmayacak.
İşlevlerde değişken aktarım yolları
Normal olarak bir değişken bir işleve aktarıldığı zaman bunun sabit(const)
dayançla yapılması lazımdır. Bu adettendir. Başlangıçta sadece verimlilikle
ilişkin görülse de (normalde programınızı tasarlarken ve birleştirirken
verimlilik ayarlaması ile uğraşmak istemezsiniz.) aslında daha fazlası
vardır, bu bölümün geri kalan kısmında anlatılan, kopya-yapıcı işlev
nesneyi değerle aktarmak için kullanılır, ve bu da her zaman yapılamaz.
Böyle bir alışkanlık için verimlilik kazançları önemli olabilir; değişkeni
değerle aktarmak için yapıcı ve yıkıcı işlev çağrımları gerekir, ama eğer
değişkeni hep aynı ise o zaman const dayançla aktarımın sadece yığıta
gönderilecek olan adrese gereksinimi vardır.
Aslında tercih edilmeyecek tek durum olan sanal olarak adres aktarımının,
nesneyi değerle aktarırken yapacağınız hasar yüzünden tek güvenilir
yaklaşım olmasıdır.(dışarıdaki nesneyi değiştirmekten ziyade, genellikle
işlevi çağıranın beklemediği bir durumdur). Bu bir sonraki bölümün
konusudur.
Kopya-yapıcı işlev
Buraya kadar anlatılanlardan C++ dilinde kullanılan dayançları
(reference) anlamış olmalısınız. Şimdi artık C++ dilinde bulunan, daha
kafa karıştırıcı bir uygulama olan kopya-yapıcı işlevleri öğrenelim. Kopyayapıcı işlevler genellikle X(X&) (X dayanç X i) olarak gösterilir. Bu
yapıcı işlev, kullanıcı tanımlı tiplerin işlev çağrımları sırasında değerle
aktarım ve geri dönüşlerini denetleyen ana unsurdur. O kadar önemlidir ki,
eğer kendiniz bir tane kopya-yapıcı işlev oluşturmazsanız ilerde
göreceğiniz gibi, derleyici otomatik olarak kopya-yapıcı oluşturur.
Değerle aktarım ve geri dönüş
Kopya-yapıcı işlev ihtiyacını anlamak için, C deki işlev çağrımlarında
değişkenlerin değerle aktarım ve geri dönüşlerini gözönüne alalım. Bir
işlevi bildirelim ve işlev çağrımı yapalım;
int f(int x,char c) ;
int g=f(a,b) ;
Derleyici değişkenleri nasıl aktaracağını ve geri döndüreceğini nasıl
bilecek?. Evet bilir.. Tiplerin kapsadıkları alan o kadar küçük olmalıdır ki
-char, int, double, float ve alt biçimleri- bunlar derleyiciye yerleşik
bulunabilmelidir. Derleyicinizin ürettiği assembly kodları nasıl ürettiğini
hesaplayabilir, ve f( ) işlev çağrımında üretilen deyimleri belirlerseniz,
aşağıdaki eşdeğerini elde edersiniz;
push b
push a
call f( )
add sp 4
mov g, register a
Bu kodu genelleştirmek için biraz düzeltme yapılabilir; a ve b için yazılan
açıklamalar global (bu dururmda _a ve _b olurlar) veya yörel (derleyici
bunları yığıtta dizinler) olmalarına göre farklı olurlar. Bu aynı zamana da g
açıklaması içinde geçerlidir. f( ) işlevine çağırmanın görünümü isim
düzenleme planına bağlıdır, "register a" ise assemblerde CPU
registerlerine verilen isimlerden gelmektedir. Kodun arkasında yatan
mantık bir şekilde aynı kalmıştır.
C ve C++ dillerinde değişkenler önce sağdan sola alınarak yığıta
yerleştirilirler, daha sonra işlev çağrımı yapılır. Çağrı kodu, yığıtta bulunan
değişkenlerin silinmesinden sorumludur (add sp 4 bunu yapar). Fakat
burada dikkat edilecek husus; değerle değişken aktarımı yapılırken,
derleyici basit bir şekilde kopyaları yığıta yerleştirir. Derleyici
büyüklüklerini tam olarak bildiğinden, değişken kopyalarını doğru şekilde
yerleştirebilir.
f( ) işlevinin geri dönüş değeri bir register'e yerleştirilir. Yine derleyici geri
dönüş değeri tipi ile ilgili herşeyi bilir zira tipi, dilin içine önceden
yerleştirilmiş bulunmaktadır. Bundan dolayı derleyici geri dönüş değerini
bir register'e yerleştirir. C dilinde temel veri tipleri ile yapılan bir değerin
bitlerini kopyalama işlemi nesne kopyalama ile eşdeğerdir.
Büyük nesnelerin aktarım ve geri dönüşü
Şimdi artık kullanıcı tanımlı tipleri ele alalım. Bir sınıf oluşturulursa ve o
sınıfın bir nesnesi değerle aktarılmak istenirse, derleyici ne yapacağını
nasıl bilecek?. Bu tip önceden derleyiciye yerleştirilmiş değil ; zira
başkaları tarafından oluşturulmuç bir sınıf.
Bunu incelemek için, register'e geri dönüşü çok büyük, basit bir yapı ile
başlayalım.
//:B11:PassingBigStructures.cpp
struct Big{
char Buf[100] ;
int i ;
long d ;
}B, B2 ;
Big bigfun(Big b){
b.i=100 ; //değişkene bazı işlemler yap
return b ;
}
int main( ){
B2=bigfun(B) ;
}///:~
Assembly kodları çözümlemek bu defa biraz karmaşıktır, zira
derleyicilerin büyük çoğunluğu işlevsel satıriçiler koymak yerine
"yardımcı" işlevler kullanırlar. main( ) işlevinde, bigfun( ) işlevinin
çağrısında tahmin edeceğiniz gibi B nin içeriğini olduğu gibi yığıta
yerleştirerek başlar. (Burada bazı derleyiciler Big in adresini ve
büyüklüğünü register'lere yükler, daha sonra yardımcı işlevi çağırarak
Big'i yığıta gönderir.)
Önceki kod parçasında değişkenleri yığıta göndermek işlev çağrısı
yapmadan evvelki gereken herşeydi. PassingBigStructures.cpp de
bunlara ek olarak bir başka etkinlikte var; B2'nin adresi çağrıdan önce
yığıta gönderilmişti, hatta o sırada açık bir biçimde değişken değildi. Bu
anda neler olduğunu kavrayabilmek için işlev çağrıları esnasında
derleyicilerin çalışma davranışlarını iyice bilmek gerekir.
İşlev-çağrı yığıt çerçevesi
İşlev çağrısı için derleyici kod üretirken, önce bütün değişkenleri yığıta
gönderir, daha sonra çağrı işlemini yapar. İşlev içerisinde, işlevin yörel
değişkenlerine yer açabilmek amacı ile, yığıt göstergeçini daha aşağı
çekmek için kod üretilir.(burada aşağı kelimesi görece bir kelimedir, zira
sizin makineniz yığıta gönderme esnasında yığıt göstergecini aşağı ya da
yukarı yapabilir, yani aşağı ya da yukarılık makineye bağlıdır.). Fakat
Assembly dili CALL işlemi sırasında, CPU işlev çağrısının geldiği yerin
adresini program koduna yerleştirir, böylece Assembly dili RETURN
işlemin de çağrı noktasına geri dönüş sırasında bu adres kullanılır. Bu
adres doğal olarak çok önemlidir, zira bu adres olmaksızın program
tamamen kaybolur. CALL işleminden sonra yığıt çerçevesi ve, işlevin
yörel değişkenlerinin bellek yerleşimi aşağıdaki gibi olur.
İşlev değişkenleri
Geri dönüş adresi
Yörel değişkenler
İşlevin geri kalanları için üretilen kod belleğin aynı şekilde yerleşimini
bekler. Bu sayede işlev değişkenleri ve yörel değişkenler geri dönüş
adresine dokunulmadan dikkatli bir şekilde alınırlar. İşlev çağrımı
sürecinde, işlev tarafından kullanılan gerekli herşeyin bulunduğu belleğin
bu bölümü, işlev çerçevesi olarak isimlendirilir.
Geri dönüş değerlerinin yığıtta bulunmasının ne kadar anlamlı olduğu açık
olarak bellidir. Derleyici onu basit bir şekilde yığıta gönderir, işlev, geri
dönüş değerinin nereden başladığını belirleyen kayma miktarını, geri verir.
Yeniden giriş
C ve C++ dilleri, işlevlerde kesimleri(interrupts) destekledikleri için, bu
konularda sorun yaşarlar. Aslında kesimler dillerin içine yeniden
girilebildiği anlardır. Bu diller ayrıca özyinelemeli işlev çağrılarını
(recursive function call ) da desteklemektedirler. Kesimler, program işlem
yaparken herhangi bir anda programa zarar vermeksizin programa girerler.
Kesim servis rutinini (İnterrupt service routine= ISR) yazan kişi, ISR de
kullanılan bütün registerlerin saklama ve düzenleme işlerinden
sorumludur. ISR yığıttan daha fazla bellek ihtiyacı duyarsa bu mutlaka
güvenli bir şekilde yapılmalıdır. (ISR, değişkenleri olmayan, geri dönüş
değeri void olan, CPU durumunu saklayan ve düzenleyen, normal bir işlev
olarak düşünülebilir. Bir ISR işlev çağrısı, normal program çağrıları yerine
donanım olaylarından biri tarafından tetiklenerek yapılır.).
Şimdi normal bir işlev, yığıtta bulunan değerlerini geri döndürmeye
çalışırsa ne olur onu düşünelim. Geri dönüş adresinin üzerinde bulunan
yığıt yerlerine dokunamazsınız, bu yüzden işlev, geri dönüş adresinden
sonraki yerlere değerleri göndermek zorundadır. Fakat Assembly dili
RETURN işlemlendiği zaman, yığıt göstergeci mutlaka geri dönüş adresini
işaret etmelidir. (ya da makineye bağlı olarak hemen aşağısı.), böylece
RETURN den hemen önce, işlev yığıt göstergecini mutlaka yukarı hareket
ettirir ve bu sayede bütün yörel değişkenlerin silinmesi sağlanır. Geri
dönüş değerlerini yığıtın geri dönüş adresinin aşağısındaki yerlere
göndermeye çalışırsanız, o anda, kesimler olduğu sürece korumasız
sakıncalı durumda kalırsınız. ISR, yığıt göstergecini, geri dönüş adresini,
yörel değişkenleri ve geri dönüş değerinizi, üzerine yazmak için, aşağı
hareket ettirir.
Bu sorunu çözmek için çağırıcı, işlevi çağırmadan önce geri dönüş
değerlerinin yığıtın ek alanlarına yerleşiminden sorumlu kılınır. C dili buna
uygun tasarımlanmamışı, fakat C++ buna mutlaka uyumlu olmalıdır.
Kısaca göreceğiniz gibi C++ derleyicileri çok etkin ve verimli şema
kullanırlar.
Bir sonraki fikriniz, değerin global veri alanına geri dönüşüdür. Fakat bu
bir şekilde çalışmaz. Yeniden girişin anlamı; herhangi bir işlevin başka bir
işlev için kesim olabilmesidir. İçinde bulunduğunuz işlevi içinize almakta
olabilir. Bundan dolayı geri dönüş değerini global alana yerleştirirseniz,
aynı işleve geri döndürmeniz gerekir, ki bu da geri dönüş değerini üzerine
yazar. Aynı mantık özyinelemeli işlevler içinde geçerlidir.
Geri dönüş değerleri için tek güvenli bellek alanları register'lerdir. O
zaman da saklanacak olan geri dönüş değerleri, register bellek alanlarından
büyük olduğu zaman, sorunlarla karşılaşırız. Bunun yanıtı; geri dönüş
değerinin varış adresini bir işlev değişkeni olarak yığıta göndermek, ve
geri dönüş bilgisini işlevin doğrudan varışa kopyalamasına izin vermektir.
Bu yol bütün sorunları çözmez, fakat oldukça yararlıdır. Ayrıca bu nedenle
PassingBigStructure.cpp de main( ) de bigfun( ) a çağrı yapılmadan
önce, derleyici B2 nin adresini yığıta gönderir. bigfun( ) için Assembly
çıktısına bakarsanız, onun saklı değişken olarak beklendiğini
görebilirsiniz, ve işlev içinde varışa kopyalamayı gerçeklenir.
İlk değer atamaya karşılık, bit kopyalama
Şimdiye kadar her şey iyi gitti. Büyük basit yapıları aktarmak ve geri
döndürmek için çalışabilir süreçler bulunmaktadır.Fakat farkedeceğiniz
gibi, sahip olunan herşey, bitlerin bir yerden başka bir yere
kopyalanmasından başka bir şey olmayıp, C nin değişkenleri gördüğü gibi
mutlaka hassas basit ilkel bir şekilde çalışır. C++nesnelerinde ise, bitlerin
yamalanmasından çok daha fazlası bulunur; anlamları vardır. Bu anlam
bitlerin kopyalarının olmasına tam karşılık gelmez.
Basit bir örnek alalım; bir sınıf kendisinin herhangi bir anda kaç tane
nesnesi olduğunu bilsin. Bunu daha önce 10. bölümde gördüğünüz gibi
static veri üyesi ekleyerek yapabilirsiniz:
//:B11:HowMany.cpp
//nesnelerini sayan sınıf örneği
#include <fstream>
#include <string>
using namespace std ;
ofstream out("HowMany.out") ;
class HowMany{
static int objectCount ;
public:
HowMany( ){objectCount++;}
static void print(const string& msg=" " ){
if(msg.size( )!=0)out<<msg<<": " ;
out<<"objectCount= "
<<objectCount<<endl ;
}
~HowMany( ){
objectCount-- ;
print("~HowMany( )") ;
}
};
int HowMany::objectCount=0 ;
//değerle aktar ve geri döndür
HowMany f(HowMany x){
x.print("f( ) islevindeki x degiskeni") ;
return x ;
}
int main( ){
HowMany h ;
HowMany::print("h nin insasindan sonra") ;
HowMany h2=f(h) ;
HowMany::print("f( ) işlevi çagrisindan sonra") ;
}///:~
HowMany sınıfında static int objectCount ve static üye işlev olan print(
) bulunmaktadır. Anılan işlev objectCount değerini iletilerle rapor halinde
verir. Yapıcı işlev her yeni nesne oluşturulduğunda değerini bir arttırır ve
yıkıcı işlevde değerini bir azaltır.
Çıktı ise bir şekilde beklendiği gibi olmaz:
h nin insasindan sonra : objectCount=1
f( ) islevindeki x degiskeni :objectCount=1
~HowMany( ) : objectCount=0
f( ) islevi cagrisindan sonra : objectCount=0
~HowMany( ) : objectCount=-1
~HowMany( ) : objectCount=-2
h oluşturulduktan sonra nesne sayısı 1(bir) dir, yani tamam. Fakat f( )
çağrısından sonra nesne sayısının iki olmasını beklersiniz, zira h2 görüntü
içindedir. Ama sayısı onun yerine 0 (sıfır) olur, bu da bazı şeylerin yanlış
gittiğini gösterir. Bunu, ayrıca son kısımda yeralan yıkıcı işlevlerin
neticeleri de onaylar, zira yıkıcı işlev neticeleri negatif çıkar, bu sonuçlar
hiçbir zaman olmamalıdır.
f( ) işlevine giriş noktasına bakalım, değişken değerle aktarıldıktan sonra
herşey olur. Bunun anlamı; esas nesne olan h işlev çerçevesinin dışındadır,
bunun yanında işlev çerçevesinin içinde ek bir nesne, yani değerle
aktarılan bir kopya bulunur. Değişken C dilinin en ilkel uygulamalarından
olan bit kopyalama ile aktarılır. C++, HowMany sınıfının bütünlüğünü
sağlamak için doğru bir ilk değer ataması gerektirir, böylece bit kopyalama
işlemi istenen etkiyi yaratmaz.
f( ) işlevinin çağrısının sonunda yörel nesne görüntü dışına çıktığı zaman,
yıkıcı işlev çağrılır, objectCount sayısı 1 (bir) indirilir bu sayedeişlev
dışında objectCount değeri 0 (sıfır) olur. h2 nin oluşumu bit kopyalama
yolu ile sağlanır, böylece yapıcı işlev oraya bir şekilde çağrılmaz, h ve h2
görüntü dışına çıktğı zaman, onların yıkıcı işlevleri objectCount ın negatif
değerine neden olur.
Kopya oluşumu
Derleyicinin varolan bir nesneden başka bir nesne oluşturmak için yaptığı
kabuller sorun oluşturur. Bir nesneyi değerle aktarırken , yeni bir nesne
oluşturulur, aktarılan nesne işlev çerçevesinin içindedir, önceden varolan
bir nesneden, esas nesne işlev çerçevesinin dışındadır. Bu genellikle
işlevden bir nesne dönerken doğrudur. Açıklama da
HowMany h2=f(h) ;
h2, önceden oluşturulmamış bir nesnedir, f( ) işlevinin geri dönüş
değerinden oluşturulur, böylece varolan bir nesneden yeni bir nesne
oluşturulur.
Bu konuda derleyicinin varsaydığı kabul, sizin bu oluşumu bit kopyalama
kullanarak yapacağınızdır. Aslında bir çok uygulamada bu usul başarılı
sonuçlar verir. Fakat HowMany de bu çalışmaz, zira ilk değer atama
işleminin anlamı, basit bit kopyalamanın ötesine geçer. Bu konuda başka
bir yaygın örnekte sınıfta göstergeçlerin bulunduğu durumdur.
Neyi gösteriyorlar?., Onları kopyalamanız lazımmı?. Onların belleğin bir
parçasına bağlanması lazımmı?
Allahtan ki, bu sürecin arasına girebilir, ve derleyicinin bit kopya
yapmasını engelleyebilirsiniz. Bunu, derleyici varolan bir nesneden yeni
bir nesne oluşturacağı zaman, ihtiyacı olduğunda, siz kullanılacak olan
işlevi tanımlayarak yapabilirsiniz. Mantık olarak yeterlidir, zira yeni bir
nesne oluşturuyorsunuz, bu nedenle bu işlev bir yapıcı işlevdir, ve ayrıca
yeterlidir, zira kendisinden nesne oluşturulan yapıcı işlev tek bir
değişkenle bunu yapmalıdır. Fakat oluşturulan bu nesne yapıcı işleve
değerle aktarılamaz, çünkü tanımlamaya çalışılan işlev değer aktararak
kullanılıyor, yazım biçimi olarak göstergeç aktarmaya uygun değildir,
hepsinden öteye varolan bir nesneden yeni bir nesne oluşturuluyor. Burada
dayançlar kurtarıcı olarak ortaya çıkıyor, o nedenle bu durumlarda kaynak
nesnenin dayancı kullanılır. Bu işleve de kopya yapıcı işlev denilir ve X
(X&) şeklinde gösterilir, ve bu gösterim X sınıfınındır.
Bir kopya yapıcı işlev oluşturduysanız, derleyici varolan bir nesneden yeni
bir nesne oluştururken, bit kopyalama yolunu kullanmaz . bu işler için her
zaman kopya yapıcı işlevi çağırır. Fakat eğer bir kopya yapıcı işlev
oluşturmazsanız, derleyiciniz belki bazı anlamlı şeyler yapabilir, fakat
sürecin tamamını denetleyemezsiniz, o nedenle kopya yapıcı işlevi
gerektiğinde oluşturmalısınız.
Şimdi HowMany.cpp bulunan hatayı sabitleyelim;
//:B11:HowMany2.cpp
//Kopya yapıcı işlev
#include <fstream>
#include <string>
using namespace std ;
ofstream out("HowMany2.out") ,
class HowMany2{
string name ; //nesne belirteci
static int objectCount ;
public:
HowMany2(const string& id=" "):name(id){
++objectCount ;
print("HowMany2( )") ;
}
//kopya yapıcı işlev
HowMany2(const HowMany2& h):name(h.name){
name+=" copy" ;
++objectCount ;
print("HowMany2(const HowMany2&)") ;
}
void print(const string& msg=" ")const{
if(msg.size( )!=0)
out<<msg<<endl ;
out<<'\t'<<name<<":"
<<"objectCount="
<<objectCount<<endl ;
}
};
int HowMany2::objectCount=0 ;
//değerle aktar ve geri döndür
HowMany2 f(HowMany2 x){
x.print("f( ) icindeki x degiskeni") ;
out<<"f( ) den geri donus"<<endl ;
return x ;
}
int main( ){
HowMany2 h("h") ;
out<<"f( ) e giris"<<endl ;
HowMany2 h2=f(h) ;
h2.print("h2,f( ) e cagrildiktan sonra ") ;
out<<"f( ) cagir, geri donus değeri yok")<<endl ;
f(h) ;
out<<"f( ) e cagirdiktan sonra")<<endl ;
}///:~
Neler olduğu hususunda daha iyi fikir elde etmek için bir sürü yeni kıvrak
hareket olduğunu görmek gerekir. Önce, stringname, nesnenin bilgilerini
yazdırırken nesne belirteci olarak davranır. Yapıcı işlevde ise, string
yapıcı işlevini kullanarak name'e kopyalanan bir belirteç string'i(genellikle
nesne adı) yerleştirebilirsiniz. =" " varsayılanı boş bir string oluşturur.
Yapıcı işlev objectCount değerini önce 1(bir) arttırır, yıkıcı işlev bu
değeri 1 (bir ) azaltır.
Daha sonra ise kopya yapıcı işlev yani, HowMany2(HowMany2&)
gelir.Kopya yapıcı işlev, varolan bir nesneden yeni bir nesne oluşturur,
yani varolan nesnenin adı name'e kopyalanır, "copy" kelimesini takip
ettiğinizde nereden kaynaklandığını görebilirsiniz. Yakından dikkatlice
bakarsanız, yapıcı işlev ilk değer atama listesinde, name(h.name) çağrısını
görürsünüz, aslında bu string kopya yapıcı çağrısıdır.
Kopya yapıcı işlev içinde, aynı normal yapıcı işlevlerde olduğu gibi, nesne
sayısı 1 (bir) arttırırlır. Bunun anlamı; nesne sayısını değerle aktarırken ve
geri döndürürken doğru nesne sayısı elde edilir.
print( ) işlevi iletiyi yazdırabilmek için değişime uğrar, nesne belirteci ve
nesne sayısı ile. Mutlaka belli bir nesnenin name verisine ulaşabilmelidir,
böylece artık bundan sonra static üye işlev olamaz.
main( ) işlevinin içinde f( ) işlevine ikinci bir defa çağrı görürsünüz. Bu
çağrıda, geri dönüş değerini gözardı eden, yaygın C dili yaklaşımı
kullanılmıştır. Fakat şu anda geri dönüş değerinin nasıl elde edildiği
bilinmektedir. (yani, geri dönüş süreci, işlev içindeki kodlar tarafından
yerine getirilmektedir, başka deyişle; adresi gizli değişken olarak aktarılan
varış yerine sonuç konulmaktadır.). Geri dönüş değeri gözardı edilince, ne
olduğunu merak edeceğinizi düşünüyoruz. Programın çıktısı bize
aydınlatıcı bazı veriler iletir.
Çıktıyı göstermeden evvel, şimdi herhangi bir dosyaya iostream'leri
kullanarak satır numaralarını ekleyen, basit bir program örneği görelim;
//:B11:Linenum.cpp
//{T} Linenum.cpp
//satir numaralarini ekle
#include ".../require.h"
#include <fstream>
#include <string>
#include <vector>
#include <iostream>
#include <cmath>
using namespace std ;
int main(int argc,char* argv[]){
requireArgs(argc,1,"Usage: linenum file\n"
"Satir nolarini dosyaya ekle")
ifstream in(argv[1]) ;
assure(in, argv[1]) ;
string line ;
vector<string> lines ;
while(getline(in,line)) //butun dosyayi oku
lines.push_back(line) ;
if(lines.size( )==0) return 0 ;
int num=0 ;
//dosyadaki satir sayisi genisligi belirler
const int width=int log10(lines.(size( )))+1 ;
for(int i=0;i<lines.size( );i++){
cout.setf(ios::right,ios::adjustfield) ;
cout.width(width) ;
cout<<++num<<") "<<lines[i]<<endl ;
}
}///:~
Bütün dosya vector<string> e okunur, aynı kodları kitabımızın önceki
bölümlerinde kullanmıştık. Satır numaraları yazdırıldığı zaman, satırların
biribiri ile uyumlu olmasını isteriz, bu da satır numaralarının bir dosyada
tutulmasını gerektirir böylece satır numaraları için izin verilen genişlik
birbiri ile tutarlı olur. Satır sayısını vector::size( ) ile belirlemek çok
kolaydır. Fakat aslında biz neyi bilmek istiyoruz? satır sayısının 10 dan,
100 den ya da1000 den fazla olup olmadığını mı?. Dosyada ki satır
sayısının 10 tabanına göre logaritmasını alıp, sonra gereken budamayı
yapıp int hale getirdiğimiz de, satır sayısının maksimum genişliği elde
edilir.
for döngüsünün içinde tuhaf bazı çağrılar dikkatinizi çekmiştir, setf( ) ve
width( ) gibi çağrılar denetim amaçlı olup, ostream çağrılarıdır. Burada
çıktının genişliğini ve doğruluğunu onaylamak için kullanılmışlardır.
Çıkan her satırdan sonra mutlaka çağrılmak zorundadırlar, ve for
döngüsünde bulunma nedenleri budur. Kitabın ilerleyen bölümlerinde
iostream'ler hakkında daha ayrıntılı bilgiler verilecektir.
Linenum.cpp, HowMany2.cpp ye uygulandığı zaman elde edilen netice
aşağıdaki gibi olur.
1) HowMany2( )
2) h:objectCount=1
3) Entering f( )
4) HowMany2(const HowMany2&)
5) h copy:objectCount=2
6) x argument inside f( )
7) h copy:objectCount=2
8) Returning from f( )
9) HowMany2(const HowMany2&)
10) h copy copy:objectCount=3
11) ~HowMany2( )
12) h copy:objectCount=2
13) h2 after call to f( )
14) h copy copy:objectCount=2
15) Call f( ), no return value
16) HowMany2(const HowMany2&)
17) h copy:objectCount=3
18) x argument inside f( )
19) h copy:objectCount=3
20)Returning from f( )
21) HowMany2(const HowMany2&)
22) h copy copy:objectCount=4
23) ~HowMany2( )
24) h copy:objectCount=3
25) ~HowMany2( )
26) h copy copy:objectCount=2
27) After call to f( )
28) ~HowMany2( )
29) h copy copy:objectCount=1
30) ~HowMany2( )
31) h:objectCount=0
Beklenildiği gibi önce normal yapıcı işlev h için gelir, nesne sayısını 1
(bir) arttırır. Fakat daha sonra f( ) işlevine girilirken, kopya yapıcı işlev
derleyici tarafından çağrılarak değerle aktarım gerçeklenir. Yeni bir nesne,
h nin kopyası olarak (h kopyası adı verilir) f( ) işlev çerçevesinde
oluşturulur, bu sefer kopya yapıcı izni ile nesne sayısı 2 (iki) olur.
Sekizinci satır f( ) işlevinden geri dönüşün başlangıcıdır. Fakat yörel
değişken "h kopya" silinmeden önce (işlev sonunda görünümden çıkar)
geri dönüş değerine h2 olarak mutlaka kopyalanmalıdır. Önceden yapıcı
işlev tarafından geliştirilmemiş olan h2 nesnesi varolan bir nesneden (f( )
işlevindeki yörel değişkendir) oluşturulur, böylece doğal olarak dokuzunca
satırda da kopya yapıcı işlev kullanılır. Şimdi adı h2 belirteci için "h kopya
kopya" oldu çünkü o f( ) işlevindeki yörel nesneden kopyalanan bir
kopyadır. Nesne geri döndükten sonra fakat işlev tamamlanmadan önce,
nesne sayısı geçici olarak 3 (üç) olur, fakat daha sonra yörel nesne "h
kopya" ortadan kalkar. f( ) işlevine çağrı 13. (onüçüncü) satırda
tamamlanmasından sonra, sadece iki tane nesne; h ve h2 bulunur, h2 nin
de "h kopya kopya" ile sonlandığını görebilirsiniz.
Geçici nesneler
15.(onbeşinci) satır f(h) işlevini çağrı ile başlar, bu kez geri dönüş değeri
gözardı edilir. 16.(onaltıncı) satırda ise kopya yapıcı işlev, değişken içeri
aktarılmadan hemen önce çağrılır. Ve ayrıca daha önce olduğu gibi, satır
21 (yirmibir) kopya yapıcı işlevin geri dönüş değeri için çağrıldığını
gösterir. Burada dikkat edilmesi gereken, kopya yapıcı işlev varış yeri
olarak mutlaka bir adrese (bir this göstergeci) sahip olmalıdır. Bu adres
nereden gelecek ?.
Derleyici gerektiğinde açıklamaları hesaplayacağı geçici bir nesne
oluşturur. İşte bu durumda da bir tane oluşturur, ve f( ) işlevinin gözardı
edilen geri dönüş değerinin varış adresi olarak kullanır , hatta siz onu
görmezsiniz bile. Bu geçici nesnenin ömrü, mümkün olduğunca kısadır ve
ortam yokedilmeyi bekleyen geçici nesnelerle darmadağın edilmeden
önemli görevler yerine getirip silinirler. Bazı durumlarda geçiciler derhal
başka işlevlere aktarılmak zorundadırlar, böyle durumların işlev
çağrılarından sonra olmaları gerekmez. Yörel nesne silimi için çağrılan
yıkıcı işlev çağrımı ile sonlanan işlev çağrısı süreci ile biter bitmez (satır
23 ve 24), geçici nesne ortadan kaldırılmış olur. (satır 25 ve 26)
Son olarak 26 ile 31. satırlar arasında, h yi takipeden h2 nesnesi silinir,
böylece sayılan nesne adedi doğru biçimde sıfırlanmış olur.
Varsayılan kopya yapıcı işlev
Kopya yapıcı işlevlerin değerle aktarım ve geri dönüş kıymeti nedeni ile,
derleyicinin basit yapılar için, bir tane kopya yapıcı işlevi üretmesi oldukça
önemlidir. Aynı özellik C dilinde de bulunur. Aslında bu öğrenilenlerin
hepsi temelde bir davranıştan kaynaklanır; bitkopya.
Daha karmaşık tiplerle işlem yapıldığında, C++ derleyicisi, eğer siz
oluşturmadıysanız, otomatik olarak bir kopya yapıcı işlev oluşturur.
Burada yine de bitkopya anlam taşımaz, zira gerekli doğru anlamı
uygulanmaz. Aşağıdaki örnek, derleyicinin yaptığına zekice bir yaklaşımı
göstermektedir. Varolan sınıfların nesnelerinden oluşan yeni bir sınıf
oluşturduğumuzu varsayalım. Bunu uygun biçimde, harçlama
(composition) olarak adlandıralım. Aslında bu, varolan sınıflardan yeni bir
sınıf oluşturma yollarından biridir. Şimdi, sorununu kestirme yoldan
çözmek için bu yolla yeni sınıf oluşturan, saf kullanıcı rolüne bürünelim.
Kopya yapıcı işlev hakkında bir şey bilmemeniz de, bu durumda doğaldır.
O zaman, kopya yapıcı işlev oluşturmazsınız. Örnek, yeni sınıf için kopya
yapıcı işlev oluşturulurken, derleyicinin ne yaptığını göstermektedir.
//:B11:DefaultCopyConstructor.cpp
//kopya yapıcı işlevin otomatik üretimi
#include <iostream>
#include <string>
using namespace std ;
class withCC{//kopya yapici ile
public:
//açık biçimde kopya yapici işlev belirtilmeli
WithCC( ){}
WithCC(const WithCC&){
cout<<"WithCC(WithCC&)"<<endl ;
}
};
class WoCC{//kopya yapıcı yok
string id ;
public:
WoCC(const string& ident=" "):id(ident){}
void print(const string& msg=" ")const{
if(msg.size!=0)cout<<msg<<" : " ;
cout<<id<<endl ;
}
};
class Composite{
WithCC withcc ;
WoCC wocc ;
public:
Composite( ):wocc("composite( )"){ }
void print(const string& msg= " ")const{
wocc.print(msg) ;
}
};
int main( ){
Composite c;
c.print("c nin ici") ;
cout<<"bilesim kopya yapici cagrisi"
<<endl ;
Composite c2=c ; ///kopya yapici cagrisi
c2.print(c2 nin ici") ;
}///:~
WithCC sınıfında ki kopya yapıcı işlev çağrıldığında ilginç unsurlar
ortaya çıkar. Composite sınıfında WithCC sınıfının bir nesnesi varsayılan
yapıcı işlev ile oluşturulur. WithCC sınıfının içinde yapıcı işlevler hiç
bulunmasaydı, o zaman derleyici otomatik olarak varsayılan yapıcı işlevi
üretirdi, ve bu durumda da yapıcı işlev hiçbir faaliyet yapmazdı.
Bir şekilde kopya yapıcı işlev eklerseniz, o zaman derleyiciye "yapıcı
kullanacaksın" diyorsunuz, ve o da artık varsayılan yapıcıyı oluşturmaz,
WithCC için yapıldığı gibi varsayılan yapıcı işlevi açık biçimde
oluşturmadığınız da, derleyici bundan şikayetçi olur.
WoCC sınıfında kopya yapıcı işlev bulunmamaktadır, fakat onun yapıcı
işlevi bir iç string'te ileti saklamaktadır, ve bu ileti print( ) işlevi
yazdırılabilir. Bu yapıcı işlev Composite'in yapıcı işlev ilk değer atama
listesinde açık biçimde çağrılır. (buna benzer konular 8. bölümde
anlatılmıştı, daha fazla ayrıntı 14. bölümde verilecektir.). Bu işlemin
nedenleri ilerleyen bölümlerde belirginleşecektir.
Composite sınıfı hem WithCC hem de WoCC sınıflarının nesne üyelerini
bulundurur (burada wocc yekpare nesnesi olması gerektiği gibi, yapıcı
işlev ilk değer atama listesinde ilk değeri yerleştirilir.), ve kopya yapıcı
işlev açık biçimde tanımlanmaz.Bunun yanında main( ) içinde tanımlama
ile kopya yapıcı işlev kullanılarak nesne oluşturulur.
Composite c2=c ;
Composite'in kopya yapıcı işlevi derleyici tarafından otomatik olarak
oluşturulur ve programın çıktısı oluşturma biçimini gösterir.
c nin ici: Composite( )
bilesim kopya yapici cagrisi:
WithCC(WithCC&)
c2 nin ici :Composite( )
Harçlama kullanan bir sınıf için kopya yapıcı işlev oluşturma, derleyicinin
bütün üye nesneler ve ana sınıf için, kopya yapıcı işlevleri öz yinelemeli
olarak çağırması ile sağlanır. Bunun anlamı; bir üye nesne başka bir
nesneyi de ihtiva ediyorsa, bu nesnenin kopya yapıcı işlevi de çağrılır.
Buradaki durumda derleyici WithCC'nin kopya yapıcı işlevini çağrırır.
Çıktı, bu kopya yapıcı işlevin çağrıldığını gösterir. WoCC'de kopya yapıcı
işlev olmadığı için, derleyici onun için bir kopya yapıcı işlev oluşturur,
bitkopya işlemi gerçeklenir, ve Composite kopya yapıcısının içine çağrılır.
c2.wocc ile c.wocc içeriklerinin aynı olması nedeni ile main içinde
bulunan Composite::print( ) çağrısı yukarıdaki işlemlerin yerine
getirildiğini gösterir. Kopya yapıcı oluşumu boyunca derleyicinin izlediği
süreç üyesel ilk değer atama olarak adlandırılır.
Aslında en iyisi, gereken kopya yapıcı işlevi, derleyici yerine, kendinizin
oluşturmasıdır. Bu yol, her işlemi kendi denetiminiz de tutmanıza yardımcı
olur.
Kopya yapıcılara başka seçenekler
Artık beyniniz deryalarda dolanırken, acaba kopya yapıcı işlevler hakkında
hiçbir şey bilmeden nasıl çalışan bir sınıf oluşturabileceğinizi
düşünüyorsunuzdur. Anımsayalım; eğer sınıfınızın nesnesi sadece değerle
aktarılacak olursa kopya yapıcı işleve gerek duyulur. Bu durumla hiç
karşılaşılmazsa, kopya yapıcı işleve de hiçbir zaman gerek duyulmaz.
Değerle aktarımı engelleme
Fakat şimdi şunu sorabilirsiniz; eğer bir kopya yapıcı işlev oluşturmazsam,
derleyici otomatik olarak bunu oluşturacak, peki bir nesnenin hiçbir zaman
değerle aktarılmayacağını nasıl bilebilirim?.
Değerle aktarımı engellemek için basit bir yöntem var; private kopya
yapıcı işlev bildirimi yapmak . Hatta üye veya friend işlevlerden birinin,
değerle aktarım gereksinimi yoksa, bu kopya yapıcı işlevin tanımına da
gerek yoktur. Kullanıcı nesneyi değerle aktarır veya geri döndürürse,
derleyici, kopya yapıcı işlevin private olmasından ötürü hata iletisi üretir.
Artık varsayılan kopya yapıcı işlev, bizzat herşeyi siz belirttiğiniz için
oluşturulmaz.
İşte bir örnek;
//:B11:NoCopyConstruction.cpp
//kopya yapıcı engellenmesi
class NoCC{
int i;
NoCC(const NoCC&) ; //tanım yok
public:
NoCC(int ii=0):i(ii){}
};
void f(NoCC) ;
int main( ){
NoCC n ;
//!f(n) ; hata :kopya yapıcı çağrıldı
//! NoCC n2=n ; //hata ky çağrıldı
//! NoCC n3(n) ; //hata ky çağrıldı
}///:~
Daha genel biçimde kullanım;
NoCC(const NoCC&) ;
bu sırada const kullanılır.
Dışarıdaki nesneleri değiştiren işlevler
Dayanç kullanmak göstergeç kullanmaya göre daha kolay ve uygundur,
okuyucuya anlamı örter. Örnek olarak iostream kütüphanesinin get( )
işlevinin bindirilmiş sürümü char& değişkenini alır, işlevin bütün amacı
get( ) işlevinin sonucunu yerleştirerek değişkeni farklılaştırmaktır. İşlevi
kullanan kodları okurken dışarıdaki nesnenin değişiyor olduğunu görmek
hemen mümkün değildir.
char c;
cin.get(c) ;
Bunun yerine, işlev çağrısı kullanımı, değerle aktarım gibi görünür ve
dışarıdaki nesnenin değişmediğini bildirir.
Bu nedenle kodların bakımı açısından, değiştirilecek değişkenlerin
adreslerini aktarırken, herhalde göstergeç kullanmak daha güvenlidir. Eğer
adresleri, dışarıdaki nesneleri adres yolu ile değiştirmeyecekseniz, const
olmayan göstergeç ile aktardığınız yer, her zaman const dayançlar olarak
aktarırsanız, böylece kodların okuyucu için takibi çok daha kolay olur.
Üye göstergeçleri
Göstergeç, bellekte yer adresi tutan değişkendir. Çalışma sırasında,
göstergecin belirttiği adresi değiştirebilirsiniz. Göstergecin varışı, veri ya
da işlev olabilir. C++ üyeye göstergeç uygulamasında aynı kabulleri takip
eder. Tek istisnası sınıf içindeki bir yeri seçtiği durumdur. İkilemin
kaynağı; göstergeçin bir adrese ihtiyaç duyması, fakat sınıf içinde "adres"
bulunmamasıdır ; bir sınıf üyesinin seçimi, sınıfta kayma anlamını taşır.
Belli bir nesnenin başlangıç adresi ile, bu kayma birleştirilene kadar,
gerçek adres üretilemez. Üyelere göstergecin yazımı, aynı anda seçilen
nesneye gereksinim duyar, böylece üyeye göstergeç desteğinden kurtulur.
Bu yazım biçimini anlamak için basit bir yapıyı ele alalım, sp göstergeci ve
so nesnesi olsun. Üyeleri aşağıdaki yazım biçimi ile seçebilirsiniz.
//:B11:SimpleStructure.cpp
struct Simple{int a;};
int main( ){
Simple so, *sp=&so ;
sp->a ;
so.a ;
}///:~
Şimdi integer'e göstergeç olarak olarak ip'yi kabul edelim, ip'nin
gösterdiğine erişmek için, göstergeci (*) işareti ile desteğinden kurtarmak
gerekir .
*ip=4 ;
Son olarakta sınıf nesnesi içinde bulunan, hatta aslında nesnenin kaymasını
gösterse bile, bazı unsurları gösteren bir göstergecimiz olduğu durumu
inceleyelim. Gösterdiğine erişmek için, mutlaka (*) işareti ile desteğinden
kurtarılmalıdır. Fakat bu özel nesne ile ilgili bir kayma miktarı bulunur, o
nedenle bu değer de mutlaka göönüne alınmalıdır. Buradan şunu
belirtebilirizi; (*) işareti nesnenin desteğinden kurtulması için kullanılır.
Daha sonra yeni yazım biçimi (->*) nesneye göstergeç amacı için
kullanılır. Aşağıda (*) işareti nesne ya da dayanç için örneklenmiştir;
objectPointer->*pointerToMember=50 ;
object.*pointerToMember=50 ;
Şimdi pointerToMember'i tanımlayan yazım biçimi ne olacak?. Öteki
göstergeçler gibi, gösterilen tipi mutlaka belirtmeli ve (*) işaretini tanımda
kullanmalısınız. Tek fark, bu üyeye göstergecin, hangi sınıfın nesneleri ile
kullanılacağının belirtilmesi zorunluluğudur. Tabii bu işlem sınıf adının
yazılması ve görüntü duyarlık işlecinin belirtilmesi ile gerçeklenir. Böylece
;
int ObjectClass::*pointerToMember ;
ObjectClass içinde bulunan, herhangi int değeri işaret eden,
pointerToMember'i çağıran üyeye göstergeç değişkenidir. Bütün
bunlardan başka, dilenirse tanımlandığı yerde üyeye göstergece ilk değer
ataması yapılabilir. (ya da istenen herhangi bir zamanda)
int ObjectClass::*pointerToMember=&ObjectClass::a ;
Aslında ObjectClass::a gösteriminde adres bulunmaz zira, sadece sınıfı
temel alıyor, yoksa o sınıfın bir nesnesini değil. Böylece &ObjectClass::a
yazım biçimi sadece üyeye göstergeçlerde kullanılabilir.
Şimdi vereceğimiz örnekte, veri üyelerini işaret eden göstergeçlerin
oluşumlarını ve, kullanım biçimlerini göreceksiniz.
//:B11:PointerToData.cpp
#include <iostream>
using namespace std ;
class Data{
public:
int a,b,c ;
void print( ) const{
cout<<"a= "<<a<<", b= "<<b
<<", c= "<<c<<endl ;
}
};
int main( ){
Data d, *dp=&d ;
int Data::*pmInt=&Data::a ;
dp->*pmInt=50 ;
pmInt=&Data::b ;
d.*pmInt=51 ;
pmInt=&Data::c ;
dp->*pmInt=52 ;
dp->print( ) ;
}///:~
Özel durumların haricinde bunları kullanmak oldukça sorunludur
(tamamen özellikle planlandıkları durumlar için kullanılmalı)
Ayrıca üyelere göstergeçler tamamen sınırlı alanlarda kullanılırlar: sadece
sınıf içinde belli bir alanda görevlendirilirler.
Normal göstergeçlerde olduğu gibi bu türleri örneğin; arttırmazsınız,
karşılaştırma yapamazsınız.
İşlevler
Benzer çalışmalar üye işlevler için de, üyeye göstergeç yazım biçimini
üretir. Bir işleve göstergeç (3. bölümün sonunda konu tanıtılmıştı)
aşağıdaki biçimde tanımlanır;
int (*fp)(float) ;
(*fp) etrafında bulunan ayraçlar derleyicinin tanımı doğru şekilde
işlemlemesi için gereklidir. Eğer bu ayraçlar bulunmazsa o zaman int*
dönüş değeri elde edilir.
Ayraçlar üye işlevlere, göstergeçlerin kullanılması ve tanımlanmasında
önemli rol oynarlar. Bir sınıfın içinde bir işlev varsa, o üye işlev için, sınıf
adını ve görüntü duyarlık işlecini, normal işlev göstergeç tanımına
yerleştirerek, bir göstergeç tanımlayabilirsiniz.
//:PmemFunDefinition.cpp
class Simple2{
public:
int f(float) const{return 1;}
};
int (Simple2::*fp)(float) const ;
int (Simple2::*fp2)(float) const=&Simple2::f ;
int main( ){
fp=&Simple2::f ;
}///:~
fp2 için tanım yapılırken üye işleve göstergeç oluşturulurken ayrıca ilk
değer atamasıda yapılır, ya da gereken herhangi bir anda. Üye işlevin
adresi alınırken, üye olmayan işlevlere benzemeyen bir biçimde, & işareti
bir opsiyon değildir. İşlev belirteci değişken listesi olmadan yazılır. Bunun
sebebi üyeye göstergecin tipi bindirim duyarlılığını belirler.
Bir Örnek
Göstergecin değeri, program çalışırken gösterdiği noktada değişmesi
mümkündür, bu programlamada büyük bir esneklik sağlar, zira göstergeç
sayesinde program çalışırken davranışı seçebilir veya değiştirebilirsiniz.
Üyeye göstergeçte farklı değildir ; çalışma esnasında üye seçimine izin
verirler. Genel olarak sınıflarda sadece herkesin görebildiği (public) üye
işlevler bulunur (veri üyeler ise genellikle uygulamanın bilinen (private)
kısmında yer alırlar)
Aşağıdaki örnek çalışma esnasında üye işlevleri seçer;
//:B11:PointerToMemberFunction.cpp
#include <iostream>
using namespace std ;
class Widget{
public:
void f(int) const{cout<<"Widget::f( )\n";}
void g(int) const{cout<<"Widget::g( )\n";}
void h(int) const{cout<<"Widget::h( )\n";}
void i(int) const{cout<<"Widget::i( )\n" ;}
};
int main( ){
Widget w ;
Widget* wp=&w ;
void (Widget::*pmem)(int) const=&Widget::h ;
(w.*pmem)(1) ;
(wp->*pmem)(2) ;
}///:~
Tabii arasıra çalışan bir programcıdan böyle karmaşık açıklamalar
beklemek yanlış olur. Eğer kullanıcı üyenin göstergecini doğrudan
işlemlemek zorundaysa, o zaman typedef kullanmalıdır. Herşeyi tertemiz
anlaşılır bir şekilde yapmak için, üyenin göstergeçi iç uygulamanın bir
parçası olarak kullanılabilir. Şimdi önceki örneği sınıf içindeki üyenin
göstergeçi ile yazalım;
Bütün kullanıcı ihtiyaçları seçilecek bir işleve aktarılan sayı ile yapılır.
///:B11:PointerToMemberFunction2.cpp
#include <iostream>
using namespace std ;
class Widget{
void f(int) const{cout<<"Widget::f( )\n;}
void g(int) const{cout<<"Widget::g( )\n;}
void h(int) const{cout<<"Widget::h( )\n;}
void i(int) const{cout<<"Widget::i( )\n;}
enum{cnt=4} ;
void (Widget::*fptr[cnt])(int) const ;
public:
Widget( ){
fptr[0]=&Widget::f( ) ; //tüm ayrıntı gerekli
fptr[1]=&Widget::g( ) ;
fptr[2]=&Widget::h( ) ;
fptr[3]=&Widget::i( ) ;
}
void select(int i,int j){
if(int i<0||i>=cnt)return;
(this->*fptr[i])(j) ;
}
int count( ){return cnt;}
};
int main( ){
Widget w ;
for(int i=0;i<w.count( );i++)
w.select(i,50) ;
}///:~
Sınıf arayüzünde ve main( ) işlevinde uygulamanın tamamını
görebilirsiniz. İşlevlerde dahil, herşey gözden uzakta yapılır. İşlevlerin
count( ) yani sayısı, kod tarafından mutlaka sorulmalıdır. Bu yol
sayesinde, sınıf kullanıcısı sınıfın bulunduğu yerde kullanılan kodları
etkilemeden, vurgulanan uygulamadaki işlevlerin sayısını değiştirebilir.
Yapıcı işlevde üyeye göstergeçlerin, ilk değer atamaları belirtilmemiş gibi
görünür.
fptr[1]=&g ;
Bunu söyleyememeniz lazımdır zira g adı üye işlevde bulunur, bu da
otomatik olarak sınıf görüntüsünde yani, sınıfın kapsama alanında bulunur.
Aslında buradaki sorun, üyeye göstergeç yazım biçimine uymamasıdır,
herkes için bu böyledir, özellikle derleyici için, neyin devam ettiğini
çözümleyebilir.
Benzer şekilde üyeye göstergeç dayancından arındırıldığında aşağıdaki
gibi görünür;
(this->*fptr[i])(j) ;
ayrıca bu da belirtilmemiştir; this burada yazılmayabilir. Yine yazım
biçimi gereği bir üyenin göstergeçi nesne dayancından arındırılırken her
zaman ona bağımlıdır.
Özet
C++ daki göstergeçler C dekiler ile hemen hemen aynıdır. Bu iyi bir
şeydir. Öte yandan bir sürü C kodu, C++ derleyicileri tarafından
derlenememektedir. Derleyicide kod hatalarının üremesi, tehlikeli değer
atamalarından kaynaklanmaktadır. Bunlar belirlenebilirlerse, basit (ve
açık) dökümle kolayca ortadan kaldırılabilirler.
C++ dili ayrıca Algol ve Pascal dillerinden dayancı (reference) almıştır.
Dayanç sabit bir göstergeçmiş gibi derleyici tarafından otomatik olarak
kullanılır. Dayanç bir adres tutar, ve siz onu nesne gibi işlemlersiniz.
(&a=a değişkeninin adresi demektir). Dayançlar işleç bindirimleri için
temiz yazım biçimleri sağlarlar (bir sonraki bölümün konusu). Bunlardan
başka normal işlevler için de nesne aktarımı ve geri döndürülmesinde
yazım çok uygundur.
Kopya yapıcı işlevler değişken olarak aynı tipin varolan nesnesine dayanç
alırlar, bu sayede varolan bir nesneden yeni bir nesne üretirler. Nesne
değerle aktarılır veya geri döndürülürse derleyici otomatik olarak kopya
yapıcı işlevi çağırır. Derleyiciler otomatik olarak bizim için kopya yapıcı
işlevi üretmelerine rağmen birisinin sizin sınıfınıza gerek duyacağını
düşünürseniz, doğru işlemler gerçeklenmesi için sizin kopya yapıcı işlevi
tanımlamanız daha güvenlidir. Nesnenin değerle aktarılması veya geri
döndürülmesini istemiyorsanız, private kopya yapıcı işlev
tanımlamalısınız.
Üyelere göstergeçler, normal göstergeçler gibidirler. Program çalışırken
saklama alanının belli bir bölgesini seçebilirsiniz. Üyelere göstergeçler,
global veri ve işlevler yerine sınıf üyeleri ile çalışırlar. Bu programlama
esnekliği, program çalışırken davranışları değiştirme iznini verir.
12. BÖLÜM
İşleç bindirimi
İşleç bindirimi işlev çağrımının ikinci yolu olup, bir çeşit yazım biçimi
"tatlandırıcısıdır". Buradaki fark; işlevin değişkenlerinin ayraç içinde
görünmemesidir, ama onların yerine, onların çevrelediği veya, her zaman
değiştirilemez işleçler olarak düşünülen karakterlerden sonra gelmeleridir.
Bir işleç ile normal bir işlev çağrımının arasında, iki temel fark bulunur.
Yazım biçimi farklılığı; bir işleç çoğunlukla iki değişken arasında veya
arasıra, değişkenden sonra "çağrılır". İkinci fark ise, derleyicinin hangi
"işlevin" çağrılacağını saptamasıdır. Bir örnekle anlatalım; eğer bir (+)
işlecini kayan noktalı değişkenlerle kullanıyorsanız, derleyici kayan
noktalı toplama işlemini gerçekleyecek işlevi "çağırır" (Bu "çağrı" satıriçi
kod yerleşimidir, veya kayan noktalı işlemleyici komutudur). Yok eğer (+)
işlecini bir kayan noktalı ve bir de ondalık değişkenle kullanıyorsanız, o
zaman derleyici özel bir işlevi çağırarak int değeri float'a çevirir ve daha
sonra, kayan noktalı toplama kodunu "çağırır".
C++ dilinin C den bir farklılığı da işleçler konusundadır. İstendiği taktirde
C++ dilinde, sınıflarla çalışabilen özel işleçler tanımlanıp kullanılabilir.
Yeni işleç tanımları normal işlev tanımlarına benzer, sadece işlevin adını
oluşturan operator anahtar kelimesinden sonra işlecin kendi biçiminin
gelmesidir. Öteki işlevler gibi olan, işleç tanımlarındaki tek fark;
derleyicinin yazım düzenini gördüğünde işlevi çağırmasıdır.
Uyarı ve yeniden güvenlik
İşleç bindirimi aşırı heyecan verici göründüğünden, insanı kullanmaya
teşvik eder. Başlangıçta eğlenceli bir oyuncaktır, ama unutmayın o sadece
bir tatlandırıcıdır. Yani, sadece bir çeşit işlev çağrımı sağlayan, bir yazım
biçimi tatlandırıcısı. Bu şekilde baktığınız zaman, işleç bindirimi
gereksizdir, eğer sınıfı içeren kod yazımını, ve özellikle de okumayı
kolaylaştırıyorsa, o zaman kullanabilirsiniz. (Kodların okunması
yazılmasından daha fazladır).
İşleç bindirimine ikinci yaygın yanıt paniktir: birdenbire, C işleçleri artık
bilinen anlamlarını kaybederler, "Herşey değişir ve C kodlarınız artık
bambaşka işler yapar".
Bu gerçek değildir. Açıklamalarda kullanılan bütün işleçlerle,
değiştirilemez yerleşik veri tipleri bulunur. Aşağıdaki biçimde bulunan
işleçlere hiçbir zaman bindirimde bulunamazsınız.
1<<4 ;
farklı davranır, veya;
1.414<<2 ;
anlamı bulunur. Sadece kullanıcı tanımlı tip içeren bir açıklamada
bindirimli işleç bulunabilir.
Yazımbiçimi
Bindirimli işleç tanımı işlev tanımına benzer, fakat burada işlev adı
operator@ dır. Buradaki @ işareti bindirime uğrayacak işlecin şeklidir.
Bindirimli işleç değişken listesindeki değişken sayısı iki etmene bağlıdır.
1- Tekyanlı (işlecin bir tarafında değişken olması) ya da çift yanlı (işlecin
heriki yanında değişken olması) işleç olması.
2- İşlecin global işlev (tekyanlı işlev için tek değişken, çiftyanlı işleç için
iki değişken) ya da üye işlev (tekyanlı için sıfır, çiftyanlı işleç için bir
değişken- nesne o zaman solyan değişkeni olur) olarak tanımlanması.
Şimdi işleç bindirimini gösteren basit bir sınıf örneği verilecek;
//:B12:OperatorOverloadingSyntax.cpp
#include <iostream>
using namespace std ;
class Integer{
int i ;
public:
Integer(int ii):i(ii){}
const Integer ;
operator+(const Integer& rv)const{
cout<<"operator+"<<endl ;
return Integer(i+rv.i) ;
}
Integer& ;
operator+=(const Integer& rv){
cout<<"operator+="<<endl ;
i+=rv.i ;
return *this ;
}
};
int main( ){
cout<<"yerleşik tipler:"<<endl ;
int i=1,j=2,k=3 ;
k+=i+j ;
cout<<"kullanıcı tanımlı tipler:"<<endl ;
Integer ii(1),jj(2),kk(3) ;
kk+=ii+jj ;
}///:~
Çağırıldığı zaman bildirilen, iki bindirilmiş işleç satıriçi üye işlev olarak
tanımlanmıştır. İşlecin sağ tarafında bir değişken görüldüğünde bunun ikili
işleç olduğu anlaşılır. Üye işlevler olarak tanımlanan tek-yanlı işleçlerde
ise değişken bulunmaz. İşlecin sol yanında bulunan nesne için işlev
çağrımı yapılır.
Duruma bağlı olmayan işleçler için (duruma bağlı olanlar bool sonucu
üretir) hemen hemen her zaman bir nesne veya aynı tipin dayancı
döndürülmek istenir . Tabii üzerinde çalışılan her iki değişken de aynı
tipse. (Eğer her ikisi aynı tip değilse döndürülecek tipin yorumu size
bırakılmıştır) . Bu yolla karmaşık açıklmalar tanımlayabiliriz ;
kk+=ii+jj ;
operator+ işleci, yeni bir Integer'i(geçici şablon) operator+= işleci için,
rv değişkeni olarak kullanmak amacı ile üretir. Bu geçici şablon, ihtiyaç
ortadan kalkar kalkmaz yani görevini yerine getirir getirmez imha edilir.
Bindirim yapılabilen işleçler
C dilinde bulunan hemen hemen bütün işleçlere bindirim yapılabilmesine
rağmen, işleç bindiriminin kullanımı oldukça sınırlıdır. Özellikle şu anda C
dilinde anlamı bulunmayan işleçleri biraraya getiremezsiniz ( örnek olarak
üs alımında kullanılan ** işleci gibi), işleçlerin hesaplamalardaki öncelik
sıralamalarını değiştiremezsiniz, ve bir işleç için gereken değişken sayısını
değiştiremezsiniz. Bu anlama gelen hareketlerden sonra, işleç bindirimleri
sizce ortalığı daha berrak nasıl yapabilir.
Bundan sonraki iki alt bölümde, bütün "düzenli" işleçlerle ilgili örnekler,
ve bindirilmiş olanların yaygın olarak kullanılanları gösterilecektir.
Tek-yanlı işleçler
Aşağıdaki örnek, bütün tekyanlı işleçlerin bindirimi için, yazım biçimini
göstermektedir. Örnek hem global (üye olmayan friend işlevler) hem de
üye işlevlere uygun verilmiştir. Önceden tanımlanmış olan Integer sınıfını
genişletir, ve yeni byte sınıfını ilave eder. Özel işleçlerinizin anlamı
kullanma yolunuza bağlıdır, müşteri programcı beklenmeyen bir şey
yapmadan önce işlemler düzenlemelidir.
Şimdi tek yanlı işleçlerin hepsini gösterelim;
//:B12:OverloadingUnaryOperators.cpp
#include <iostream>
using namespace std ;
//Üye olmayan işlevler
class Integer{
long i ;
Integer* this( ){return this ;}
public:
Integer (long ll=0):i(ll){}
//yan etkiler const& ve değişken almaz
friend const Integer&
operator+(const Integer& a);
friend const Integer
operator-(const Integer& a);
friend const Integer
operator-(const Integer& a) ;
friend Integer*
operator&(Integer& a) ;
friend int
operator!(const Integer& a) ,
//Yan etkilerde sabit olmayanlar ve değişkenler var
//Önek:
friend const Integer&
operator++(Integer& a) ;
//Sonek:
friend const Integer
operator++(Integer& a,int) ;
//Önek
friend const Integer&
operator--(Integer& a) ;
//sonek
friend const Integer
operator--(Integer& a,int) ;
};
//Global işleçler
const Integer& operator+(const Integer& a){
cout<<"+Integer\n" ;
return a ; //tek yanlı + ,etki yok
}
const Integer& operator-(const Integer& a){
cout<<"-Integer\n" ;
return Integer(-a.i) ;
}
const Integer& operator-(const Integer& a){
cout<<"-Integer\n" ;
return Integer(-a.i) ;
}
Integer* operator&(Integer& a){
cout<<"&Integer\n" ;
return a.this( ) ;
//&a özyinelemeli
}
int operator!(const Integer& a){
cout<<"!Integer\n" ;
return !a.i ;
}
//Önek ; bir arttırılan değeri geri döndür
const Integer& operator++(Integer& a){
cout<<"++Integer\n" ;
a.i++ ;
return a ;
}
//Sonek değer artmadan önce değeri geri döndür
const Integer operator++(Integer& a,int){
cout<<"Integer++\n" ;
Integer before(a.i) ;
a.i++ ;
return before ;
}
//Önek azalan değeri geri döndür
const Integer& operator--(Integer& a){
cout<<"--Integer\n" ;
a.i-- ;
return a ;
}
//Sonek azalmadan önce değeri geri döndür
const Integer operator--(Integer& a,int){
cout<<"Integer--\n" ;
Integer before(a.i) ;
a.i-- ;
return before ;
}
//Çalışan bindirilmiş işleçleri göster
void f(Integer a){
+a ;
-a ;
~a ;
Integer* ip=&a ;
!a ;
++a ;
a++ ;
--a ;
a-- ;
}
//Üye işlevler ("this" e özgü)
class Byte{
unsigned char b ;
public:
Byte(unsigned char bb=0):b(bb){}
//yan etki yok: const üye işlev
const Byte& operator+( ) const{
cout<<"+Byte\n" ;
return *this ;
}
const Byte operator-( ) const{
cout<<"-Byte\n" ;
return Byte(-b) ;
}
const Byte operator-( ) const{
cout<<"-Byte\n" ;
return Byte(-b) ;
}
Byte operator!( ) const{
cout<<"!Byte\n" ;
return Byte(!b) ;
}
Byte* operator&( ){
cout<<"&Byte\n" ;
return this ;
}
//yan etkiler: sabit olmayan üye işlev:
const Byte& operator++( ){//önek
cout<<"++Byte\n" ;
b++ ;
return *this ;
}
const Byte operator++(int){//sonek
cout<<"Byte++\n" ;
Byte before(b) ;
b++ ;
return before ;
}
const Byte& operator--( ){//önek
cout<<"--Byte\n" ;
--b ;
return *this ;
}
const Byte operator--(int){//sonek
cout<<"Byte--\n" ;
Byte before(b) ;
--b ;
return before ;
}
};
void g(Byte b){
+b ;
-b;
-b;
Byte* bp=&b ;
!b ;
++b ;
b++;
--b ;
b-- ;
}
int main( ){
Integer a ;
f(a) ;
Byte b ;
g(b) ;
}///:~
İşlevler aktarılan değişkenlerine göre gruplandırıldılar. Değişkenlerin
aktarılma ve geri döndürülme ile ilgili temel bilgileri, ilerleyen bölümlerde
ayrıntıları ile verilecektir. Yukarıdaki biçimleri (ve aşağıdaki
gösterilenleri) başlangıç olarak kullanabilir, kendi işleçlerinizin bindirimi
için, kullanım düzeni yapabilirsiniz.
Değer artımı ve azaltımı
(++) ve (--) işleçlerinin bindirilmiş kullanımı bir ikileme yolaçar. Bunun
sebebi; bu işleçler görüldüğünde, üzerinde işlem yapılan nesnenin
sağındamı yoksa solundamı olduğuna bağlı olarak farklı işlevlerin
çağrılmasıdır. Aslında çözüm çok basittir, fakat başlangıçta biraz zihin
karıştırır. Örnek olarak derleyici, ++a (ön artım) yı gördüğünde,
operator++(a) işlev çağrımını üretir, a++ (sonra artım) yı gördüğünde,
operator++(a,int) işlev çağrımını üretir. Yani derleyici iki farklı biçimi,
kendisi farklı işlevleri çağırarak ayırır.
OverloadingUnaryOperators.cpp program parçasının üye işlev
sürümlerinde, derleyici ++b yi görürse, B::operator++( ) işlev çağrısını,
b++ yi görürse, B::operator++(int) çağrısını yapar.
Bütün kullanıcılar, önek ve sonek sürümleri için farklı işlevlerin
çağrıldığını görürler. Bu yüzden her iki sürüm için farklı işlev gövdeleri
işleme tabi tutulur. Derleyici sonek sürümündeki int değişkeni için, boş bir
sabit değer(belirteç konmaz zira hiç bir zaman kullanılmaz) aktararak
farklı bir gösterim oluşturulur.
İki yanlı işleçler
Aşağıdaki örnek, OverloadingUnaryOperators.cpp de bulunan bütün tek
yanlı işleçlerin, iki yanlı işleçlere göre düzenlenmiş halidir. Böylece
bindirilmiş olarak kullanılabilecek bütün çift yanlı işleçler öğrenilmiş olur.
Ve yine global sürümler ile üye işlev sürümleri birlikte gösterilmiştir.
//:B12:Integer.h
//Üye olmayan bindirilmiş işleçler
#ifndef INTEGER_H
#define INTEGER_H
#include <iostream>
//üye olmayan işlevler
class Integer{
long i ;
public:
Integer(long ll=0):i(ll){ }
//işleçler new oluştururlar, değiştirilen değer:
friend const Integer
operator+(const Integer& left,
const Integer& right) ;
friend const Integer
operator-(const Integer& left,
const Integer& right) ;
friend const Integer
operator*(const Integer& left,
const Integer& right) ;
friend const Integer
operator/(const Integer& left,
const Integer& right) ;
friend const Integer
operator%(const Integer& left,
const Integer& right) ;
friend const Integer
operator^(const Integer& left,
const Integer& right) ;
friend const Integer
operator&(const Integer& left,
const Integer& right) ;
friend const Integer
operator|(const Integer& left,
const Integer& right) ;
friend const Integer
operator<<(const Integer& left,
const Integer& right) ;
friend const Integer
operator>>(const Integer& left,
const Integer& right) ;
//lvalue değiştirme ve geri döndürme görevlendirmeleri
friend Integer&
operator+=(Integer& left,
const Integer& right) ;
friend Integer&
operator-=(Integer& left,
const Integer& right) ;
friend Integer&
operator*=(Integer& left,
const Integer& right) ;
friend Integer&
operator/=(Integer& left,
const Integer& right) ;
friend Integer&
operator%=(Integer& left,
const Integer& right) ;
friend Integer&
operator^=(Integer& left,
const Integer& right) ;
friend Integer&
operator&=(Integer& left,
const Integer& right) ;
friend Integer&
operator|=(Integer& left,
const Integer& right) ;
friend Integer&
operator<<=(Integer& left,
const Integer& right) ;
friend Integer&
operator>>=(Integer& left,
const Integer& right) ;
//duruma bağlı işleçler true/false döndürürler
friend int
operator==(const Integer& left,
const Integer& right) ;
friend int
operator!=(const Integer& left,
const Integer& right) ;
friend int
operator<(const Integer& left,
const Integer& right) ;
friend int
operator>(const Integer& left,
const Integer& right) ;
friend int
operator<=(const Integer& left,
const Integer& right) ;
friend int
operator>=(const Integer& left,
const Integer& right) ;
friend int
operator&&(const Integer& left,
const Integer& right) ;
friend int
operator||(const Integer& left,
const Integer& right) ;
//ostream sınıf içeriklerini yaz
void print(std::ostream& os) const{os<<i; }
};
#endif //INTEGER_H//
//:B12:Integer.cpp {O}
//Bindirilmiş işleç uygulamaları
#include "Integer.h"
#include "../require.h"
const Integer
operator+(const Integer& left,
const Integer& right){
return Integer(left.i+right.i) ;
}
const Integer
operator-(const Integer& left,
const Integer& right){
return Integer(left.i-right.i) ;
}
const Integer
operator*(const Integer& left,
const Integer& right){
return Integer(left.i*right.i) ;
}
const Integer
operator/(const Integer& left,
const Integer& right){
return Integer(left.i/right.i);
}
const Integer
operator%(const Integer& left,
const Integer& right){
return Integer(left.i%right.i) ;
}
const Integer
operator^(const Integer& left,
const Integer& right){
return Integer(left.i^right.i) ;
}
const Integer
operator&(const Integer& left,
const Integer& right){
return Integer(left.i&right.i) ;
}
const Integer
operator|(const Integer& left,
const Integer& right){
return Integer(left.i|right.i) ;
}
const Integer
operator<<(const Integer& left,
const Integer& right){
return Integer(left.i<<right.i) ;
}
const Integer
operator>>(const Integer& left,
const Integer& right){
return Integer(left.i>>right.i) ;
}
//lvalue geri dönüş ve değişim görevlendirmeleri
Integer& operator+=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendine görevlendirme*/}
left.i+=right.i ;
return left ;
}
Integer& operator-=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i-=right.i ;
return left ;
}
Integer& operator*=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i*=right.i ;
return left ;
}
Integer& operator/=(Integer& left,
const Integer& right){
require(right.i!=0,"divide by zero")
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i/=right.i ;
return left ;
}
Integer& operator%=(Integer& left,
const Integer& right){
require(right.i!=0,"modulo by zero") ;
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i/=right.i ;
return left ;
}
Integer& operator^=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i^=right.i ;
return left ;
}
Integer& operator&=(Integer& left,
const Integer& right){
if(&right==&left){/*kendi kendini görevlendirme*/}
left.i&=right.i ;
return left ;
}
Integer& operator|=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i|=right.i ;
return left ;
}
Integer operator>>=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i>>=right.i ;
return left ;
}
Integer operator<<=(Integer& left,
const Integer& right){
if(&left==&right){/*kendi kendini görevlendirme*/}
left.i<<=right.i ;
return left ;
}
//durum işleçleri true/false üretir
int operator==(const Integer& left,
const Integer& right){
return left.i==right.i ;
}
int operator!=(const Integer& left,
const Integer& right){
return left.i!=right.i ;
}
int operator<(const Integer& left,
const Integer& right){
return left.i<right.i ;
}
int operator>(const Integer& left,
const Integer& right)7
return left.i>right.i ;
}
int operator<=(const Integer& left,
const Integer& right){
return left.i<=right.i ;
}
int operator>=(const Integer& left,
const Integer& right){
return left.i>=right.i ;
}
int operator&&(const Integer& left,
const Integer& right){
return left.i&&right.i ;
}
int operator||(const Integer& left,
const Integer& right){
return left.i||right.i ;
}///:~
//:B12:IntegerTest.cpp
//{L} Integer
#include "Integer.h"
#include <fstream>
using namespace std ;
ofstream out("IntegerTest.out") ;
void h(Integer& c1,Integer& c2){
//karmaşık açıklama
c1+=c1*c2+c2%c1 ;
#define TRY(OP)\
out<<"c1= " ; c1.print(out);\
out<<",c2= " ; c2.print(out) ;\
out<<"; c1 " #OP" c2 produces ";\
(c1 OP c2).print(out); \
out<<endl ;
TRY(+) TRY(-) TRY(*) TRY(/)
TRY(%) TRY(^) TRY(&) TRY(|)
TRY(<<) TRY(>>) TRY(+=) TRY(-=)
TRY(*=) TRY(/=) TRY(%=) TRY(^=)
TRY(&=) TRY(|=) TRY(>>=) TRY(<<=)
//durumlar
#define TRYC(OP)\
out<<"c1= " ; c1.print(out);\
out<<",c2= " ; c2.print(out) ;\
out<<"; c1 " #OP" c2 produces ";\
out<<(c1 OP c2);\
out<<endl ;
TRY(<) TRY(>) TRY(==) TRY(!=) TRY(<=)
TRY(>=) TRY(&&) TRY(||)
}
int main( ){
cout<<"friend functions" <<endl ;
integer c1(47) c2(9) ;
h(c1,c2) ;
}///:~
//:B12:Byte.h
//Üye bindirilmiş işleçler
#ifndef BYTE_H
#define BYTE_H
#include "../require.h"
#include <iostream>
//üye işlevler (implicit this)
class Byte{
unsigned char b ;
public:
Byte(unsigned char bb=0):b(bb){ }
//yan etkileri yok: const üye işlev:
const Byte
operator+(const Byte& right)const{
return Byte(b+right.b) ;
}
const Byte
operator-(const Byte& right)const{
return Byte(b-right.b) ;
}
const Byte
operator*(const Byte& right)const{
return Byte(b*right.b) ;
}
const Byte
operator/(const Byte& right)const{
require(right.b!=0,"divide by zero");
return Byte(b/right.b) ;
}
const Byte
operator%(const Byte& right)const{
require(right.b!=0,"modulo by zero");
return Byte(b%right.b) ;
}
const Byte
operator^(const Byte& right)const{
return Byte(b^right.b) ;
}
const Byte
operator&(const Byte& right)const{
return Byte(b&right.b) ;
}
const Byte
operator|(const Byte& right)const{
return Byte(b|right.b) ;
}
const Byte
operator<<(const Byte& right)const{
return Byte(b<<right.b) ;
}
const Byte
operator>>(const Byte& right)const{
return Byte(b>>right.b) ;
}
//görevlendirmeler: lvalue değerlerinin geri dönüşü ve değişimi
//= işleci sadece üye işlev olabiliyor
Byte& operator=(const Byte& right){
//kendi kendini görevlendirme işlemi
if(this==&right)return *this ;
b=right.b;
return *this ;
}
Byte& operator+=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b+=right.b ;
return *this ;
}
Byte& operator-=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b-=right.b ;
return *this ;
}
Byte& operator*=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b*=right.b ;
return *this ;
}
Byte& operator/=(const Byte& right){
require(right.b!=0,"divide by zero") ;
if(this==&right){/*kendi kendine atama*/}
b/=right.b ;
return *this ;
}
Byte& operator%=(const Byte& right){
require(right.b!=0,"modulo by zero") ;
if(this==&right){/*kendi kendine atama*/}
b%=right.b ;
return *this ;
}
Byte& operator^=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b^=right.b ;
return *this ;
}
Byte& operator&=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b&=right.b ;
return *this ;
}
Byte& operator|=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b|=right.b ;
return *this ;
}
Byte& operator>>=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b>>=right.b ;
return *this ;
}
Byte& operator<<=(const Byte& right){
if(this==&right){/*kendi kendine atama*/}
b<<=right.b ;
return *this ;
}
//durum işleçleri true/false döndürür
int operator==(const Byte& right)const{
return b==right.b ;
}
int operator!=(const Byte& right)const{
return b!=right.b ;
}
int operator<(const Byte& right)const{
return b<right.b ;
}
int operator>(const Byte& right)const{
return b>right.b ;
}
int operator<=(const Byte& right)const{
return b<=right.b ;
}
int operator>=(const Byte& right)const{
return b>=right.b ;
}
int operator&&(const Byte& right)const{
return b&&right.b ;
}
int operator||(const Byte& right)const{
return b||right.b ;
}
//içerikleri ostreame yaz
void print(std::ostream& os)const{
os<<"0x"<<std::hex<<int(b)<<std::dec ;
}
};
#endif //BYTE_H//
//:B12:ByteTest.cpp
#include "Byte.h"
#include <fstream>
using namespace std ;
ofstream out("ByteTest.h") ;
void k(Byte& b1,Byte& b2){
b1=b1*b2+b2%b1 ;
#define TRY2(OP)\
out<<"b1= ";b1.print(out);\
out<<", b2= ";b2.print(out);\
out<<"; b1"#OP "b2 produces ";\
(b1 OP b2).print(out);\
out<<endl ;
b1=9; b2=47 ;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=)
//atama işleci
//durumlar
#define TRYC2(OP)\
out<<"b1= ";b1.print(out);\
out<<", b2= ";b2.print(out) ;\
out<<"; b1 " #OP "b2 produces "\
out<<(b1 OP b2) ;\
out<<endl ;
b1=9;b2=47 ;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
//ardışık atama
Byte b3=92 ;
b1=b2=b3 ;
}
int main( ){
out<<"üye işlevler: "<<endl ;
Byte b1(47),b2(9) ;
k(b1,b2) ;
}///:~
Daha sonra anlatılacağı üzere operator= e sadece üye işlev olarak izin
verilebilir.
Farkedeceğiniz gibi atama işleçlerinin hepsi kendi kendine atamayı
sorgulayan kodlar taşırlar; bu genel kuraldır. Gerçi bu bazı durumlar için
gerekmez; örnek olarak operator += ile demek istenen çoğu kez A+=A
dır, yani A nın kendine eklenmesidir. Kendi kendine atamanın
sorgulandığı en önemli yer operator= dir, zira karmaşık nesnelerle
felaketlere yolaçan sonuçlar elde edilebilir.( Bazı durumlar için tamam,
fakat operator= işlecini yazarken bunu her zaman aklınızda
bulundurmanız lazım)
Önceki iki örnekte gösterilen bütün işleçlere, tek bir tipin kullanımı için
bindirim yapılmıştır. Bunun yanında, karıştırılmış tiplerde de işleçlere
bindirim yapılabilir, örnek olarak böylece elmaları ayvalara
ekleyebilirsiniz. İşleçlerin uzun süren bindirimlerine başlamadan evvel,
bölümün ilerleyen kısımlarında yer alan, otomatik tip değişimine göz
atabilirsiniz. Çoğu kez uygun yerde yapılan tip değişimi, sizi yorucu işleç
bindirimlerinden kurtarır.
Değişkenler ve geri dönüş değerleri
Başlangıçta, OverloadingUnaryOperators.cpp, Integer.h ve Byte.h
programlarına bakıldığı zaman, biraz şaşırtıcı gelebilir, aktarılan ve geri
döndürülen değişkenler için bütün yolların kullanıldığı görülür. Aktarılan
ve geri döndürülen değişkenler için sizin herhangi bir yolu seçme
özgürlüğünüz olmasına rağmen, örneklerde kullanılan seçimler gelişigüzel
değildir. Seçimler mantıklı bir düzeni takip etmektedir, ve sizde
seçimleriniz de bunları tercih edebilirsiniz.
1--Herhangi bir işlev değişkeni ile, eğer sadece değişkenden okuma
gereksinimi varsa ve okunan değiştirilmeyecekse, aktarılan, const dayanç
olarak kabul edilir. Bilinen aritmetik (+ve - vb işlemler ) ve Bool işlemleri
bu değişkenleri değiştiremez.Bu sayede const dayançla aktarım kullanılana
göre üstünlük taşır. İşlev bir sınıfın üyesi ise, bu const üye işleve çevrilir.
Sadece atama (+= gibi ) işleçleri ve operator= ile, sol yan değişkeni
değişir, yani sol taraftaki değişken değişmez değildir. Fakat gene de adres
olarak aktarılır zira adres değişir.
2--Seçilen geri dönüş tipi, işlecin beklenen anlamına bağlıdır. (Gene
değişkenler ve geri dönüş değerleri ile, istediğiniz herhangi bir şeyi
yapabilirsiniz) . İşlecin etkisi yeni bir değer üretirse, o zaman geri dönüş
değeri olarak yeni bir nesne üretimi gerekir. Örnek olarak,
Integer::operator+ mutlaka işlenenlerin toplamı bir Integer nesne
üretmelidir. Bu nesne de const değeri ile geri döndürülür. Bundan dolayı
sonuç lvalue olarak değiştirlemez.
3--Bütün atama işleçleri lvalue değerini değiştirir. Zincirleme
açıklamalarda atama sonuçlarına izin verilerek kullanılabilirler, örnek
olarak a=b=c gibi, yeni değiştirilmiş aynı lvalue değeri dayanç olarak geri
döndürmesi beklenir.
Fakat bu dayanç değeri const'mı yoksa const değil mi? . Siz a=b=c
açıklamasını soldan sağa doğru okumanıza rağmen, derleyici onu sağdan
sola doğru bölümler, böylece atama zincirini desteklemek için const
olmayan geri dönüş değeri mecburiyetinde kalmazsınız. İnsanlar bazen
yeni atama yapılmış unsurlar üzerinde işlem yapmak isterler, (a=b).f( )
etkinliği gibi. Burada b değeri atanmış a için f( ) işlevi çağrılmakta. Bu
yüzden atama işleçlerinin tümü için geri dönüş değeri const olmayan
dayanç olarak lvalue'e konulmalıdır.
4--Mantık işleçleri için herkesin beklediği en kötü tip, int geri dönüşü, en
iyisi de bool'dur (C++ daki yerleşik bool tipini destekleyen derleyicilerin
çoğundan önce geliştirilen kütüphanelerde int ya da eşdeğeri typedef
kullanılır.)
Değer artırımı ve azaltımı ön ve arkada yeralan işaret sürümleri yüzünden
ikileme yolaçar. Her iki sürümde nesnede değişime sebeb olur, bu nedenle
nesne const olarak işlemlenemez. Önişaretli sürümde nesnenin değeri
değiştirildikten sonra geri döndürürlür.Böylece nesneyi değeri değiştirilmiş
olarak geri almayı beklersiniz. Önişaretli durumda *this dayanç olarak
hemen geri döndürülür. Arka işaretli sürümde ise, değer değiştirilmeden
önce geri döndürüldüğü kabul edilir, bu yüzden o değeri gösteren ve geri
döndüren ayrı bir nesne oluşturulmak zorunluluğu ortaya çıkar. Böylece
arka işaretli durumda beklenen anlam sağlanmak istenirse, mutlaka değerle
geri döndürmek zorunluluğu doğar.(Burada hemen belirtelim, bazı
durumlarda değer azaltımı ve artırımı işleçleri int veya bool geri döndürür,
göstermek için örnek olarak; liste sonundaki nesnenin liste içinde hareket
edecek şekilde tasarımlanmış olması gibi.). Şimdi soru şu: Bunların geri
dönüşü const'mı yoksa const olmayan bir değermi?. Nesnenin
değişmesine izin verilirse, ve birisi de (++a).func( ) yazdıysa, o zaman
func( ) a'nın kendi üzerinde işlem yapar. Fakat (a++).func( ) arka işaretli
sürüm durumunda, func( )
arka işaretli operator++ tarafından geri döndürülen geçici nesne üzerinde
işlem yapar. Geçici nesneler otomatik olarak const olurlar ve derleyici
tarafından bayraklanırlar, fakat tutarlılık adına daha anlamlı kılmak amacı
ile, burada yapıldığı gibi her ikisi de const yapılırlar. Ya da önişaretli olanı
const olmayan, arka işaretliyi ise const olarak seçmek mümkündür. Bunun
nedeni ise anlam çeşitliliği ile azaltan ve arttıran işleçleri farklılaştırmaktır,
böyle bir şeyin gereksinimi durumdan duruma temeline dayandırmaktır.
const olarak değeri geri döndürmek
Değeri const olarak geri döndürmek başlangıçta hassas bir konu gibi
görünür, ve o nedenle biraz daha açıklanması lazımdır.İkili işleç
operator+ gözönüne alalım. Biz bunu şöyle bir açıklamanın içinde
kullanırsak f(a+b), a+b nin sonucu, f( ) işlev çağrımında kullanılan geçici
nesne olur. Geçici olmasından ötürü otomatik olarak const olur. Bundan
dolayı geri dönüş değerini ya açık şekilde const, ya da etkisi olmayan bir
özellik yaparsınız.
Bunlardan başka a+b nin geri dönüş değerini işlev içine aktarmak yerine
bu geri dönüş değerine bir ileti gönderebilirsiniz. Örnek olarak (a+b).g( )
yazılabilir, g( ) bu durumda Integer'in üye işlevlerinden biridir.Geri dönüş
değerini const yaparak, geri dönüş değeri olarak sadece const üye işlev
çağrılabileceği belirtilmiş olur. Bu const doğrulamasıdır, zira böylece
nesnedeki çoğunlukla kaybolacak potansiyel değerli bilgilerin
saklanmasını engeller.
Geri dönüşün iyileştirilmesi
Değerle geri döndürmek için yeni nesneler oluşturulduğunda, biçim
dikkate alınmalıdır. örnek olarak operator+ alalım;
return Integer(left.i+right.i) ;
İlk bakışta "yapıcı işleve işlev çağrımı" gibi görünüyor, fakat öyle değil.
Yazım biçimi, geçici nesne yazım biçimidir. İlgili satırın söylediği; "geçici
bir Integer nesne oluştur ve geri döndür" . Bu nedenle sonucu, adı konmuş
yörel bir nesne oluşturup onu geri döndürmeyle aynı olur. Ama aşağıdaki
gibi yazılmış olsaydı, sonuç tamamen farklı olurdu;
Integer tmp(left.i+right.i) ;
return tmp ;
Burada üç şey olur. Birincisi; yapıcı işlev çağrımını da içeren tmp nesnesi
oluşturulur. İkincisi; kopya yapıcı işlev tmp yi dışarıdaki geri dönüş
değerinin bellek yerine kopyalar. Ve üçüncüsü ise; yıkıcı işlev görünümün
sonunda tmp için çağrılır.
Bunun tersine, "geçici olanı geri döndürme" yaklaşımı tamamen farklı
çalışır. Derleyici bunu yaptığınızı gördüğü zaman bilirki sizin, oluşturulan
nesnenin geri döndürmekten başka bir şeye ihtiyacınız yoktur. Derleyici bu
özelliği kullanarak nesneyi doğrudan dışarıdaki geri dönüş değerinin
bellek yerine inşa eder. Bunun için gereken sadece normal bir yapıcı işlev
çağrımıdır (kopya yapıcı işlev çağrımı gerekmez) ve hiç bir zaman da,
yörel bir nesne oluşturulmadığı için, yıkıcı işlev çağrımı olmaz. Böylece
hiçbir maliyet olmadan, programcı uyanıklığı ile daha fazla verim elde
edilir.Bu "geri dönüş değeri iyileştirmesi" olarak adlandırılır.
sık rastlanmayan işleçler
Bazı ilave işleçler ötekilerden farklı yazım biçimleri ile bindirim yapılırlar.
Bunlardan biri operator[] işlecidir.
operator[] işleci mutlaka üye işlev olmalı ve tek değişkeni olmalıdır.
Bunun nedeni, operator[] işlecinin, çağrıldığında bir dizi gibi davranan
nesneyi işaret etmesidir. Bu işleçten genellikle dayanç geri döndürülür,
böylece eşit işaretinin sol yanında uygun şekilde kullanılabilir. Bu işlece
yaygın biçimde bindirim yapılır; konu ile ilgili örnekler ilerleyen
bölümlerde verilecektir.
Bundan başka, new ve delete işleçleri bilindiği gibi dinamik bellek
yerleşiminde kullanılmaktadır ve bu işleçlere birden çok değişik yolla
bindirim yapılabilir. new ve delete işleçlerinin ayrıntıları 13. bölümde
verilecektir.
virgül işleci
Virgül işleci, tanımının verildiği tipin nesnesinden sonra geldiğinde
çağrılır. Bununla beraber "operator," işlev değişken listeleri için
çağrılmaz, sadece virgüllerle ayrılmış açıktaki dış nesneler için çağrılır.Bu
işlecin uygulamada fazla kullanımı yokmuş gibi görünür; sadece
programlama dilinin tutarlılığı içindir. Şimdi vereceğimiz örnek bir
nesneden önce virgül ortaya çıktığında virgül işlevinin nasıl çağrıldığını,
hemen sonra göstermektedir.
//:B12:OverloadingOperatorComma.cpp
#include <iostream>
using namespace std ;
class After{
public:
const After& operator, (const After&)const{
cout<<"After::operator, ( )"<<endl ;
return *this ;
}
};
class Before{};
Before& operator, (int,Before& b){
cout<<"Before::operator, ( )"<<endl ;
return b ;
}
int main( ){
After a,b ;
a,b ; //virgül işleci çağrılıyor
Before c ;
1,c ; //virgül işleci çağrılıyor
}///:~
Global işlev soruda, nesneden önce virgül yerleşimine izin verir. Yukarıda
gösterilen kullanım oldukça belirsiz ve sorunludur. Daha karmaşık
açıklama parçalarında virgülle ayrılmış listeler kullanılmasına rağmen,
birçok durumda kullanımı maharet ister.
operator->
operator-> işleci genellikle bir nesneyi göstergeçle açıklamak için
kullanılır. Zira böyle bir nesne bilinen göstergeçten çok daha fazla
akıllıdır, ve bu tür nesneler genellikle "akıllı göstergeçler" olarak anılır.
Bunlar özellikle bir göstergeci emniyete almak için bir sınıf bir göstergecin
etrafında sarmalanmak istenirse ya da, yineleyicinin ortak kullanımında
çok yararlıdır. Yineleyeci bir nesnedir, öteki nesnelerin topluluğuna/kabına
gidip her seferinde bir nesne seçer.Ve bu işlemi de kab uygulamasına
doğrudan erişmeden gerçekler. (Kablar ve yineleyicileri genellikle sınıf
kütüphanelerinde bulacaksınız.Mesela standart C++ kütüphanesi gibi.)
Bir göstergeç dayançtan kurtama işleci mutlaka üye işlev olmalıdır.
Benzeri olmayan ve ek koşulları vardır; mutlaka nesne geri döndürmelidir,
ki ayrıca bir göstergeç dayançtan kurtarma işleci olmalı ya da, bir
göstergeç döndürmelidir ve bu, göstergeç dayançtan kurtarma işleç okunun
işaret ettiğini seçmek için kullanılabilmelidir. Şimdi basit bir örnek
verelim;
//:B12:SmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std ;
class Obj{
static int i,j ;
public:
void f( ) const{cout<<i++<<endl ;}
void g( ) const{cout<<j++<<endl; }
};
//static üye tanımları
int Obj::i=11 ;
int Obj::j=48 ;
//Kab
class ObjContainer{
vector<Obj*>a ;
public:
void add(Obj* obj) {a.push_back(Obj);}
friend class SmartPointer ;
};
class SmartPointer{
ObjContainer& oc ;
int index ;
public:
SmartPointer(ObjContainer& objc) : oc(objc){
index=0 ;
}
//geri dönüş değeri liste sonunu gösterir
bool operator++( ){//önek
if(index>=oc.a.size( ))return false ;
if(oc.a[++index]==0)return false ;
return true ;
}
bool operator++(int){//arkaek
return operator++( ) ; //önek sürümünü kullan
}
Obj* operator->( ) const {
require(oc.a[index]!=0,"zero value"
"returned by SmartPointer::operator->( )") ;
return oc.a[index] ;
}
};
int main( ){
const int sz=10 ;
Obj o[sz] ;
ObjContainer oc ;
for(int i=0;i<sz;i++)
oc.add(&o[i]) ; // onu doldur
SmartPointer sp(oc) ; //bir yineleyici oluştur
do{
sp->f( ) ; //göstergeç dayançtan kurtarma işleç çağrısı
sp->g( ) ;
}while(sp++) ;
}///:~
Bu programda Obj sınıfı, işlemlenen nesneleri tanımlamaktadır. f( ) ve g( )
işlevleri static veri üyelerini kullanarak ilginç değerler yazdırır. İlgili
nesneler ObjContainer tipinin kabındaki add( ) işlevi kullanılarak
saklanır. ObjContainer sınıfı bir göstergeç dizisi gibi görünür, fakat
farkedeceğiniz gibi göstergeçleri geri almak olası değildir.
SmartPointers, friend sınıf olarak bildirilir, böylece kabın içine bakma
iznine sahip olur. SmartPointer sınıfı meraklı bir göstergeçe çok fazla
benzer- operator++ işleci kullanarak onu yukarı hareket ettirmek
mümkündür( ayrıca operator-- işlecini de tanımlamak mümkündür),
göstergecin işaretlediği kabın sonundan öteye geçemez, ve onun gösterdiği
değeri (göstergeç dayançtan kurtarma işleci aracılığı ile) üretir.Burada
dikkat edilmesi gereken SmartPointer sınıfının üretildiği kaba uygun
olmasıdır; bilinen normal göstergeçlere benzemeyen bir biçimde "genel
amaçlı" bir akıllı göstergeç bulunmamasıdır. Akıllı göstergeçlerle ilgili
olan yineleyiciler (iterators) hakkında son bölümde ayrıntılı bilgiler
verilecek.
main( ) işlevinde oc kabı Obj nesneleri ile bir kez doldurulur, bir tane
SmartPointer sp oluşturulur. Akıllı göstergeç çağrıları aşağıdaki satırlarda
olur;
sp->f( ) ; //akıllı göstergeç çağrımları
sp->g( );
Bu arada görüleceği üzere sp gerek f( ) gerekse g( ) üye işlevlerine sahip
olmasa da, göstergeç dayançtan kurtarma işleci, bu işlevleri
SmartPointer::operator-> tarafından geri döndürülen Obj* için otomatik
olarak çağırır. İşlev çağrısının emniyetli biçimde çalışması derleyici
denetimiyle sağlanır.
Göstergeç dayançtan kurtarma işleci normal göstergeçlerden çok daha
karmaşık olmasına karşın, amaç tamamen aynıdır; sınıfların kullanıcılarına
çok daha uygun yazım biçimleri sağlamak.
yuvalanmış yineleyici (nested iterator)
Sınıfların servisleri olarak, kendilerinde yuvalanmış "akıllı göstergeç"
veya, "yineleyici" sınıfları görmek oldukça yaygındır. Önceki örneği
ObjContainer sınıfında yuvalanmış SmartPointer sınıfı ile yeniden
yazmak olasıdır.
//B12:NestedSmartPointers.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std ;
class Obj{
static int i,j ;
public:
void f( ) {cout<<i++<<endl ;}
void g( ) {cout<<j++<<endl ;}
};
//static üye tanımları
int Obj ::i=11 ;
int Obj::j=48 ;
//kab
class ObjContainer{
vector<Obj*> a ;
public:
void add(Obj* obj){a.push_back(obj) ;}
class SmartPointer ,
friend SmartPointer ,
class SmartPointer{
ObjContainer& oc ;
unsigned int index ;
public:
SmartPointer(ObjContainer& objc) : oc(objc){
index=0 ;
}
//geri dönüş değeri liste sonunu gösterir
bool operator++( ){//önek
if(index>=oc.a.size( ))return false ;
if(oc.a[++index]==0)return false ;
return true ;
}
bool operator++(int){//arkaek
return operator++( ) ; //önek sürümünü kullan
}
Obj* operator->( ) const{
require(oc.a[index]!=0,"Zero value"
"returned by SmartPointer::operator->( )") ;
return oc.a[index] ;
}
};
//akıllı göstergeç üreten işlev, ObjContainer'in baş kısmını işaret eder
SmartPointer begin( ){
return SmartPointer(*this) ;
}
};
int main( ){
const int sz=10 ;
Obj o[sz] ;
ObjContainer oc ;
for(int i=0;i<sz;i++)
oc.add(&o[i]) ; //doldur
ObjContainer::SmartPointer sp=oc.begin( ) ;
do{
sp->f( ) ; //göstergeç dayançtan kurtarma işlecini çağırma
sp->g( ) ;
}while(++sp) ;
}///:~
Gerçek bir sınıf yuvalanmasından başka, burada sadece iki farklılık
bulunmaktadır. İlki sınıfın bildirimindedir, friend olabilir.
class SmartPointer ;
friend SmartPointer ;
Derleyici, mutlaka friend olduğu sınıfın var olduğunu önceden bilmek
zorundadır.
İkinci fark, ObjContainer'de bulunan begin( ) üye işlevinden gelir. Bu
ObjContainer dizisinin başlangıcını işaretleyen SmartPointer'i üretir.
Gerçekte bir rahatlık sağlamasına rağmen, değerlidir de zira standart C++
kütüphanesinin biçimlerine uyan davranışlar sergiler.
Operator->*
operator->* ikili işleçtir ve bütün diğer ikili işleçlerin gösterdiği
özellikleri taşır. Önceki bölümde anlatılan, yerleşik "üyeye-göstergeç"
yazım biçiminin sağladığı davranışlar taklid edilmek istendiğinde
kullanılır.
Üyeye-göstergeç dayançtan kurtarma işleçlerinden operator->, de olduğu
gibi aynı biçimde, "akıllı göstergeçleri" imgeleyen bazı nesnelerle birlikte
kullanılır. Burada verilecek olan örnek oldukça basit olmasına rağmen,
konunun anlaşılmasında çok yararlıdır. operator->* işlecinin
tanımlanmasındaki incelik, çağrılan üye işlev değişkenleri ile çağrılabilen
operator( ) için, mutlaka nesne geri döndürebilmelidir.
operator( ) işlev çağrısı üye işlev olmak zorundadır. İzin verdiği değişken
sayısı bellidir.Ve nesnenin aynı gerçek bir işlev gibi görünmesini sağlar.
Farklı değişkenlerle çok sayıda bindirilmiş operator( ) işlevi
tanımlanabilmesine rağmen, çoğunlukla sadece tek işleme sahip tipler
kullanılmaktadır veya en azından özellikle biri göze çarpar. İkinci ciltte
"işlev nesneleri" üretmek için işlev çağrı işleçleri kullanan standart C++
kütüphanesi anlatılacaktır.
operator->* oluşturmak için mutlaka içinde operator( ) işlevi bulunan bir
sınıf kurmak gerekir ve operator->* geridönüşünün nesne tipini bu sınıf
belirler. Bu sınıf bir şekilde operator( ) işlevi çağrıldığında, mutlaka
gereken bilgiyi yakalayabilmelidir (otomatik olarak yapılır), üyeye
göstergeç nesne için, dayançtan kurtarılır. Aşağıdaki örnekte
FunctionObject yapıcı işlevi, nesneye göstregeci ve üye işleve göstergeci,
yakalar ve bellekte saklar. ve daha sonra operator( ) işlevi bunları
kullanarak gerçek "üyeye-göstergeç " çağrısını yapar.
//:B12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std ;
class dog{
public:
int run(int i) const{
cout<<"run\n" ;
return i ;
}
int eat(int i) const{
cout<<"eat\n" ;
return i ;
}
int sleep(int i) const{
cout<<"Horr\n" ;
return i ;
}
typedef int (Dog::*PMF)(int) const ;
//operator->* bir nesne oluşturmalıdır
//operator( ) işlevine sahip olmalıdır
class FunctionObject{
Dog* ptr ;
PMF pmem ;
public:
//üye göstergeç ve nesne göstergecini saklayın
FunctionObject(Dog* wp,PMF pmf)
:ptr(wp),pmem(pmf){
cout<<"FunctionObject yapıcı işlevi\n" ;
}
//nesne göstergecini ve üye göstergeci kullanarak çağrı yapınız
int operator( )(int i) const{
cout<<"FunctionObject::operator( )\n" ;
return (ptr->pmem)(i) ; //çağrı yapılışı
}
};
FunctionObject operator->*(PMF pmf){
cout<<"operator->*"<<endl ;
return FunctionObject(this,pmf) ;
}
};
int main( ){
Dog w ;
Dog::PMF pmf=&Dog::run ;
cout<<w->*pmf(1)<<endl ;
pmf=&Dog::eat ;
cout<<w->*pmf(2)<<endl ;
pmf=&Dog:: sleep ;
cout<<w->*pmf(3)<<endl ;
}//:~
Dog sınıfında üç tane üye işlev vardır, hepsinde int değişkenler bulunur ve
geriye int döndürür. Dog üye işlevleri için üyeye göstergecin tanımını
basitleştirmek amacı ile PMF typedef olarak belirlenir.
Bir FunctionObject oluşturulur ve operator->* tarafından geri
döndürülür. Burada operator->* ,hem (this) için çağrılan üyeye göstergeç
nesnesini, hemde üyeye göstegeci bilir. Ve onları değerleri saklayan
FunctionObject yapıcı işlevine aktarır. operator->* çağrıldığı zaman,
derleyici derhal istenileni yapar ve operator->* geri dönüş değeri için,
operator->* deki değişkenleri aktararak operator( ) işlevini çağırır.
FunctionObject::operator( ) değişkenleri alır ve daha sonra bellekte
bulunan, üyeye göstergeç ile nesne göstergecini kullanarak "gerçek"üyeye
göstergeci dayançtan kurtarır.
Burada yapılan işleme dikkat edilmelidir, aynı operator->, da olduğu gibi,
kendiniz operator->* çağrı ortasına yerleştirme yapıyorsunuz. Bu size,
gerektiğinde ek işlemler yapmanıza izin verir.
Burada uygulaması verilen operator->* mekanizması, sadece değişkenleri
ve geri dönüşü int olan üye işlevler için çalışır. Bu sınırlayıcı durum, farklı
her durum için bindirilmiş mekanizmalar oluşturmaya çalıştığınızda,
engelleyici unsur gibi görünür. Fakat allahtan ki C++ daki template
mekanizmaları (kitabımızın son bölümünde anlatılacak) bu tür sorunları
çözmek için tasarlanmışlardır.
Bindirim yapılamayan işleçler
Eldaki bazı işleçlere bindirim yapılamaz. Bunun genel amacı güvenliği
sağlamaktır. Eğer bu işleçlere bindirim yapılabilseydi, o zaman bazı şeyler
tehlikeye atılmış olur, ya da güvenlik mekanizması kırılır, bazı şeyler daha
zor yerine getirilir, veya varolan uygulama karışırdı.
**Üye seçim operator. (nokta). Şu sıralarda sınıf üyeleri için nokta, bir
anlam taşımaktadır, fakat eğer bindirime uğratılsaydı, o zaman üyelere
normal yollarla erişilemezdi; onun yerine göstergeç ve ok kullanılırdı
operator-> .
**Üye dayanç kurtarma operator.* göstergeci, operator. deki benzer
nedenle bindirim yapılamaz.
**Üs işleci bulunmamaktadır. En yaygın üs işleci Fortran dilinden
kaynaklanan operator** dir. Bölümleme sorunlarından dolayı zorluklar
yaratır. Ayrıca C dilinde üs işleci bulunmamaktadır, bundan dolayı C++
dilinin de ihtiyacı yok görünmektedir, çünkü işlev çağrımı yolu ile bunu
çözmek münkündür. Üs işleci için uygun bir işaret belirlemek olasıdır,
fakat böyle bir işlecin eklenmesi derleyicinin karmaşıklığını arttırır.
**Kullanıcı tanımlı işleçler bulunmaz. Yani dilin kendinde bulunan
işleçlerin dışında yeni bir işleç uydurulamaz. Bunun nedeni, işleç
önceliğinin belirlenmesi sorunudur. Başka bir nedende sorun yaratmama
gereksinimidir.
**İşleç öncelik kuralları değiştirilemez. Bunlar hatırlanması mecbur katı
kurallar olup, kullanıcının oynamasına izin verilmez.
Üye olmayan işleçler
Daha önce anlatılan örnekler de, işleçlerin bazıları üye, bazıları da üye
değillerdi. Bunlar çok büyük farka yolaçmış gibi gözükmemektedir. Bu da
genellikle akla şu soruyu getirmektedir; "hangisini seçmem lazım?"
Genellikle eğer fark etmeyecekse, işleç ve onun sınıfı arasındaki ilişkiyi
vurgulamak için üye olmaları lazımdır. Uygulama, sol-yan işleneni her
zaman bu sınıfın bir nesnesi olduğunda, gayet iyi çalışır.
Bazı durumlarda sol-yan işleneninin, başka bazı sınıfların bir nesnesi
olması istenir. Bunun yaygın olarak görüldüğü yer << ve >> işleçlerinin
iostream'ler için bindirime uğratıldığı durumlardır. iostream'ler temel
C++ kütüphanelerinden olduğundan, kullanıcı kendi sınıfları için bunları
bindirime uğratmak isteyebilir. Bu süreç programcı olmaya niyetlenenler
için ezberlenmeye değer.
//:B12:IostreamOperatorOverloading.cpp
//üye olmayan bindirilmiş işleçler örneği
#include "../require.h"
#include <iostream>
#include <sstream> //string streamleri
#include <cstring>
using namespace std ;
class IntArray{
enum{sz=5};
int i[sz] ;
public:
IntArray( ){memset(i,0,sz* sizeof(*i));}
int& operator[] (int x){
require(x>=0&&x<sz,
"IntArray::operator[] out of range");
return i[x] ;
}
friend ostream&
operator<<(ostream& os, const IntArray& ia);
friend istream&
operator>>(istream& is, IntArray& ia) ;
};
ostream&
operator<<(ostream& os, const IntArray& ia){
for(j=0;j<ia.sz;j++){
os<<ia.i[j] ;
if(j!=ia.sz-1)
os<<", " ;
}
os<<endl ;
return os ;
}
istream& operator>>(istream& is, IntArray& ia){
for(int j=0;j<ia.sz;j++)
is>>ia.i[j] ;
return is ;
}
int main( ){
stringstream input("48 35 50 92 100") ;
IntArray I ;
input>>I ;
I[4]=-1 ; //bindirilmiş işleç [] kullan
cout<<I ;
}//:~
Sınıfta ayrıca operator[] bindirilmiş işleci, dizideki değere uygun dayanç
geri döndürür. Zira bir daayanç geri döner, açıklamaya bakalım;
I[4]=-1 ;
Göstergeç kullanımından sadece daha medeni değil, aynı zamanda istenen
etkiyide sağlamaktadır.
Bindirilmiş kaydırma işleçlerinin dayançla aktarılması ve geri
döndürülmesi önemlidir; zira böylece dışarıdaki nesnelerde bu işlemlerden
etkilenir. Aşağıdaki açıklamalarda görüldüğü gibi işlev tanımlarında ;
os<<ia.i[j] ;
varolan bindirilmiş işleç işlevleri çağrılır (yani, <iostream> de
tanımlanmış). Bu durumda çağrılan işlev ostream& operator<<
(ostream&,int) dir, zira ia.i[j], int'e çözümlenir.
Bütün işlemler istream veya ostream'de bir defa yerine getirildiğinde,
daha karmaşık açıklamalarda kullanmak için geri döndürülürler.
main( ) de iostream'in yeni bir tipi kullanılmıştır; stringstream
(<sstream> olarak bildirilmiştir). Bu sınıf string alır (burada gösterildiği
gibi char dizisinden oluşturulabilir) ve onu iostream'e çevirir. Yukarıdaki
örnek, kaydırma işleçlerinin dosya açmaksızın ve komut satırına veri
yazmaksızın da denenebileceğini gösterir.
Bu örnekte gösterilen biçim, yerleştirici ve çıkarıcı için bir standarttır. Bu
işleçleri kendi sınıflarınız için oluşturmak isterseniz, yukarıdaki işlev
imzalarını ve geri dönüş tiplerini kopyalayın ve gövde biçimini izleyin.
İpuçları
Rob Murray, üye olanlar ve üye olmayanlar arasında seçim yapmak için
şu ipuçlarını vermiştir;
İŞLEÇ
KULLANIM
ÖNERİLEN
Bütün tek-yanlı işleçler -------------------------------- > üye
= ( ) [] -> ->*
---------------------------------> mutlaka üye
olmalı
+= -= *= /= ^= &= |= %= <<= >>= ---------------------> üye
Öteki bütün iki-yanlı işleçler -----------------------------> üye olmamalı
Bindirim Ataması
Yeni C++ programcılarını en çok şaşırtan konulardan biri atamalardır.
Böyle olması hiç şüphesiz = işaretinin programlamada en temel işlemi
göstermesidir. Makine düzeyinde yapılan işlem sağyandaki değerin
kaydediciye kopyalanmasıdır. Buna ek olarak, bazı durumlarda kopya
yapıcı işlev (11. bölümde anlatıldı) = işareti ile çağrılır.
MyType b ;
MyType a=b ;
a=b ;
İkinci satırda a nesnesi tanımlanıyor. Daha önce nesnenin bulunmadığı
yerde, yeni bir nesne oluşturuluyor. Şimdiye kadar öğrendiklerinizden ilk
değer atamalarında, C++ derleyicisinin ne kadar korunaklı olduğunu
gördünüz, ve ilk defa bir nesne tanımlandığı zaman, oraya bir yapıcı
işlevin çağırılmak zorunda olduğunu öğrendiniz. Fakat hangi yapıcı işlev?.
a nesnesi MyType sınıfının b nesnesinden oluşturuluyor. İşte burada tek
bir seçenek bulunmaktadır; kopya yapıcı işlev. Burada eşit (=) işareti olsa
bile, kopya yapıcı işlev çağrılır.
Üçüncü satırda olanlar ise oldukça farklıdır. Eşit işaretini sol yanında yer
alan a nesnesinin ilk değer ataması önceden yapılmıştı. Açıkça
görülmektedir ki önceden oluşturulan bir nesne için yapıcı işlev çağrılmaz.
Bu durumda a için MyType::operator= çağrılır, ve sağ yanda yer alan
değişken değerini kullanır. (farklı sağ yan değişken tipleri için, çok sayıda
operator= işlevi kullanbilirsiniz)
Bu davranış tarzı sadece kopya yapıcı işlevlerle sınırlı değildir. Nesneye
ilk değer ataması normal yapıcı işlev çağrımı yerine eşit (= ) işareti ile
yapıldığında, derleyici sağ yanda yer alan değeri kabul edecek bir yapıcı
işlev arar;
//:B12:CopyingVsInitialization.cpp
class Fi{
public:
Fi( ){}
};
class Fee{
public:
Fee(int){}
Fee(const Fi&){ }
};
int main( ){
Fee fee=1 ; // Fee(int)
Fi fi ;
Fee fum=fi ; // Fee(Fi)
}///:~
Eşit (=) işareti ile uğraşırken bu farkı akılda tutmalıyız. Eğer nesne henüz
oluşmamışsa, ilk değer ataması istenebilir, aksi taktirde atama işleci
operator= işleci kullanılır.
Hatta ilk değer atama işlemlerini eşit (=) işareti kullanmadan, onun yerine,
uygun yapıcı işlevlerle yapmak her zaman daha iyidir.
Yukarıda yeralan eşit işaretli iki yapı o zaman şöyle olur;
Fee fi(1) ;
Fee fum(fi) ;
Bu yolla programı okuyanları da karışıklıktan kurtarırsınız.
operator= İşlecinin davranış biçimi
Integer.h ve Byte.h program parçalarında operator= işleci sadece üye
işlev olabilir. Eşit (=) işaretinin sol yanında yer alan nesneye özel olarak
bağlanırlar. Eğer operator= işleci global olarak tanımlanbilseydi, o zaman
yerleşik '=' işareti yeniden tanımlamak gerekirdi.
int operator=(int MyType) //global 0 işaretine izin verilmez
Derleyici bunu sağlamak için, operator= işlecini üye işlev yaptırır.
operator= oluşturduğunuz zaman, mutlaka sağ yanda yer alan nesneden
gereken bilgiler o anki nesneye ( yani operator= işlecinin çağrıldığı
nesne) aktarılır. Ve böylece sınıf için atama gerçeklenir. Basit nesneler için
bu açıkça bellidir.
//:B12:SimpleAssignment.cpp
//basit atama operator=( )
#include <iostream>
using namespace std ;
class Value{
int a,b ;
float c;
public:
Value(int aa=0,int bb=0,float cc=0.0)
:a(aa),b(bb),c(cc) {}
Value& operator=(const Value& rv){
a=rv.a ;
b=rv.b;
c=rv.c ;
return *this ;
}
friend ostream&
operator<<(ostream& os, const Value& rv){
return os<<"a= "<<rv.a<<",b= "<<rv.b
<<",c= "<<rv.c ;
}
};
int main( ){
Value a,b(1,2,4.4) ;
cout<<"a= "<<endl ;
cout<<"b= "<<endl ;
a=b ;
cout<<"a after assignment : "<<a<<endl ;
}///:~
Burada eşit(=) işaretinin sağ yanında yer alan nesneye, sol yanda yer alan
nesnenin bütün ögeleri kopyalanır, ve kendine bir dayanç geri döndürür,
böylece daha karmaşık bir açıklama geliştirilmiş olur.
Bu örnekte yaygın rastlanan bir hata bulunmaktadır. Aynı tipin iki
nesnesini atadığınız zaman, her zaman önce kendi kendine atamaya
bakmalısınız; nesne kendine atama yapmaktamı?. Bazı durumlarda böyle
atamaları yaparsanız, herhangi bir zararı olmayabilir.Fakat eğer
değişiklikler sınıf uygulamasında yapılırsa, fark oluşturabilir. Eğer bunu
bir el alışkanlığı olarak yapmazsanız, unutursunuz ve bulunması zor
hatalar üretirsiniz.
Sınıflardaki göstergeçler
Nesneler çok basit olmadığı zaman ne olur?. Örnek olarak nesne, başka
nesneleri gösteren göstergeçlere sahipse ne olur?. Basit bir şekilde
göstergeç kopyalamanın anlamı bellekte aynı yeri gösteren iki tane nesne
ile sonlanmadır. Benzeri durumlarda kendi hesaplarınızı iyi yapmalısınız,
bu sizin sorumluluğunuzda.
Bu sorunun çözümü için iki yaklaşım bulunmaktadır. En basit teknik,
atamaları, veya kopya yapıcı işlevlerle, yaptığınız zaman göstergeçlere
karşılık gelenleri kopyalamaktır. Bu çok basittir.
//:B12:CopyingWithPointers.cpp
//atama ve kopya yapıcı işlev kullanma esnasında
//göstergeçin işaretlediğini yineleme yolu ile
//göstergeç öteki ad sorununu çözmek
#include "../require.h"
#include <string>
#include <iostream>
using namespace std ;
class Dog{
string nm ;
public:
Dog(const string& name):nm(name){
cout<<"Creating Dog : "<<*this<<endl ;
}
//kopya yapıcı işlev ve operator= oluşumu doğru
//Dog göstergecinden bir Dog oluştur
Dog(const Dog* dp,const string& msg)
:nm(dp->nm+msg){
cout<<"kopyalanmıs Dog"<<*this<<"den"
<<*dp<<endl ;
}
~Dog( ){
cout<<"Deleting Dog"<<*this<<endl ;
}
void rename(const string& newName){
nm=newName;
cout<<"Dog renamed to"<<*this<<endl ;
}
friend ostream&
operator<<(ostream& os,const Dog& d){
return os<<"["<<d.nm<<"]";
}
};
class DogHouse{
Dog* p ;
string houseName;
public:
DogHouse(Dog* dog,const string& house)
:p(dog),houseName(house){}
DogHouse(const DogHouse& dh)
:p(newDog(dh.p,"copy-constructed")),
houseName(dh.houseName
+"copy-constructed"){}
DogHouse& operator=(const DogHouse& dh){
//kendi kendine atamayı gözle
if(&dh!=this){
p=new Dog(dh.p,"assigned") ;
houseName=dh.houseName+"assigned" ;
}
return *this ;
}
void renameHouse(const string& newName){
houseName=newName ;
}
Dog* getDog( ) const{return p;}
~DogHouse( ){delete p;}
friend ostream&
operator<<(ostream& os,const DogHouse& dh){
return os<<"["<<dh.houseName
<<"] contains"<<*dh.p ;
}
};
int main( ){
DogHouse fidos(new Dog("Fido"),"FidoHouse") ;
cout<<fidos<<endl ;
DogHouse fidos2=fidos ; //kopya yapıcı işlev
cout<<fidos2<<endl ;
fidos2.getDog( )->rename("Nokta") ;
fidos2.renameHouse("NoktaHouse") ;
cout<<fidos2<<endl ;
fidos=fidos2 ; //atama
cout<<fidos<<endl ;
fidos.getDog( )->rename("Dev") ;
fidos2.renameHouse("DevHouse") ;
}///:~
Dog köpek isimlerini string tipi içinde tutan basit bir sınıftır. Genellikle
çağrıldıkları zaman Dog'a ne olduğunu yapıcı ve yıkıcı işlevlerin
yazdıklarından öğrenebilirsiniz. İkinci yapıcı işlev biraz kopya yapıcı
işlevi andırmakta fakat buradaki fark Dog dayanç yerine göstergeç
almakta. Ve ayrıca ikinci değişken olarak Dog adına ek olan bir ileti
bulunmaktadır. Bu ileti program davranışını izlemede yardımda kullanılır.
Görebileceğiniz gibi, bir üye işlev bilgi yazdırırken doğrudan bilgiye
erişerek değil, *this cout'a yollanarak bu gerçeklenir. ostreamoperator<<
çağrılarak yapılan işlemin değeri, Dog bilgisini yeniden biçimlendirmek
isterseniz, ("[" ve "]" eklemesi ile gerçekleştirdik) sadece tek bir yere
gereksinim bulunmaktadır.
Bir DogHouse bir Dog* bulundurur ve her zaman tanımlamak zorunda
olunan dört işlevde sınıfınızın göstergeçleri yüzünden bulunur; bütün
normal yapıcı işlevler, kopya yapıcı işlevler, operator= ve yıkıcı işlevler.
Burada kendi kendine atamayı denetleyen operator= in bulunma nedeni
nasıl kullanıldığını öğrenmek içindir, mutlaka gerekli olmamasına rağmen.
Böylece eğer kodda değişiklik yaparsanız, kendine atamayı denetleme
olasılığını unutmanızı engeller.
Dayanç sayma
Yukarıdaki örnekte, kopya yapıcı işlev ve operator=, göstergecin işaret
ettiği yeni bir kopya oluştururlar, ve yıkıcı işlev daha sonra onu siler. Fakat
bir şekilde nesneniz büyük bellek gerektirir veya büyük ilk değer atama
maliyeti yüklerse, bu kopyalamadan kurtulmak istersiniz. Sorunu çözmede
kullanılan en yaygın yaklaşım dayanç saymadır. İşaret edilen nesneye zeka
eklenerek, ona kaç tane nesneyi işaret ettiği bilmesi sağlanır. Bu da kopya
yapma veya atama, varolan bir nesneye ikinci bir göstergeç iliştirmek ve
dayanç sayısını bir arttırmaktır. Yıkıcılık ise, dayanç sayısını bir azaltmak,
ve dayanç adedi sıfır olduğu zaman nesneyi ortadan kaldırmaktır.
Ama eğer nesneye yazmak isterseniz ne olur?. (Yukarıdaki örnekte Dog) .
Birden fazla nesne bu Dog'u kullanabilir, böylece kendinizin olduğu gibi,
yakın olmadıkları halde başkasının Dog'unu da değiştirirsiniz. Bu yeniden
adlandırma (takma ad) sorununu çözmek için, yazım üzerine kopyalama
(copy-on-write) denilen bir teknik kullanılır. Bir bellek bütünü yazılmadan
önce, mutlaka daha evvel başka biri tarafından kullanılmadığından emin
olunmalıdır.
Dayanç sayısı birden büyükse, yazmadan evvel mutlaka bellek bütününün
şahsi bir kopyasını kendiniz için almalısınız, böylece başkalarının tarlasına
da girmemiş olursunuz. Dayanç sayımı ve yazıma kopyalamaya basit bir
örnek verelim;
//:B12:ReferenceCounting.cpp
//Dayanç sayımı, yazıma kopyalama
#include "../require.h"
#include <string>
#include <iostream>
using namespace std ;
class Dog{
string nm ;
int refcunt ;
Dog(const string& name)
:nm(name),refcount(1){
cout<<"Creating Dog"<<*this<<endl ;
}
//Atamayı engelle
Dog& operator=(const Dog& rv) ;
public:
//Dogs sadece kümede (heap)oluşabilir
static Dog* make(const string& name){
return new Dog(name) ;
}
Dog(const Dog& d)
:nm(d.nm+"copy"),refcount(1){
cout<<"Dog kopya yapici işlev"<<*this<<endl ;
}
~Dog( ){
cout<<"Dog silme islemi"<<*this<<endl ;
}
void attach( ){
++refcount ;
cout<<"attached Dog: "<<*this <<endl ;
}
void detach( ){
require(refcount!=0) ;
cout<<"Detaching Dog :"<<*this<<endl ;
//kimse kullanmıyorsa nesneyi yok et
if(--refcount==0)delete this ;
}
//koşullara göre Dog kopyala
//Dog değişmeden evvel çağrı yap
//sonuç göstergecini *Dog işaretle
Dog* unalias( ){
cout<<"Unaliasing Dog : "<<*this<<endl ;
//yeniden adlandırma yapılmadı ise yineleme
if(refcount==1)return this ;
--refcount ;
//yinelemek için kopya yapıcı işlevi kullan
return new Dog(*this) ;
}
void rename(const string& newName){
nm=newName ;
cout<<" Dog yeniden adlandırıldı: "<<*this<<endl ;
}
friend ostream&
operator<<(ostream& os, const Dog& d){
return os<<"["<<d.nm<<"],rc= "
<<d.refcount ;
}
};
class DogHouse{
Dog* p ;
string houseName ;
public:
DogHouse(Dog* dog, const string& house)
:p(dog),houseName(house){
cout<<"Created DogHouse: "<<*this<<endl ;
}
DogHouse(const DogHouse& dh)
:p(dh.p),
houseName("copy-constructed"+
dh.houseName){
p->attach( ) ;
cout<<"DogHouse copy constructor : "
<<*this<<endl ;
}
DogHouse& operator=(const DogHouse& dh){
//kendine atamayı denetle
if(&dh!=this){
houseName=dh.houseName+"assigned" ;
//ilk kullanılanı sil
p->detach( ) ;
p=dh.p ; //kopya yapıcı işlev benzeri
p->attach( ) ;
}
cout<<"DogHouse operator= : "
<<*this<<endl ;
return *this ;
}
//refcount değerini bir azalt, duruma göre yoket
~DogHouse( ){
cout<<"DogHouse yıkıcı işlevi: "
<<*this<<endl ;
p->detach( ) ;
}
void renameHouse(const string& newName){
houseName=newName ;
}
void unalias( ){p=p->unalias( );}
//yazıma kopyala göstergeç içeriğinin değiştiği
//herhangi durumda önce onu unalias et
void renameDog(const string& newName){
unalias( ) ;
p->rename(newName) ;
}
//ya da başka birinin erişimine izin verilidiği zaman
Dog* getDog( ){
unalias( ) ;
return p ;
}
friend ostream&
operator<<(ostream& os,const DogHouse& dh){
return os<<"["<<dh.houseName
<<"] contains "<<*dh.p ;
}
};
int main( ){
DogHouse
fidos(Dog::make("Fido"),"FidoHouse"),
noktalar(Dog::make("Nokta"),"NoktaHouse");
cout<<"entering copy construction"<<endl ;
DogHouse bobs(fidos) ;
cout<<"after copy constructing bobs"<<endl ;
cout<<"fidos: "<<fidos<<endl ;
cout<<"noktalar: "<<noktalar<<endl ;
cout<<"bobs: "<<bobs<<endl ;
cout<<"entering noktalar=fidos "<<endl ;
noktalar=fidos ;
cout<<" after noktalar=fidos "<<endl ;
cout<<"noktalar:"<<noktalar<<endl ;
cout<<"kendine atamaya giriş:"<<endl ;
bobs=bobs ;
cout<<"kendine yapılan atama sonrası:"<<endl ;
cout<<"bobs:"<<bobs<<endl ;
//aşağıdaki satırları yorumlayın
cout<<"yeniden adlandırmaya giriş (\"Bob\")"<<endl ;
bobs.getDog( )->rename("Bob") ;
cout<<"yeniden adlandırdıktan sonra (\"Bob\")"<<endl ;
}///:~
Dog sınıfı DogHouse tarafından işaret edilen nesnedir. İçinde dayanç
sayımı ve dayanç sayımını denetleyen, okuyan işlevler bulundurur.Ayrıca
içinde bulunan kopya yapıcı işlev ile varolan bir nesneden yeni bir Dog
oluşturulabilir.
Başka bir nesnenin kullandığını gösteren attach( ) işlevi Dog dayanç
değerini bir arttırır, detach( ) işlevi ise dayanç değerini bir azaltır. Dayanç
değeri sıfıra eşit olursa o zaman onu bir daha kimse kullanamaz, böylece
delete this diyerek üye işlev kendi nesnesini yok eder.
Herhangi bir değişiklik yapmadan önce (bir Dog'u yeniden adlandırmadan
önce), başka nesneler tarafından Dog'un kullanılmadığından emin olmak
gereklidir. Bunu gerçeklemek için DogHouse::unalias( ) çağrımı
yapmalıdır, bu Dog::unalias( ) ile geri verilir. Dayanç değeri bir olursa
(anlamı Dog'u işaret eden başka göstergeç yok demektir.) ikinci işlev,
varolan Dog göstergecini geri döndürür, fakat eğer dayanç sayısı birden
fazla ise Dog kopyası oluşur.
Kopya yapıcı işlev kendi bellek alanını oluşturma yerine, kaynak nesnenin
Dog'una Dog'u atar. O zaman bellek bütünün kullanan ek nesne nedeni ile,
Dog::attach( ) çağrılarak dayanç değeri bir arttırılır.
operator= eşit (=,) işaretinin sol tarafında hemen oluşturulmuş nesne ile
ilgili olup, Dog için detach( ) işlevini çağırarak mutlaka önce onu silmek
zorundadır, başka biri kullanmıyorsa eski Dog'u siler. Demeki operator=
işleci burada kopya yapıcı işlevi tekrarlıyor. Burada dikkat edilmesi
gereken önce aynı nesneyi kendine atama yapıyormusunuz, o algılanmaya
çalışılıyor.
Yıkıcı işlev detach( ) çağrısı yaparak Dog'u koşullu olarak ortadan
kaldırıyor.
Yazıma kopyalamayı (copy-on-write) gerçeklemek için, bellek bütününüze
yazma eyleminin mutlaka bütün unsurlarını denetleyebilmeniz gereklidir.
Örnek olarak renameDog( ) üye işlevi bellek bütününün değerini
değiştirmeye izin verir. Ama önce unaliasDog( ) yeniden adlandırılmış
Dog'un değişmesini engellemek için kullanılır (bir Dog birden fazla
DogHouse nesnesi ile birlikte onu işaret ederler). Ve eğer DogHouse
içinden Dog'a bir göstergeç bulundurma gereksinimi duyarsanız, önce o
göstergeci yeni adından kurtarırsınız.
main( ) işlevi belli işlevleri, dayanç değerini doğru sayması için onları
sınar; yapıcı işlev, yıkıcı işlev, kopya yapıcı ve operator=. Ayrıca
renameDog( ) işlevini çağırarak yazıma kopyalamayı sınar.
İşte çıktı aşağıda biraz ufak değişikliklerle verildi;
Creating Dog: [Fido], rc = 1
Created DogHouse: [FidoHouse]
contains [Fido], rc = 1
Creating Dog: [Spot], rc = 1
Created DogHouse: [SpotHouse]
contains [Spot], rc = 1
Entering copy-construction
Attached Dog: [Fido], rc = 2
DogHouse copy-constructor:
[copy-constructed FidoHouse]
contains [Fido], rc = 2
After copy-constructing bobs
fidos:[FidoHouse] contains [Fido], rc = 2
spots:[SpotHouse] contains [Spot], rc = 1
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 2
Entering spots = fidos
Detaching Dog: [Spot], rc = 1
Deleting Dog: [Spot], rc = 0
Attached Dog: [Fido], rc = 3
DogHouse operator= : [FidoHouse assigned]
contains [Fido], rc = 3
After spots = fidos
spots:[FidoHouse assigned] contains [Fido],rc = 3
Entering self-assignment
DogHouse operator= : [copy-constructed FidoHouse]
contains [Fido], rc = 3
After self-assignment
bobs:[copy-constructed FidoHouse]
contains [Fido], rc = 3
Entering rename("Bob")
After rename("Bob")
DogHouse destructor: [copy-constructed FidoHouse]
contains [Fido], rc = 3
Detaching Dog: [Fido], rc = 3
DogHouse destructor: [FidoHouse assigned]
contains [Fido], rc = 2
Detaching Dog: [Fido], rc = 2
DogHouse destructor: [FidoHouse]
contains [Fido], rc = 1
Detaching Dog: [Fido], rc = 1
Deleting Dog: [Fido], rc = 0
Çıktı üzerinde kaynak kodu izleyip çalışmak ve programla değişik
denemeler yapmak, bu tekniği derinliğine anlamanıza yardımcı olur.
Otomatik operator= oluşumu
Aynı tipin nesnesini diğer nesnesine atama herkes tarafından beklenen bir
etkinlik olduğu için yazılmadığı taktirde derleyici otomatik olarak
type::operator=(type) oluşturur. İşleç yazımının davranışı, otomatik
olarak oluşan kopya yapıcı işlevinki gibidir; sınıfta nesneler varsa (veya
kalıtım yolu ile başka sınıftan alınmış nesneler olabilir), böyle nesneler
için operator= işleci kendi kendini çağırır. Bunun adına üye kaynaklı
atama denilir. Örnek olarak;
//:B12:AutomaticOperatorEquals.cpp
#include <iostream>
using namespace std ;
class Cargo{
public:
Cargo& operator=(const Cargo&){
cout<<"inside Cargo::operator=( )"<<endl ;
return *this ;
}
};
class Truck{
Cargo b ;
};
int main( ){
Truck a,b ;
a=b ; // prints: "inside Cargo::operator=( )"
}
Truck için otomatik olarak üretilen operator=, Cargo::operator= i
çağırır.
Genellikle bu işlemi otomatik olarak derleyicinin yapması istenmez.
Sınıflarda ki bazı ayrıntılar (özellikle göstergeçler) sizin açık biçimde
operator= i oluşturmanızı gerektirir. Atama işlemini müşterinin yapmasını
gerçekten istemezseniz, o zaman operator= işlecini, private olarak
bildirmelisiniz.. (sınıf içinde değilse buna gerek yok)
Otomatik Tip Çevrimi
Gerek C ve gerekse C++ dillerinde derleyici, bir açıklama veya işlev
çağrısında uygun olmayan tip kullanıldığını gördüğünde otomatik olarak
tip çevrimini gerçekler. C++ dilinde kullanıcı tanımlı tipler için benzer
etkiyi, otomatik tip çevirici işlevler tanımlayarak başarabiliriz. Bu işlevler
iki şekilde önümüze gelir; yapıcı işlevin özel tipi ve bindirilmiş işleç.
Yapıcı işlev çevrimi
Yapıcı işlev tanımlanırken tek değişken olarak başka bir tipin nesnesini
( veya dayancını) alırsa, yapıcı işlev derleyiciye otomatik olarak tip
çevrimi yapma izni verir. Örnek olarak;
//:B12:AutomaticTypeConversion.cpp
//tip çevirme yapıcı işlevi
class One{
public:
One( ){}
};
class Two{
public:
Two(const One&){}
};
void f(Two){}
int main( ){
One one ;
f(one) ; //içinde One olan Two isteniyor
}///:~
Derleyici One nesnesi ile çağrılan f( ) işlevini gördüğü zaman, f( ) için
yapılan bildirime bakar ve onun Two istediğini görür. Daha sonra One'dan
Two elde etmenin bir yolu varmı arar, ve Two::Two(One) çağrılan yapıcı
işlevini bulur. Sonuç olarak elde edilen Two nesnesi f( ) işlevinde
kullanılır.
Uygulamada otomatik tip çevrimi, f( ) işlevinin bindirilmiş iki sürüm
tanımına gerek bırakmadı. Ama yine de buradaki maliyet gizli yapıcı
işlevin Two'ya çağrımıdır, f( )'e çağrıların verimliliği size bir şey ifade
ediyorsa tabii.
Yapıcı işlev çevrimini engelleme
Otomatik tip çevriminin yapıcı işlevle gerçeklendiği bazı durumlarda
sorunlar ortaya çıkabilir. Bundan sakınmak için, yapıcı işlevi explicit
(sadece yapıcı işlevlerle çalışır) anahtar kelimesi ile başlatmalıdır.
Aşağıdaki örnekte, yukarıda verilen Two sınıfının yapıcı işlevi
değiştirilmiştir;
//:B12:ExplicitKeyword.cpp
//explicit anahtar kelime kullanımı
class One{
public:
One( ){}
};
class Two{
explicit Two(const One&){}
};
void f(Two){}
int main( ){
One one ;
//! f(One) ; //otomatik çevrim yok
f(Two(one)) ; //tamam
}///:~
Two'nun yapıcı işlevini explicit yaparak derleyiciye, o özel yapıcı işlevi
kullanarak herhangi otomatik çevrim yapılamayacağı söyleniyor.( sınıftaki
explicit olmayan öteki yapıcı işlevler hala otomatik çevrim yapabilir).
Kullanıcı çevrimin olmasını isterse, kodlama dışarda yapılmalıdır.
Yukarıdaki kodda f(Two(one)), one'den Two tipinde geçici bir nesne
oluşturur, aynı önceki sürümde derleyicinin yaptığı gibi.
İşleç çevrimi
Otomatik tip çevrimini gerçeklemenin ikinci yolu işleç bindirimidir. O anki
tipi kullanan bir üye işlev oluşturulur, bu tipi istenen tipe çevirmek için,
operator anahtar kelimesinin ardına istenen tip eklenir. İşleç bindiriminin
bu uygulaması tektir, zira bir geri dönüş tipine gereksinim yoktur.- Geri
dönüş tipi bindirilmiş işlecin adıdır. İşte bir örnek ;
//:B12:OperatorOverloadingConversion.cpp
class Three{
int i ;
public:
Three(int ii=0,int=0):i(ii){}
};
class Four{
int x;
public:
Four(int xx):x(xx){}
operator Three( ) const{return Three(x);}
};
void g(Three){}
int main( ){
Four four(1) ;
g(four) ;
g(1) ; // Three(1,0) çağrılıyor
}///:~
Yapıcı işlev tekniği ile varış sınıfı çevrimi gerçekliyor, işleç tekniğinde ise,
kaynak sınıf çevrimi yapıyor. Yapıcı işlev tekniğinin değeri yeni bir sınıf
oluşturarak varolan sisteme yeni çevrim yolu eklemektir.Bununla birlikte,
tek değişkenli yapıcı işlev oluşumu her zaman istenmeyen, otomatik tip
çevrimini tanımlar (ve hatta birden fazla değişken olsa bile eğer geri kalan
değişkenler varsayılan değişkenler ise) .Bütün bunlara ilaveten, kullanıcı
tipten yerleşik tipe çevrimi yapıcı işlev ile yapan herhangi bir yol
bulunmaz; sadece işleç bindirimi ile böyle bir çevrim gerçeklenebilir.
Dönüşümlülük
Global sürümlerde üye işleçler yerine global bindirimli işleçlerin
kullanılmasının en geçerli sebeblerinden birisi, otomatik tip çevriminin
işlenenlerden herhangi birine uygulanabilmesidir, oysa üye nesnelerin
olduğu yerde solyan işleneni mutlaka uygun tipte olmalıdır. Her iki işlenen
çevrilmek istenirse, global sürümler çok sayıda koddan kurtulmuş olur.
İşte basit bir örnek daha;
//:B12:ReflexivityInOverloading.cpp
class Number{
int i ;
public:
Number(int ii=0):i(ii){}
const Number
operator+(const Number& n)const{
return Number(i+n.i) ;
}
friend const Number
operator-(const Number&, const Number&);
};
const Number
operator-(const Number& n1,
const Number& n2){
return Number(n1.i-n2.i) ;
}
int main( ){
Number a(48),b(10) ;
a+b ; //ok
a+1 ; //ikinci değişken Number yapıldı
//!1+b ; //yanlış ilk değişken Number tipi değil
a-b ; //ok
a-1 ; //ok ikinci arg Number tipi yapıldı
1-a ; //birinci arg Number yapıldı
}///:~
Number sınıfı hem operator+ üyesini hem de friend operator- barındırır.
Bunun nedeni yapıcı işlevin tek int değişkeni almasıdır, haliyle uygun
koşullarda bir int otomatik olarak Number'e çevrilebilir. main( ) işlevinin
içinde bir Number'e bir diğer Number eklenebilir zira, bindirilmiş işlece
tam bir uyum vardır. Ayrıca derleyici a+ ve int tarafından takip edilen bir
Number gördüğü zaman Number::operator+ üye işlevine uyumlu
yapabilir ve yapıcı işlevi kullanarak int değişkenini Number'e çevirir.
Fakat derleyici int, a+ ve Number gördüğü zaman ne yapacağını bilemez,
zira hepsinin sahip olduğu, gereken Number::operator+ solyan işleneni,
baştan bir Number nesnesidir. Bu yüzden derleyici hata üretir.
friend operator- ile durumlar farklıdır. Derleyici için gereken her iki
değişkeni doldurabilmektir bununla birlikte yapabilir, zira solyan
değişkeni olarak bir Number ile sınırlama yoktur. Bu yüzden aşağıdaki
açıklamayı gördüğünde
1-a ;
İlk değişkeni yapıcı işlevi kullanarak Number'e çevirebilir.
Bazı durumlarda işleçlerinizi üye yaparak kullanımlarını
sınırlandırabilmek isteyebilirsiniz. Örnek olarak bir matriksi bir vektörle
çarpmak isteyebilirsiniz, o zaman vektör mutlaka sağyana gitmelidir. Eğer
işleçlerinizin değişkenlerden herhangi birini çevirebilmesini isterseniz,
işlecinizi friend işlev yapmalısınız.
Derleyici allahtan 1-1 alıp her iki değişkeni Number nesneleri yapıp
sonrada operator- işlecini çağırmaz. Bu C kodlarının birdenbire farklı
çalışmaya başladığını gösterirdi. Derleyici önce en basit uyum olasılığını,
burada 1-1 açıklaması için yerleşik işleçlerdir- kullanır.
Tip çevirme örneği
Karakter string'lerini sarmalayan herhangi bir sınıfla otomatik tip çevrimi
oldukça yaralı bir uygulamadır, şimdi böyle bir örnek verelim. (bu
durumda standart C++ sınıflarından string sınıfı, daha basit olduğu için
kullanılacaktır.) Standart C kütüphanesinde varolan bütün string işlevleri,
otomatik tip çevrimi olmaksızın kullanılmak istenirse, herbiri için bir üye
işlev oluşturmak zorundasınız. Aynı aşağıdaki gibi;
//:B12:Strings1.cpp
//otomatik tip çevrimi yok
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std ;
class Stringc{
string s;
public:
Stringc(const string& str=" "):s(str){}
int strcmp(const String& S)const{
return ::strcmp(s.c_str( ),S.s.c_str( )) ;
}
//string.h işlevlerinin hepsi için benzerleri yazılır
};
int main( ){
Stringc s1("merhaba"), s2("nehaber") ;
s1.strcmp(s2) ;
}///:~
Burada görüldüğü üzere sadece strcmp( ) işlevi oluşturuldu, fakat siz
<cstring> te bulunan herbiri için karşılık gelen bir -gerek varsa- üye işlev
oluşturmak zorundasınız. Allahtanki <cstring> te yeralan bütün işlevlere
erişebilmek için otomatik tip çevrimi sağlanabilmekte, böylece fazla uğraşı
gerekmemekte.
//:B12:Strings2.cpp
//otomatik tip çevrimi ile
#include ".../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std ;
class Stringc{
string s ;
public:
Stringc(const string& str=" "):s(str){}
operator const char*( )const{
return s.c_str( ) ;
}
};
int main( ){
Stringc s1("merhaba"), s2("nehaber") ;
strcmp(s1,s2) ; //standart C string işlevleri
strspn(s1,s2) ; //herhangi string işlevleri
}///:~
char* değişkenini bulunduran işlevler, Stringc işlevi de bulundurabilir,
çünkü derleyici Stringc den char* dönüşümünün nasıl yapılacağını bilir.
Otomatik tip çevrimindeki tuzaklar
Çevrimlerinizi doğru tasarlamazsanız, derleyici tip çevriminde seçim
yapmak zorunda olduğu için sorunlar çıkabilir. Basit ve açık bir örnek
olarak X sınıfının kendisini operatorY( ) ile, Y sınıfının nesnesine
çevirmeyi verebiliriz. Eğer Y sınıfı yapıcı işlevi X tipinde tek bir
değişkene sahipse, bu özdeş tip çevrimini gösterir. Derleyicinin X ten Y ye
gitmek için şimdi iki yolu bulunmaktadır, o nedenle çevrim olurken
derleyici iki anlamlılık hatası üretir.
//:B12:TypeConversionAmbiguity.cpp
class Orange ; //tip bildirimi
class Apple{
public:
operator Orange( ) const ; //Apple'ı Orange'a çevirme
};
class Orange{
public:
Orange(Apple) ; //Apple'ı Orange'a çevirme
};
void f(Orange) {};
int main( ){
Apple a ;
//! f(a) ; //Hata : iki anlamlılık hatası
}///:~
Bu sorunun açık çözümü bir tipten diğer tipe çevrimi tek bir yolla
sağlamaktır.
Otomatik tip çevriminde rastlanılan daha karmaşık bir sorunda, birden
fazla tipe çevirmedir. Bu durum bazen çıkış (fan-out) diye adlandırılır.
//:B12:TypeConversionFanout.cpp
class Orange{} ;
class Pear{};
class Apple{
public:
operator Orange( ) const ;
operator Pear( ) const ;
};
//overload eat( ) ;
void eat(Orange) ;
void eat(Pear) ;
int main( )7
Apple c ;
//! eat(c) ;
//Hata : Apple->Orange veya Apple->Pear ?
}///:~
Apple sınıfı hem Orange hem de Pear'e otomatik tip çevrimi yapar.
Haince bir kenarda duran bu hata, birisi eat( ) işlevinin iki bindirilmiş
sürümünü oluşturana kadar ortaya çıkmaz. (main( ) işlevinin içindeki kod
sadece tek sürümle düzgün çalışır.). Burada da çözüm - otomatik tip
çevrimlerinin genel yaklaşımı-; bir tipten öteki tipe sadece tek çevrim
olmalıdır. Başka tiplere de çevrim yapabilirsiniz, fakat onlar otomatik
çevrim olmazlar. Adı açıkça belirtilmiş işlev çağrıları oluşturabilirsiniz,
aynı makeA( ) ve makeB( ) gibi.
Gizli etkinlikler
Otomatik tip çevrimi beklenenden daha fazla etkinlik yapar. Biraz kafa
ütüleyici de olsa aşağıdaki değişikliklere bakalım:
//:B12:CopyingVsInitialization2.cpp
class F1{};
class Fee{
public:
Fee(int){}
Fee(const Fi&){}
};
class Fo{
int i ;
public:
Fo(int x=0):i(x){}
operator Fee( ) const{return Fee(i);}
};
int main( ){
Fo fo ;
Fee fee=fo ;
}///:~
Fo nesnesinden Fee fee oluşturmak için yapıcı işlev bulunmamaktadır.
Bununla beraber Fo, Fee'e çevrim için otomatik tip çevrimine sahiptir. Fee
den Fee oluşturabilmek için bir kopya yapıcı işlev bulunmamaktadır, fakat
derleyici bu özel işlevi sizin için oluşturabilir.(varsayılan yapıcı işlev,
kopya yapıcı işlev, operator= ve yıkıcı işlev derleyici tarafından otomatik
olarak oluşturulur). Zararsız görünen aşağıdaki deyim;
Fee fee=fo ;
Otomatik tip çevrimi çağırılır ve kopya yapıcı işlev oluşturulur.
Otomatik tip çevrimi dikkatli kullanılmalıdır. Bütün işleç bindirimleri
kodlamayı azaltan durumlarda mükemmel sonuçlar verir, ama faydasız
kullanmaya değmez. İşleç bindirimleri hayatı kolaylaştırılacaksa
kullanılmalıdır. Yoksa bunlarla ilgili sihirli herhangi bir şey
bulunmamaktadır.
Özet
İşleç bindiriminin varlık nedeni hayatı kolaylaştırmak içindir. Burada
sihirbazlıkla veya Müştak Dede ile ilgili bir şey bulunmamaktadır.
Bindirilmiş işleçler matrak isimleri bulunan işlevlerdir. İşlev çağrıları
bizim için derleyici tarafından gerçeklenir. Tabii herşey doğru düzende
yazılmışsa. Ama yine de işleç bindirimi size yarar sağlamıyorsa artistlik
olsun diye bunları kullanmayın, zira kullanıcılar için kafa karıştırıcı
olabilir. Programlamada temel kural aradan ne kadar zaman geçerse geçsin
yazmış olduğunuz programa baktığınızda herşeyi anlayabilmenizdir. Yani
herşey basit olmalı.
13. BÖLÜM
Nesneyi Dinamik Oluşturma
Programlarda nesnelerin sayısını, tipini ve ömrünü bazen tam olarak
bilirsiniz. Ama bu her zaman olmaz.
Örnek olarak; Kaç tane uçak hava trafik denetim kulesine gereksinim
duyar?. CAD sistemi kaç tane şekil kullanır?. Bir ağda kaç tane düğüm
bulunur?. bunları önceden tam olarak belirleyemezsiniz.
Programlamanın bu genel sorununu çözmek için nesneler, program
çalışırken oluşturulur ve yok edilir.
Aslında C dilinde dinamik bellek yerleşim işlevleri her zaman
sağlanmıştır ; malloc( ) ve free( ) gibi.
malloc( ) işlevi benzerleri gibi program çalışması sırasında kümeden yerler
alarak (küme=heap bazende serbest alan=free store denir) saklamalar
yapar.
Bununla birlikte bu uygulama C++ dilinde iş görmez. Zira yapıcı işlev, ilk
değer ataması için bellekteki yeri kullanmanıza izin vermez, ama iyi bir
nedeni vardır. Zaten eğer bunu yapabilseydiniz;
1- Unuturdunuz. C++ dilinde nesnelerin güvence altındaki ilk değer
atamalarında sorunlar ortaya çıkardı.
2- Doğru iş yaptığınız umudu ile, ilk değer ataması yapılmadan önce,
nesnelere kazara işlem yaptırabilirdiniz.
3- Yanlış büyüklükte nesne kullanırdınız.
Ve tabii siz her şeyi doğru yapmış olsanız bile, daha sonra sizin
programınızda değişiklik yapılması, aynı hataların ortaya çıkmasına
yolaçabilir. Programlama sorunlarının büyük bir kesiminden, uygun
olmayan ilk değer atamaları sorumludur. Kümede oluşan nesneler için
yapıcı işlev çağrılarının garanti edilmesi, özellikle çok önemlidir.
C++ nasıl böyle garanti, uygun ilk değer atama ve silme etkinliğini
yapacak, ve kümede nesneleri dinamik olarak oluşturmaya izin verecek.
Yanıt; dinamik nesne oluşumunu dilin çekirdeğine getirmektir. malloc( )
ve free( ) kütüphane işlevleridir ve bu yüzden derleyici denetimi dışında
kalırlar. Bununla birlikte, eğer dinamik bellek yerleşimi ile ilk değer
atamasını birlikte gerçekleyen bir işleciniz, ve yine silme ile bellek
boşaltmayı birlikte gerçekleyen ikinci bir işleciniz daha varsa, derleyici
hala bütün nesneler için, yapıcı ve yıkıcı işlevleri çağırmayı garanti
edebilir.
Bu bölümde, C++ nın new ve delete ile, kümelerde nesneler oluşurken bu
sorunu ne kadar güzel çözümlediğini göreceksiniz.
Nesne Oluşumu
C++ dilinde nesne oluşturulduğu zaman, iki olay meydana gelir;
1- Nesne için bellekte yer ayrılır.
2- Belleğe ilk değer ataması (başlatma) için yapıcı işlev çağrılır.
Şimdiye kadar ikinci adımın hep olduğunu kabul ettik. C++ dili, program
hatalarının en büyük bölümünü oluşturan nesneye ilk değer atamasını
mutlaka yaptırır. Nesnenin nerede ve nasıl oluştuğu farketmez, yapıcı işlev
her zaman çağrılır.
Bununla birlikte birinci adım, birkaç şekilde veya değişik zamanlarda
olabilir.
1- Statik bellek alanına saklama, program çalışmaya başlamadan önce
gerçeklenir. Bu bellek alanı saklaması, bütün program boyunca yaşamını
sürdürür.
2- Yığıt üzerinde saklama, program çalışması özel bir noktaya vardığı
zaman olabilir (örnek olarak zincirli ayraç açılması gibi) . Bellek
alanındaki veri, çalışma tamamlandığında boşaltılır (zincirli ayracın
kapanması gibi). Yığıta yerleştirme işlemleri işlemcinin komut setinde
bulunur ve çok verimlidir. Bununla birlikte derleyicinin doğru kodu
üretebilmesi için, programı yazarken kaç tane değişkene gerek olduğu tam
olarak bilinmelidir.
3- Saklama küme (heap veya free store=serbest alan) denilen bellek
havuzundan alınan alana yapılır. Buna dinamik bellek yerleşimi denir.
Belleğe yerleşim için, program çalışırken bir işlev çağrılır; bu, herhangi bir
anda gereken kadar bellek isteyebilirsiniz anlamına gelir. Ayrıca bellek
boşaltımının ne zaman yapılacağı sizin sorumluluğunuzda olup, bellek
kullanım ömrü sizin seçiminize kalmıştır, yani sistem görüntüsü tarafından
belirlenmez.
Yukarıdaki üç bellek bölgesi, fiziksel belleğin tek bir alanına bitişik olarak
yerleşir; statik alan, yığıt ve küme.(derleyici yazarının belirlediği
sıralamada). Ama yinede sıralamada kesin herhangi bir kural
bulunmamaktadır. Yığıt, özel bir alanda bulunabilir ve küme de, işletim
sisteminden bellek parçalarına yapılan çağrılarla uygulanabilir. Programcı
olarak siz bunların farkına varmazsınız, sizin için herşey sadece bellek
alanı ve çağrılardır.
Kümelere C'nin yaklaşımı
Çalışma sırasında belleğe dinamik olarak yerleşim için C dili standart
kütüphanesinde işlevler bulunmaktadır. Kümeden bellek alanı kullanmak
için malloc( ) ve onun çeşitleri olan calloc( ), realloc( ), ve küme boşaltma
için
free( ). Bu işlevler esnek ama, kullanırken programcının dikkat ve iyi
anlamasını gerektirirler. C'nin dinamik bellek yerleşim işlevlerini
kullanarak, kümede sınıf unsuru oluşturmak için, aşağıdaki gibi benzer
bazı işlemleri yapmalıdır.
//:B13:MallocClass.cpp
//sınıf nesneleri yapılması gerekenler
//eğer new kullanılmayacaksa
#include "../require.h"
#include <cstdlib>
//malloc( ) & free( )
#include <cstring>
//memset( )
#include <iostream>
using namespace std ;
class Object{
int i,j,k ;
enum{sz=200 };
char buf[sz] ;
public:
void initialize( ){//yapıcı işlev kullanılamaz
cout<<"Initializing Obj"<<endl ;
i=j=k=0 ;
memset(buf,0,sz) ;
}
void destroy( ) const{//yıkıcı kullanılamaz
cout<<"destroying Object"<<endl ;
}
};
int main( ){
Obj* obj=(Obj*)malloc(sizeof(Obj)) ;
require(obj!=0) ;
obj->initialize( ) ;
//..bir süre sonra..
obj->destroy( ) ;
free(obj) ;
}///:~
Görüldüğü gibi malloc( ) işlevinin kullanımı, aşağıdaki satırda bulunan
nesne için saklama alanı oluşturur.
Obj* obj=(Obj*)malloc(sizeof(Obj)) ;
Burada kullanıcı mutlaka nesne büyüklüğünü bilmek zorundadır (işte hata
kaynağı olabilecek yer). Nesne değil bir bellek yaması ürettiği için malloc(
) işlevi void* geri döndürür. C++ dili void* tipinin herhangi bir göstergeçe
atanmasına izin vermez, bundan dolayı döküm yapılması zorunluluğu
doğar.
Geri dönen göstergecin denetlenmesi zorunluluğu, işlemin başarılı
olduğunu anlamamıza yarar, zira malloc( ) işlevi herhangi bir bellek yeri
bulamayabilir (sıfır değeri geri döndürdüğü durum). Denetleme emin
olmak için yapılır.
Ama en kötü sorun kaynağı, aşağıdaki satırdır:
Obj->initialize( ) ;
Kullanıcılar onu yaparsa, bu çok doğru olur. Nesnelerin kullanılmadan
önce mutlaka ilk değer atamaları yapılmalıdır. Farkettiğiniz gibi yapıcı
işlev kullanılmadı, zira yapıcı işlev aleni biçimde çağrılmadı.- nesne
oluşturulurken yapıcı işlev, derleyici tarafından bizim için çağrılır.
Buradaki sorun; kullanıcının, nesnenin kullanılmadan önce ilk değer
atamasını unutabilmesidir, böylece ana hata kaynağı ortaya çıkar .
Programcıların çoğu, C dilindeki dinamik bellek yerleşim işlevlerini
karmaşık, ve kafa karıştırıcı bulurlar. Statik bellek alanında, dinamik
bellek yerleşim sorunlarından kurtulmak için, büyük boyutlu dizi
değişkenlerini yerleştirirken, programcılar arasında sanal bellek makineleri
kullanmak pek yaygın değildir. Dikkatsiz programcılara C++ kütüphane
kullanımını kolaylaştırdığı için, C dilinin dinamik belleğe yaklaşımı kabul
görmemektedir.
new işleci
C++ dilinde dinamik nesne oluşumu için gereken bütün eylemler bir
işleçle, yani new ile yerine getirilir. new ile bir nesne oluşturulduğu zaman
(yani new açıklaması ile), nesne için küme üzerinde yeterli bellek ayarlanır
ve o yer için yapıcı işlev çağrılır. Böylece eğer
MyType *fp=new MyType(1,2) ;
çalışma anında söylendiğinde malloc(sizeof(MyType)) eşdeğeri çağrılır.
(çoğunlukla malloc( ) işlevinin yazım çağrısı olup, MyType yapıcı işlevi
ile sonlanan this adres göstergeci çağrılır, ve değişken listesinde (1,2)
bulunur). Göstergeç fp ye atandğı zaman ilk değeri atanmış canlı bir
nesnedir, zaten daha önce onu kullanamazsınız. Ayrıca uygun MyType
tip, otomatik olarak belirlendiği için döküm gerekmez.
new işleci bellek yerleşimini başarı ile gerçekledikten sonra yapıcı işleve
adres aktarır. Bundan dolayı çağrı başarılı ise, ayrıca sizin açıkça adres
aktarmanız gerekmez. Bu bölümün ilerleyen kısımlarında bellekte yer
kalmadığı zaman neler olacağını da inceleyeceğiz.
Sınıf için geçerli yapıcı işlevlerden birini kullanarak yeni bir açıklama
oluşturabilirsiniz. Yapıcı işlevde değişken bulunmuyorsa, yeni açıklamayı
değişken listesi bulunmaksızın yazarsınız:
MyType* fp=new MyType ;
Gördüğünüz gibi, kümelerde yeni nesne oluşturma süreci ne kadar basit;
sadece tek bir açıklama. Bütün boyut bilgileri, çevrimler ve güvenlik
denetimleri burada yerleşik olarak bulunmaktadır. Küme üzerinde nesne
oluşturmak, yığıt üzerinde nesne oluşturmak gibi kolaydır.
delete işleci
delete açıklaması new açıklamasının tamamlayıcısıdır, önce yıkıcı işlev
çağrılır ve daha sonra bellek boşaltılır ( çoğu kez free( ) işlevi çağrısıdır.
Hemen new açıklaması, nesneye bir göstergeç geri döndürür, delete
açıklaması için nesnenin adresi gerekir)
delete fp ;
Bu silme işlemini yapar ve daha önceden belleğe dinamik olarak yerleşmiş
olan MyType nesnesinden belleği boşaltır.
delete sadece new işleci ile oluşturulmuş nesneler için çağrılabilir. Bir
nesneyi malloc( ) (calloc( ) veya realloc( ) ta olabilir) işlevi ile
oluşturursanız, daha sonra delete ile ortadan kaldırmak isterseniz nasıl
sonuç alınacağı tanımsızdır. Zira new ile delete için varsayılan
uygulamalar malloc( ) ve free( ) işlevlerini kullanırlar. Büyük ihtimalle
bellek boşaltmayı yıkıcı işlev çağırmaksızın gerçekler.
delete edilen göstergeç değeri sıfır ise, herhangi bir şey olmaz. Bu nedenle
çoğunlukla insanlar delete işleminden hemen sonra, göstergeci sıfıra
eşitleyerek iki kez delete edilmesini engelleme öneriyorlar. Bir nesnenin
iki kez delete edilmesi kötü bir işlemdir ve programda sorunlara yolaçar.
Basit bir örnek
Aşağıdaki örnek ilk değer atama işlemlerini göstermektedir;
//:B13:Agac.h
#ifndef AGAC_H
#define AGAC_H
#include <iostream>
class Agac{
int boy ;
public:
Agac(int agacBoy):boy(agacBoy){ }
~Agac( ){std::cout<<"*";}
friend std::ostream&
operator<<(std::ostream& os, const Agac* a){
return os<<"Agac boyu: "
<<a->boy<<std::endl ;
}
};
#endif //AGAC_H//:~
//:B13:NewVeDelete.cpp
//new ve delete işleçleri için basit bir örnek
#include "Agac.h"
using namespace std ;
int main( ){
Agac* a=new Agac[50] ;
cout<<a ;
delete a ;
}///:~
Agac değerini yazdırmak ile yapıcı işlevin çağrıldığını ispatlayabiliriz.
Bunu operator<< bindirimi, ostream ve Agac* ile gerçekleriz. Bununla
beraber belirtmemiz gereken; işlev friend olarak bildirilse bile, satıriçi
(inline) olarak tanımlanır. Temiz bir açıklama olan bu durum, sınıfa satıriçi
olarak tanımlanmış işlevin friend olmasını, veya global işlev ve sınıf üye
işlevi olmamasını etkilemez. Ayrıca geri dönüş değerinin, çıktının bütünün
sonucu olduğu gözardı edilmemelidir, yani ostream&. (geri dönüş değer
tipinin tutarlılığı için, olması zorunlu.)
Bellek yöneticisinin maliyeti
Nesneler yığıt üzerinde otomatik olarak oluşturulduğu zaman, nesnelerin
büyüklüğü ve ömürleri üretilen kodun içine inşa edilir, zira derleyici tipi,
miktarı ve görünümü tam olarak bilir. Nesnelerin kümeler üzerinde
oluşturulması ek maliyetler getirir. Bu maliyetler hem zaman hem de
bellek alanı açısındandır. Tipik bir uygulama ise ( malloc( ) ile calloc( )
veya realloc( ) işlevleri yer değiştirebilir).
malloc( ) çağrılarak havuzdan bellek parçası talep edilir. (kod aslında
malloc( ) un bir parçasıdır)
Havuz istenene uygun büyüklükte bellek taşıyormu araştırılır. Bu işlem bir
haritalama veya bir biçimde düzenlenmiş dizinlere bakılarak kullanımdaki
bellek ile kullanılmayanları inceleyerek belirlenir. İşlemler hızlı
süreçlerdir, ama sistem deterministik çalışmaz, bundan dolayı malloc( ) her
zaman aynı sürede gerçeklenmez.
Bellek parçasının göstergeci geri döndürülmeden önce, daha sonraki
malloc( ) çağrılarının onu kullanmaması için, mutlaka büyüklüğü ve yeri
kaydedilmelidir. Bundan dolayı free( ) işlevi çağrıldığı zaman, sistem
neredeki, ne kadar belleği boşaltacağını bilir.
Bunları gerçekleyen çok değişik yollar bulunmaktadır. Örnek olarak
işlemcideki belleğe yerleşimi sağlayan temel komutları önleyen, herhangi
bir unsur yoktur. Meraklı iseniz malloc( ) işlevinin uygulamasını sınayan
kodlar yazabilir- malloc( ) sürecini izleyebilirsiniz. Varsa kütüphane
kaynak kodlarına bakabilirsiniz.
Önceki örneklerin yeniden tasarımı
Kitabın önceki bölümlerinde verilen Stash örneği new ve delete
kullanılarak yeniden yazılacak ve bu işlevlerin özellikleri ayrıntıları ile
incelenecektir.
Kitabın burasında artık ne Stash ne de Stack işaretlenecek kendi
nesnelerine sahiptir: yani Stash ve Stack nesneleri görünüm dışına
çıktıklarında, işaret edilen nesnelerin tamamı için artık delete çağrılmaz.
Bunun mümkün olmama nedeni; derinlerden gelmektedir zira,
göstergeçleri void dir. Bir void göstergeci delete ederseniz, olacak tek şey;
bellek boşalmasıdır, çünkü burada ne tip bellidir ne de derleyici hangi
yıkıcı işlevi çağıracağını bilmektedir.
delete void* büyük ihtimalle bir hatadır
Burada bunu vurgulamak, göstergeç varış noktası çok basit değilse
önemlidir, zira delete void* edilmesi özellikle de bir yıkıcı işlev lazım
değilse, bir program için hata kaynağıdır. Aşağıdaki örnek ne olduğunu
gayet iyi göstermektedir;
//:B13:BadVoidPointerDeletion.cpp
//void göstergeçlerin silinmesi bellek kaçaklarına yola açar
#include <iostream>
using namespace std ;
class Object{
void* data; // saklama yeri
const int size ;
const char id ;
public:
Object(int sz, char c):size(sz),id(c){
data=new char[size];
cout<<"Constructing object"<<id
<<", size= "<<size<<endl ;
}
~Object( ){
cout<<"Destructing Object"<<id<<endl ;
delete []data; //OK, bellek boşaltma
//yıkıcı işlev çağrımı gereksiz
}
};
int main( ){
Object* a=new Object(40,'a') ;
delete a;
void* b=new Object(40,'b') ;
delete b;
}///:~
Object sınıfı "ham" veriyi (yıkıcı işlev sahibi nesneleri işaretlemezler) ilk
değer atayan bir void* bulundurur. Object yıkıcı işlevinde delete, void*
için olumsuz yanlar olmaksızın çağrılır, çünkü bu; belleği boşaltmak için
tek koşuldur.
Bunun yanında, main( ) işlevinde delete birlikte çalıştığı nesne tipini
bilmelidir. İşte çıktı;
Constructing object a, size=40
Destructing object a
Constructing object b=40
delete a, a nın işaret ettiği Object yıkıcı işlev çağrısını bilir ve böylece
data için belirlenen bellek boşalır. Bununla birlikte, delete b durumunda
olduğu gibi, bir nesneyi void* aracılığı ile işlemlerseniz, o zaman tek şey
olur; Object bellek alanı boşalması,- ama yıkıcı işlev çağrılmaz, böylece
data'nın işaretlediği bellek alanı boşalmaz. Bu programı derlediğiniz
zaman büyük ihtimalle hata uyarıları görülmez; derleyici, ne yaptığınızı
bildiğinizi kabul eder. Böylece oldukça büyük bir bellek kaçağı oluşur.
Programınızda böyle bir bellek kaçağı oluşursa, önce bütün delete
deyimlerini inceleyin ve silinecek göstergeç tiplerini denetleyin. Eğer bu
göstergeç bir void* ise, o zaman büyük ihtimalle bellek kaçağı
kaynaklarından birini buldunuz demektir. (C++ dili bellek kaçağı
konusunda oldukça uygun fırsatlar sağlar)
Göstergeçlerin silme sorumluluğu
Stash ve Stack kaplarını daha esnek yapabilmek için, void göstergeçleri
(böylece herhangi bir tipi saklayabilirler)kullanılır. Bunun anlamı Stash
veya Stack nesnelerinden göstergeç döndürüldüğü zaman, kullanmadan
önce mutlaka uygun tipe döküm yapmalısınız; ayrıca yukarıda görüldüğü
gibi delete edilmeden öncede uygun tipe döküm yapmak zorundasınız,
aksi taktirde bellek kaçağı oluşur.
Başka bir bellek kaçağı sebebi; kap içinde tutulan her bir nesne göstergeci
için delete çağrısı yapıldığından emin olmak gerektiğidir. Kap kendine
özel göstergeç barındırmaz, zira onları void* olarak tutar ve bu yüzden
uygun silmeyi gerçekleştiremez. Kullanıcı, nesnelerin silinmesinden
mecburen kendisi sorumludur. Eğer aynı kapta,kümelerde ve yığıtta
oluşturulmuş nesnelere göstergeçler eklenirse, delete açıklaması
emniyetsiz olan göstergeçin küme üzerine yerleştirilememesinden ötürü,
ciddi sorunlar ortaya çıkar. (ve kaptan bir göstergeç getirildiği zaman
nesnesinin nerede yerleşik olduğunu nasıl bileceksiniz.). Böylece Stash ve
Stack'in takip eden sürümlerinde saklı nesnelerin, yalnız küme üzerinde
bulunduklarından emin olunmalıdır. Ya dikkatli bir programlama yapılmalı
ya da küme üzerinde inşa edilebilen sınıflar oluşturmalıdır.
Ayrıca müşteri programcının kaptaki bütün göstergeçleri silme
sorumluluğunu güvenceye almak önemlidir. Önceki örneklerde Stack
sınıfının bütün Link nesnelerini geri çekmek için, nasıl yıkıcı işlevini
denetlediğini görmüştünüz. Stash göstergeçleri için ikinci bir yaklaşım
gerekmektedir.
Göstergeçler ve Stash
Stash sınıfının bu yeni sürümünde, PStash kümede kendiliklerinden
bulunan nesnelerin göstergeçlerini tutar. Daha önceki bölümlerde bulunan
eski Stash nesneleri değerleri ile Stash kabına kopyaladı. Küme üzerinde
oluşturulmakta olan nesnelerin göstergeçlerini new ve delete kullanarak
tutmak güvenli ve kolaydır.
"Göstergeç Stash" için aşağıda bir başlık dosyası örnek olarak verilmiştir.
//:B13:PStash.h
//Nesnelerin yerine, göstergeçleri tutar.
#ifndef PSTASH_H
#define PSTASH_H
class PStash{
int quantity ; //saklama alan sayısı
int next ;
//bir sonraki boş alan
//göstergeç saklama alanı
void** storage ;
void inflate(increase) ;
public:
PStash( ):quantity(0),storage(0),next(0){}
~PStash( );
int add(void* element) ;
void* operator[](int index) const; //getirmek
//Bu PStash ten dayancı kaldır
void* remove(int index) ;
//Stash te bulunan öge sayısı:
int count( ) const{return next ;}
};
#endif ///PSTASH_H//:~
Belli veri ögeleri oldukça birbirlerine benzerler, ama şimdi storage, void
göstergeçeler dizisidir, ve bu diziyi saklamak için malloc( ) işlevi yerine
new kullanılarak yerleşim yapılır. Aşağıdaki açıklamada:
void** st=new void*[quantity+increase] ;
Yerleştirilen nesne tipi bir void* tir. Bu yüzden açıklamada yapılan
yerleşim, void göstergeçler dizisidir.
Yıkıcılar void göstergeçlerin tutulduğu yerleri boşaltırlar yoksa onların
işaret ettiklerini değil. (daha önce belirtildiği gibi, saklama yeri boşaltılır
ve yıkıcılar çağrılmaz, zira void göstergeçlerde tip bilgisi bulunmaz. )
Başka bir farklılıkta fetch( ) işlevi ile operator[] yer değiştirmesidir, bu
daha duyarlı bir yazım biçimidir. Bununla birlikte yine void* geri döner,
bu yüzden kullanıcı kaptakilerin tipini hatırlamak ve göstergeçleri dışarı
getirdiğinde döküm işlemine tutmak zorundadır.(döküm=casting=tipi
uygun tipe çevirme) (sorun ilerleyen bölümlerde çözümlenecek)
İşte üye işlev tanımları:
//:B13:PStash.cpp {O}
//Stash göstergeç tanımları
#include "PStash.h"
#include "../require.h"
#include <iostream>
#include <cstring> //'mem' işlevleri
using namespace std ;
int PStash::add(void* element){
const int inflateSize=10 ;
if(next>=quantity)
inflate(inflateSize);
storage[next++]=element ;
return(next-1) ;
}
//sahiplik yok
PStash::~PStash( ){
for(int i==;i<next;i++)
require(storage[i]==0,
"PStash silinmedi");
delete []storage;
}
//işleç bindiriminin fetch için yer değiştirmesi
void* PStash::operator[] (int index)const{
require(index>=0,
"PStash::operator[] index negative") ;
if(index>=next)
return 0 ; //sona gelindi
//arzulanan ögeye göstergeç üretme
return storage[index] ;
}
void* PStash::remove(int index){
void* v=operator[] (index) ;
//göstergeci "kaldır"
if(v!=0)storage[index]=0;
return v ;
}
void PStash::inflate(int increase){
const int psz=sizeof(void*) ;
void** st=new void*[quantity+increase];
memset(st,0,(quantity+increase)*psz) ;
memcpy(st,storage,quantity* psz);
quantity+=increase ;
delete []storage ; //eski saklama yeri
storage=st ; //yeni bellek yerini işaretliyor
}///:~
add( ) işlevi daha önceki etkinliği ile aynıdır, sadece bütün nesnenin bir
kopyası yerine göstergeç saklanmıştır. void* dizisinin bellek yerleşimi
için, daha önceki tasarım yerine inflate( ) işlevinin kodları değiştirilmiştir.
Eski tasarım ham baytlarla çalışıyordu. Burada dizi dizinleme ile
kopyalamanın öncelikli yaklaşımını kullanmak yerine, memset( ) Standart
C kütüphane işlevi önce kullanılarak bütün yeni bellek sıfıra eşitlenir. (Bu
katı olarak gerekli değildir, zira herhalde PStash bütün belleği doğru
şekilde yönetir.- fakat yine de ilave çaba sarfetmek zararsızdır.). memcpy
( ) varolan veriyi, eski yerinden alıp yeni yere götürür. Çoğunlukla memset
( ), memcpy( ) gibi işlevler zaman içinde iyileştirilmekte ve önceki
döngülerden daha hızlı olmaktadırlar. Ama inflate( ) gibi benzeri işlevlerin
kullanılmadığı durumlarda çoğu kez performans değişikliği gözlemlenmez.
Bununla birlikte işlev çağrıları, döngülere göre daha kısa ve, kodlama
hatalarını engelleyicidir.
Nesnelerin silinmesi sorumluluğu müşteri programcının omuzlarına
yüklenir, PStash içinde göstergeçlere erişmek için iki yol bulunur;
operator[] göstergeçi basitçe geri döndürür ama onu kabın bir üyesi olarak
bırakır, ve ikincisi remove( ) üye işlevi olup göstergeçi geri döndürür, ve
ayrıca yerine sıfır atayarak onu kaptan çıkarır. PStash için yıkıcı işlev
çağrıldığı zaman, bütün nesne göstergeçlerinin kaldırıldığından emin
olmak için denetim yapılır; eğer kalkmamışsa size bildirilir, böylece bellek
kaçağından kurtulunmuş olur. (çok daha güzel çözümler ilerleyen
bölümlerde verilecek)
Sınama
Stash için yazılmış eski sınama programı şimdi PStash için yeniden
yazılacak;
//:B13:PStashTest.cpp
//{L} PStash
//Stash göstergeçinin sınanması
#include "PStash.h"
#include "require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
PStash intStash ;
//new yerleşik tiplerle de çalışır.
//yardımcı yapıcı işlev yazım biçimi
for(int i=0;i<25;i++)
intStash.add(new int(i)) ;
for(int j=0;j<intStash.count( );j++)
cout<<"intStash["<<j<<"]= "
<<*(int*)intStash[j]<<endl ;
//silme
for(int k=0;k<intStash.count( );k++)
delete intStash.remove(k) ;
ifstream in("PStashTest.cpp") ;
assure(in,"PStashTest.cpp") ;
PStash stringStash ;
string line ;
while(getline(in,line))
stringStash.add(new string(line));
//stringleri yazdır
for(int u=0;stringStash[u];u++)
cout<<"stringStash["<<u<<"]= "
<<*(string*)stringStash[u]<<endl ;
//silme
for(int v=0;v<stringStash.count( );v++)
delete (string*)stringStash.remove(v) ;
}///:~
Daha önce plduğu gibi Stash oluşturulur ve içi bilgi ile doldurulur, fakat
bu sefer bilgi, new den kaynaklanan göstergeçlerdir. İlk durumda
aşağıdaki satır;
intStash.add(new int(i)) ;
new int(i) açıklaması yardımcı yapıcı işlev kullanır, böylece küme
üzerinde yeni int nesnesi için bellek yeri ayrılır ve int ilk değer ataması i
ile gerçeklenir.
Yazdırma işlemi esnasında PStash::operator[] dan geri dönen değer
mutlaka uygun tipe dönüştürülmelidir (döküm); aynı işlem programda
bulunan öteki PStash nesneleri için de tekrarlanır. Temel gösterim olarak
void göstergeçlerin kullanımı istenmeyen etkiler oluşturur, ve daha sonra
ilerleyen bölümlerde düzeltilecekler.
İkinci sınama kaynak kod dosyasını açar ve her seferinde PStash'e bir satır
okur. Herbir satır string'e okunurken
getline( ) işlevini kullanır. Daha sonra line'dan satırın bağımsız kopyası
olan bir new string oluşturulur. Her seferinde line adresini hemen
aktarsaydık, sadece dosyadan okunan en son satır olan line'ı işaret eden
göstergeçler demetimiz olurdu.
Göstergeçler getirildiği zaman, aşağıdaki açıklamayı görürsünüz;
*(string*)stringStash[v] ;
operator[] den geri dönen göstergeç mutlaka tip uygunluğunu sağlamak
için string* tipine dönüştürülmelidir. Daha sonra string* dayancından
kurtarılır yani adresi tespit edilir böylece açıklama bir nesneyi hesaplar,
işte burası derleyicinin cout'a göndereceği string nesnesini gördüğü
yerdir.
Küme üzerinde oluşturulan nesne, mutlaka remove( ) deyimi kullanılarak
ortadan kaldırılmalıdır, veya başka bir seçenek olarak çalışma sırasında
PStash'te bulunan nesnelerin tamamen silinmediği iletisi verilmelidir.
Farkına vardıysanız int göstergeçlerin olduğu durumlarda tip dönüşümü
gerekmemektedir zira int için yıkıcı işlev bulunmamaktadır ve bütün
gereken sadece bellek boşaltımıdır.
delete intStash.remove(k) ;
Bununla beraber string göstergeçlerin kullanıldığı durumlarda tip
dönüşümleri yapılmazsa, başka bir bellek kaçağı kaynağı ortaya çıkar.
Burada döküm, yani tip dönüşümü ana unsurdur.
delete (string*)stringStash.remove(k) ;
Bu olumsuz özelliklerin bazıları (hepsi değil) şablonlar (template)
kullanılarak ortadan kaldırılabilir. Şablonlar ilerleyen bölümlerde
anlatılacak.
Dizilerde kullanılan new ve delete
C++ dilinde nesne dizileri, yığıt veya kümelerde aynı kolaylıkla
oluşturulabilir. Doğal olarakta dizide bulunan her nesne için yapıcı işlev
çağrılır. Yalnız bir kısıtlayıcı kural bulunur; mutlaka varsayılan yapıcı
işlev olmalı, yığıtta ilk değer atamalarını yapma hariç, zira değişkenleri
olmayan yapıcı işlev herbir nesne için mutlaka çağrılmalıdır.
Küme üzerinde new ile nesne dizisi oluştururken yapılması gereken başka
bir şey daha bulunur. Böyle bir örnek aşağıda verilmiştir;
MyType* fp=new MyType[100] ;
Yukarıdaki açıklama küme üzerinde 100 tane MyType nesnesi için yeterli
bellek yerini hazırlar ve herbiri için yapıcı işlevi çağırır. Fakat burada bir
de MyType* bulunmaktadır, eğer aşağıdaki açıklamayı yazsaydınız ki
tamamen aynı olurdu;
MyType* fp2=new MyType ;
tek bir nesne oluşurdu. Kod yazıldığı için fp dizinin başlangıç adresidir, bu
yüzden dizi ögelerini seçmek için fp[4] gibi bir açıklama anlamlıdır. Şimdi
geldik dizinin yokedilmesine, deyimler aşağıda bakalım ne olacak;
delete fp2 ; //OK
delete fp ; //istenen etki olmaz
Aynı şekilde bakın, etki de tamamen aynı. Gösterilen adresteki MyType
nesnesi için yıkıcı işlev çağrılır ve orada bulunan nesne silinir, fp2 için
herşey iyi gider. Fakat fp için 99 tane yıkıcı işlev çağrımı yapılmaz. Hala
bile uygun miktarda bellek yeri boşalmıştır, bunun yanında, bellek
yerleşimi büyük bir küme halinde yapıldığı için yerleştirme işlemi
esnasında bu küme bir yere gizlenir.
Çözüm dizinin başlangıç adresini derleyiciye vermekten geçer. Bu da
aşağıdaki yazım biçimi ile gerçeklenir;
delete []fp ;
İçi boş köşeli ayraçlar derleyiciye, dizi oluşturulurken herhangi bir yere
konan, dizide kaç tane nesne varsa getirmesini sağlayan kodu ürettirir, ve o
kadar sayıdaki nesne için yıkıcı işlevi çağırır. Aslında bu, eski
kodlamalarda rastlanılanın biraz daha gelişmişidir. Eskiden genellikle;
delete [100]fp ;
Nesne sayısı dizi de yazılmaya mecbur bırakılırdı.Böyle bir mecburiyet
bazen programcının yanlış rakam yazmasına yolaçabilirdi. Derleyicinin
bunu kullanmasının maliyeti oldukça düşüktü. Nesne sayısını iki yerine bir
yerde belirtmek daha iyi olarak düşünülürdü.
Bir göstergeçin diziden daha fazlasını yapması
Öte yandan, yukarıda tanımlanmış fp başka şeyleri gösterecek şekilde de
değiştirilebilir. Burada dizinin başlangıç adresi bir anlam ifade etmez.
Sabit olarak tanımlanması daha anlamlıdır, böylece göstergeçte yapılacak
değişiklik hata olarak bayrak çekilmesine yolaçar. Etkinleştirmek için
yapılması gereken;
int const* q=new int[10] ;
veya
const int* q=new int[10] ;
Her iki durumda da const int'e bağlanır, bunun anlamı; işaret edilenin ne
olduğu göstergeçin niteliğinden daha önemlidir demektir. Yerine mutlaka
aşağıdakini yazmalısınız;
int* const q=new int[10] ;
Şimdi, q'da bulunan dizi ögeleri değiştirilebilir, ama q'ya yapılacak
herhangi bir değişikliğe izin verilmez (q++ gibi). Yani normal dizi
belirteci gibi değil.
Saklama alanı dışında çalışma
Nesneyi tutmak için operator new, ardışık bellek alanlarında yer
bulamazsa ne olur?. new yöneticisi (new-handler) denilen özel bir işlev
çağrılır. Ya da daha ziyade bir işleve ait göstergeç denetlenir, ve eğer
göstergeç sıfır değilse, o zaman işaretlenen işlev çağrılır.
new yöneticisinin varsayılan davranışı istisna fırlatmaktır (=throw an
exception- ikinci ciltte ayrıntıları ile anlatılacak). Bununla birlikte
programınızda küme yerleşimi kullanıyorsanız, en azından new
yöneticisini şöyle bir ileti ile yer değiştirmeniz; "şu anda bellek alanı
dışında çalışıyorsunuz lütfen programdan çıkın" akıllıca olur. Bu usul hata
ayıklama sırasında ne olduğuna dair ipucu verir. Zira en son program için
çok daha sağlam bir yapı kurulması gereklidir.
new yöneticisi new.h ile yer değiştirdikten sonra, yerleştirilmek istenen
işlevin adresine sahip olan set_new_handler( ) işlevi çağrılır.
//B13:NewHandler.cpp
//new yöneticisinin değiştirilmesi
#include <iostream>
#include <cstdlib>
#include <new>
ısing namespace std ;
int count=0 ;
void out_of_memory( ){
cerr<<"memory exhausted after"<<count
<<"allocations "<<endl ;
exit(1) ;
}
int main( ){
set_new_handler(out_of_memory) ;
while(1){
count++ ;
new int[1000] ; //bellek bitti
}
}///:~
new yönetici işlevinin değişkenleri bulunmaması şarttır, ayrıca geri dönüş
tipi void olmalıdır. Burada while döngüsü boş bellek yeri kalmayana kadar
int nesnelerinin yerleşimini (geri dönüş adreslerini de fırlatır) sağlar. En
son new çağrısından sonra artık yerleştirilecek bellek yeri kalmamıştır, ve
new yöneticisi çağrılır.
new yöneticisinin davranışı operator new'e bağlanmıştır, öyleki, eğer
operator new bindirime uğratılırsa (bir sonraki bölümde açıklanacak),
new yöneticisi kendiliğinden çağrılmaz. Eğer hala new yöneticisinin
çağrılması isteniyorsa o zaman, bindirilmiş operator new içine ilgili
kodlar yazılmalıdır.
Tabii istenirse çok daha iyi düşünülmüş new yöneticileri yazılabilir, hatta
bazıları belleği islah etmeye çalışabilir, (yaygın olarak bilinen adı ile
çöplük toplayıcısı= garbage collector). Bu işler tabii ki acemi programcı işi
değil.
new ve delete işleçlerinin bindirimi
Bir new açıklaması oluşturulduğu zaman iki şey olur. Önce operator new
kullanılarak saklama yeri yerleşimi yapılır, ve daha sonra yapıcı işlev
çağrılır. Bir delete açıklamasında ise; yıkıcı işlev çağrılır daha sonra
operator delete kullanılarak bellek yeri boşaltılır. Yapıcı ve yıkıcı
işlevlerin çağrısı hiçbir zaman bizim denetimimizde olmaz (aksi taktirde
kazara kullanabilirdik), ama saklama yeri yerleşim işlevleri; operator new
ve operator delete değiştirilebilir.
new ve delete tarafından kullanılan bellek yeri yerleşim sistemi, genel
amaçlı kullanım için tasarlanır. Bununla birlikte bazı özel durumlarda
ihtiyaçlar karşılanmayabilir. Yerleştiriciyi (allocator) değiştirmenin en
yaygın sebebi verimdir; o kadar çok sayıda sınıf nesnesi oluşturulur ve
yokedilir ki, hız darboğazı ortaya çıkar. C++de kendi saklama yeri
yerleşim planlarınızı uygulamanız için new ve delete bindirimine izin
verir. Böylece yukarıdakilere benzer sorunları halledebilirsiniz.
İkinci konuda küme bölünmesidir. Değişik büyüklüklerdeki nesnelerin
belleğe yerleşimi sırasında kümenin parçalara ayrılması, ve böylece
saklama alanı yetmezliğinden bellek dışına taşılması mümkündür. Bunun
anlamı; bellek yeri bulunmaktadır, fakat bellek bölünmesi yüzünden bellek
parçalarının büyüklüğü nesne için yeterli olmamaktadır. Belli bir sınıf için
kendi yerleştiricinizi oluşturursanız böyle bir durumla hiçbir zaman
karşılaşmazsınız.
Tekleştirilmiş (embedded) ve gerçek zamanlı dizgelerde bir program kısıtlı
kaynaklarla çok uzun süre çalışır. Bu çeşit dizgede ayrıca, bellek yerleşimi
her zaman aynı süreyi alır ve, küme bitmesi veya bellek bölünmesine izin
verilmez. Alışılmış yerleştiriciler çözümü sağlar.Aksi taktirde programcılar
new ve delete birlikte kullanmaktan sakınırlar ve böylece değerli C++
özelliklerinden yararlanamazlardı.
Bir konuyu hemen hatırlatalım ; operator new ve operator delete
bindirime uğradığı zaman sadece ham saklama yeri
yolu değiştiriliyor, burası önemli!. Derleyici, saklama yerleşimi için
kolayca, varsayılan yerine sizin new sürümünüzü çağırıyor, daha sonra da
aynı yer için yapıcı işlevi çağrılır. Böylece derleyici saklama yeri
yerleşimini yapıp, yapıcı işlevi çağırmasına rağmen, new'i gördüğü zaman,
new bindirimi yaptığınızda değiştirebileceğiniz herşey saklama yeridir. 0
(delete'de benzer kısıtlamalara haizdir).
operator new bindirime uğradığı zaman, bellek dışına çıktığında
gösterdiği davranışı da yer değiştiriyor. Bu nedenle operator new sıfıra
geri döndüğü zaman yapılacak olana, mutlaka karar vermelisiniz. new
yöneticisini çağıran bir döngü yazılır ve bellek yerleşimi geri alınır ya da
(tipik olarak) bad_alloc istisnası fırlatılabilir.( ikinci kitapta anlatılacak)
new ve delete bindirimleri öteki işleç bindirimleri gibidirler. Bununla
birlikte, global yerleştirici bindirim veya, belli bir sınıf için farklı
yerleştirici kullanma seçeneklerinden birine sahipsiniz.
Global new ve delete bindirimi
new ve delete global sürümleri bütün dizge için yetersiz olduğu zaman,
global bindirim çok sert bir yaklaşımdır. Global sürümler bindirime
uğratılırsa, varsayılanlar tamamı ile erişilemez yapılır, hatta onları yeniden
yaptığınız tanımlarınızın içinden dahi çağıramazsınız.
Bindirilmiş new mutlaka değişken olarak size_t yi almalıdır (standart C de
büyüklükler için standart tip). Bu değişken bizim yerleşiminden sorumlu
olduğumuz nesnenin büyüklüğü olup, bize derleyici tarafından üretilip
aktarılır. Nesneye ya büyüklüğü kadar bir göstergeç (veya eğer neden
varsa daha büyük) geri döndürmelisiniz, ya da bellekte yer yoksa (bu
durumda yapıcı işlev çağrılmaz) sıfır değeri geri döndürülmelidir. Bunun
yanında eğer bellekte yer yoksa, sıfır değeri geri döndürmekten daha
fazlası yapılabilir; örnek olarak new yöneticisi çağrılabilir veya sorun
olduğunu gösteren bir istisna işareti fırlatılabilir.
operator new geri dönüş değeri herhangi belli bir tipe göstergeç değil
void* tir. Yapılan herşey bellek belirlemektir, yoksa bitmiş bir nesne değil.
Zaten nesne oluşturma işlemi sadece yapıcı işlev çağrısı ile mümkündür.
Ve bu denetimimizde, olmayıp derleyici tarafından yerine getirilir.
operator delete daha önceden operator new tarafından kullanılmış
belleğe void* alır. operator delete sadece yıkıcı işlev çağrıldıktan sonra
göstergeçi aldığı için void* tir. Böylece bellek parçasındaki nesnelik
ortadan kalkar. Geri dönüş tipi void'tir.
Şimdi bindirime uğramış global new ve delete örneği verelim;
//:B13:GlobalOperatorNew.cpp
//global new/delete bindirimi
#include <cstdlib>
#include <cstdio>
using namespace std ;
void* operator new(size_t sz){
printf("operator new: %d Bytes\n",sz) ;
void* m=malloc(sz) ;
if(!=m) puts("out of memory") ;
return m ;
}
void operator delete(void* m){
puts("operator delete");
free(m) ;
}
class S{
int i[100] ;
public:
S( ){puts("S::S( )");}
~S( ){puts("S::~S( )");}
};
int main( ){
puts("creating ve destroying an int") ;
int* p=new int(49) ;
delete p;
puts("creating and destroying an S") ;
S* s=new S ;
delete s ;
puts("creating and destroying S[5]");
S* sa=new S[5] ;
delete []sa ;
}///:~
Burada new ve delete bindirimlerinin genel biçimini görmektesiniz.
Bunlar yerleştirici olarak Standart C kütüphanesinden malloc( ) ve free( )
işlevlerini kullanırlar. (herhalde varsayılan new ve delete'te kullanırlar)
Bununla birlikte ayrıca neler yaptıkları ile ilgili iletilerde yazdırırlar.
Farkedeceğiniz gibi iostreams yerine printf( ) ve puts( ) tercih
edilmektedir. Bunun nedeni bir iostream nesnesi oluşturulduğu zaman
(global cin, cout ve cerr benzerleri gibi), bellek yerleşimi için new
çağrılır. printf( ) ile ise,nesnenin kendine ilk değer ataması için new
çağrısı olmadığı için çıkmaza girilmez.
main( ) de yerleşik tiplerin nesneleri oluşturulurken, ayrıca bindirilmiş
new ve delete çağrılabileceği de gösterilmiştir.Daha sonra S tipinin tek
nesnesi oluşturulur, ve S dizisi takip eder. Dizi için, istenen byte
sayısından, tutulacak nesne sayısı hakkındaki bilgiyi saklamak için ek
bellek yerleşimi yapılır. Bütün durumlarda, new ve delete global
bindirilmiş sürümleri kullanılır.
Bir sınıf için bindirilmiş new ve delete işleçleri
Sınıf için new ve delete bindirime uğratıldığında, açıkça static olduğu
belirtilmemiş olmasına rağmen, static üye işlev oluşturulmaktadır. Yazım
biçimi daha önce bindirim yapılmış öteki işleçlerdeki gibidir. Derleyici,
sınıf nesnesinin new kullanılarak oluşturulduğunu gördüğü zaman , global
üzerindeki üye operator new'i seçer. Bunun yanında new ve delete global
sürümleri her çeşit tip nesnesi için kullanılır. (eğer kendi new ve delete leri
yoksa)
Aşağıdaki örnekte Framis sınıfı için temel saklama yeri yerleşim dizgesi
oluşturuluyor. Programın başlangıç kısmında bellek alanının bir bölümü
satik bellek alanı olarak ayrılıyor. Bu bellek alanı Framis tipi nesnelerin
yerleşimi için kullanılır. Yerleşim bloklarını belirtmek için basit bir byte
dizisi kullanılır, her bloka bir byte yerleştirilir.
//:B13:Framis.cpp
//yörel new ve delete bindirimi
#include <cstddef>
#include <fstream>
#include <iostream>
#include <new>
using namespace std ;
ofstream out("Framis.out") ;
class Framis{
enum{sz=10};
char c[sz] ;
//bellek alanı almak için kullanmak için değil
static unsigned char pool[];
static bool alloc_map[] ;
public:
enum{psize=100}; //frami yerleşimine izin verildi
Framis( ){out<<"Framis( )\n" ;}
~Framis( ){out<<"~Framis...";}
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*) ;
};
unsigned char Framis::pool[psize * sizeof(Framis)}] ;
bool Framis::alloc_map[psize]={false} ;
//Size gözardı edilir . Framis nesnesini void* alalım
void* Framis::operator new(size_t) throw(bad_alloc){
for(int i=0;i<psize;i++)
if(!alloc_map[i]){
out<<"kullanilan blok"<<i<<"....";
alloc_map[i]=true ;
//kullanilani isaretle
return pool+(i*sizeof(Framis)) ;
}
out<<"bellek alan disi"<<endl ;
throw bad_alloc( ) ;
}
void Framis::operator delete(void* m){
if(!m)return ; //boş yeri denetle
//havuzda oluturulduğunu kabul et
//hangi blok ta olduğunu hesapla
unsigned long block=(unsigned long)m-(unsigned long)pool ;
block/=sizeof(Framis) ;
out<<"blok silme"<<block<<endl ;
//bellek boşalmasını işaretle
alloc_map[block]=false ;
}
int main( ){
Framis* f[Framis::psize] ;
try{
for(int i=0;i<Framis::psize;i++)
f[i]=new Framis ;
new Framis ; //bellek alanı dışı
}catch(bad_alloc){
cerr<<"bellek disi"<<endl ;
}
delete f[10] ;
f[10]=0 ;
//boşalan belleği kullan
Framis* x=new Framis ;
delete x ;
for(int j=0;j<Framis::psize;j++)
delete f[j] ;
//f[10] silimi OK
}///:~
psizeFramis nesnelerini tutmak için yeterli büyüklükte byte dizi yerleşimi
yapılarak, bellek havuzunda Framis kümesi oluşturulur. Yerleşim tablosu
psize ögelerinin uzunluğundadır, ve her blok için bir bool değeri vardır. İlk
ögenin değerini belirlerken kullanılan birleşik atama uyanıklığı ile bellek
yerleşim tablosundaki bütün değerler false yapılır. Bundan sonra ise
derleyici bütün hepsine ilk değerleri otomatik normal varsayılan olarak
atar. (bool durumunda false).
Yörel operator new, global new gibi aynı yazım biçimine sahiptir.
Yapılan şey; yerleşim tablosunda false arayıp bulmaktır. Daha sonra o yere
true atayarak yerleşim yapılmakta olduğu gösterilir, ve ilgili bellek
bloğunun adresi geri döndürülür.Yerleşim için bellek yeri bulunamazsa, iz
dosyasına bir ileti gönderilir ve bad_alloc istisnası fırlatılır.
Bu istisnalarla ilgili bu kitapta gördüğünüz ilk örnek. Daha ayrıntılı istisna
çalışmaları ikinci kitaba bırakıldı. Buradaki çok basit bir inceleme.
operator new da iki tane istisna yönetimi eseri bulunmakta; ilki throw
(bad_alloc) ardından gelen işlev değişken listesi. Bu, derleyici ve
okuyucuya, bu işlevin bad_alloc tipinde istisna fırlattığını belirtir. İkincisi
ise; eğer bellekte daha fazla yer yoksa, işlev aslında throw bad_alloc
deyimi ile istisna fırlatır. Bir istisna fırlatıldığı zaman işlev programın
normal çalışmasını durdurur ve denetim istisna yönetimine geçer, bu da
catch tümcesi ile belirtilir.
main( ) de resmin öteki parçası, try-catch tümcesi görülür. try bölümü,
içinde bütün istisna kodlarını kapsayan zincirli ayraçlarla çevrilmiştir. -bu
durumda new'e herhangi çağrı Framis nesnelerini de kapsar. try'ı hemen
takip eden bölümlerde bir veya daha fazla catch tümcesi bulunur, ve
herbiri yakalayacakları istisna tipini belirler. Buradaki durumda catch
(bad_alloc), bad_alloc istisnalarının burada yakalanacağını gösterir. Bu
özel catch tümcesi sadece bad_alloc istisnası fırlatıldığı zaman çalışır, ve
son catch tümcesinin bitmesinden sonra program çalışmaya devam eder.
(burada sadece bir tane olmasına rağmen, daha fazla da bulunabilir) .
Örneğimizde iostreamlerin kullanılması kabul edilebilir zira global new ve
delete'e dokunulmuyor.
operator delete, Framis adresinin bellek havuzunda oluşturulduğunu
kabul ediyor. Kabul, yörel operator new, kümede tek bir Framis nesnesi
oluşturulduğu herhangi bir anda çağrıldığı için uygundur. Ama diziye
değil; global new diziler için kullanılır. Bu yüzden kullanıcı diziyi ortadan
kaldırmak için boş köşeli ayraç kullanmaksızın, kazara operator delete
çağrısı yapabilir. Bu sorunlara yolaçar. Ayrıca kullanıcı yığıt üzerinde
bulunan nesnenin göstergecini
siliyor olabilir. Eğer bunların olduğunu düşünürseniz, adresin havuzda ve
doğru sınırlarda bulunduğunu belirtmek için bir satır ekleyebilirsiniz.
(bindirilmiş new ve delete, bellek kaçaklarının bulunmasında nasıl
yardımcı olur görülüyor.)
operator delete, göstergecin işaret ettiği havuzdaki bellek bloğunu
hesaplar. Ve daha sonra o bellek bloğunun yerleşim tablo bayrağını false
yaparak o bloğun boşaltıldığını belirtir.
main( ) de yeterli sayıda Framis nesnesi dinamik olarak bellek bitene
kadar yerleştirilir; bu da bellek dışında kalma denetimini yapar.Daha sonra
nesnelerden biri bellekten silinir, ve bir ikinci nesne oluşturularak
boşaltılan yerin yeniden kullanıldığı gösterilir.
Bu bellek yerleşim uygulaması Framis nesnelerine özel olduğu için,
varsayılan new ve delete için yapılan genel amaçlı bellek yerleşim
uygulamasından çok daha hızlıdır. Bununla birlikte belirtmeliyiz ki, eğer
kalıtım sözkonusu olursa bu otomatik olarak çalışmaz. (kalıtım 14.
bölümde ayrıntıları ile anlatılacak)
Diziler için new ve delete bindirimi
Bir sınıf için new ve delete işleçlerini bindirime uğratırsanız, sınıfın bir
nesnesini oluşturduğunuz herhangi bir anda işleçler çağrılır. Bununla
birlikte, eğer o sınıfın nesnelerinden bir dizi oluşturulursa, bütün diziyi
saklayan yeterli bellek yerleşimi için global operatornew çağrılır ve yine,
belleği boşaltmak için de global operatordelete çağrılır. Nesne dizilerinin
bellek yerleşimini, sınıfın operator new[] ve operator delete[] özel dizi
sürümleri bindirimleri ile denetleyebilirsiniz. Şimdi verilen örnek iki farklı
sürümün çağrıldığı uygulamadır;
//:B13:ArrayOperatorNew.cpp
//Diziler için new işleci
#include <new>
#include <fstream>
using namespace std ;
ofstream trace("ArrayOperatorNew.out") ;
class Widget{
enum{sz=10;}
int i[sz] ;
public:
Widget( ){trace<<"*";}
~Widget( ){trace<<"~" ;}
void* operator new(size_t sz){
trace<<"Widget::new:"
<<sz<<"bytes "<<endl ;
return::new char[sz] ;
}
void operator delete(void* p){
trace<<"Widget::delete "<<endl ;
::delete []p ;
}
void* operator new[](size_t sz){
trace<<"Widget::new[]: "
<<sz<<"bytes "<<endl ;
return ::new char[sz] ;
}
void operator delete[](void* p){
trace<<"Widget::delete[] "<<endl ;
:: delete []p ;
}
};
int main( ){
trace<<"New Widget "<<endl ,
Widget* w=new Widget ;
trace<<"\ndelete Widget "<<endl ;
delete w ;
trace<<"\nnew Widget[30]"<<endl ;
Widget* wa=new Widget[30] ;
trace<<"\ndelete []Widget "<<endl ;
delete []wa ;
}///:~
Yukarıda new ve delete işleçlerinin global sürümleri çağrıldı ve böylece
new ve delete, eklenmiş iz bilgileri hariç bindirilmiş sürümleri yokmuş
gibi etki oluştu. Tabii, bindirilmiş new ve delete uygulamasında istediğiniz
bellek yerleşim biçimini kullanabilirsiniz.
Gördüğünüz üzere dizilerin new ve delete yazım biçimi tek nesne
sürümleri ile aynıdır, sadece köşeli ayraçlar eklenir.Her iki halde de eldeki
bellek büyüklüğüne uygun yerleşim yapmak zorundasınız. Dizi
uygulamasında eldeki bellek bütün dizi büyüklüğüne uygun olmalıdır.
Akılda tutulması gereken tek şey; gereken bindirilmiş işleç new yeterli
büyüklükte bellek alanını işaretleyen bir göstergeç ile işlemleri yapmak
kullanılır. Bu bellek alanı içinde ilk değer atama işlemi bizzat
gerçekleyebilmenize rağmen, aslında normalde bu işlem yapıcı işlev
tarafından yerine getirilir. İlk değer atama derleyici tarafından otomatik
çağrılan yapıcı işlevin işidir.
Yapıcı ve yıkıcı işlevler karakterleri basit bir şekilde yazdırırlar, bu yüzden
onları çağırırken görebilirsiniz de. Şimdi bir iz dosyasının bir derleyici için
nasıl göründüğüne bakalım;
new Widget
Widget::new: 40 bytes
*
delete Widget
~Widget::delete
new Widget[30]
Widget::new[]:1204 bytes
**********************
delete []Widget
-------------------------------Widget::delete[]
Beklendiği gibi tek bir nesne oluşturmak için 40 byte gerektirmektedir.
(makinemiz int tipi için 4 byte yer ayırmıştır).
operator new çağrılır daha sonra da yapıcı işlev (* işareti ile gösterilir).
Yapıcı işlev işlemlerini tamamlamak için delete çağrısı yıkıcı işlevi
çağrırır, daha sonra da operator delete.
Widget nesnelerinin dizisi oluşturulduğu zaman, operator new'in
söylendiği gibi dizi sürümü kullanılır. Ama farkettiğiniz gibi gereken
bellek alanı, beklenenden 4 byte daha fazladır. Bu fazladan 4 byte,
sistemin dizi hakkında bilgiyi tuttuğu yerdir, özellikle de dizideki nesne
sayısını. Bu şekilde yazdığınızda söylediğiniz;
delete []Widget ;
Köşeli ayraçlar derleyiciye bunun bir nesneler dizisi olduğunu belirtir. Bu
nedenle derleyici dizideki nesne sayısını arayan kod üretir ve yıkıcı işlevi o
sayıda çağırır. Görebileceğiniz gibi operator new ve operator delete
dizisi bütün dizi topluluğu için sadece bir kere çağrılsa bile, dizideki her
nesne için varsayılan yapıcı ve yıkıcı işlev çağrılır.
Yapıcı işlev çağrıları
Aşağıdaki yazımı gözönüne alalım;
MyType* f=new MyType ;
MyType büyüklüğünde saklama alanı yerleşimi için new çağrılır, daha
sonra o bellek alanında MyType yapıcı işlevi çalıştırılır. Peki saklama
alanında new iş görmezse ne olur?. Bu durumda yapıcı işlev çağrılmaz,
böylece başarısız bir şekilde oluşturulmuş hala bir nesne vardır, en azından
yapıcı işlev uyarılamaz ve this göstergeçin yerine sıfır kullanılır. Bunu
gösteren bir örnek verelim;
//:B13:NoMemory.cpp
//new iş görmediğinde yapıcı işlev çağrılmaz
#include <iostream>
#include <new>
//bad_alloc tanımı
using namespace std ;
class NoMemory{
public:
NoMemory{
cout<<"NoMemory::NoMemory( )"<<endl ;
}
void* operator new(size_t sz) throw(bad_alloc){
cout<<"NoMemory::operator new"<<endl ;
throw bad_alloc( ) ; //"bellek alanı dışı
}
};
int main( ){
NoMemory* nm=0 ;
try{
nm=new NoMemory ;
}catch(bad_alloc){
cerr<<"Bellek disi istisnasi"<<endl ;
}
cout<<"nm= "<<nm<<endl ;
}///:~
Program çalıştığı zaman yapıcı işlevin iletisi yazılmaz, sadece operator
new ve istisna yöneticisinden ileti gelir. new hiçbir zaman geri dönmediği
için yapıcı işlev hiç bir zaman çağrılmaz ve böylece onun iletisi de
yazılmaz.
new açıklaması hiç tamamlanmadığı için nm ilk değerinin sıfırlanması
önemlidir, yanlış kullanmamak ve emniyete almak için göstergeci
sıfırlamak lazımdır. Bununla birlikte istisna yönetiminde ileti yazdırmaktan
daha fazlasını yapmanız lazım ve sanki nesne başarılı bir şekilde
oluşturulmuş gibi devam etmelisiniz. İdeal olan programın sorundan
kurtulmasını sğalayan birşeyler yapmanızdır, en azından bir hata
yapmadan programdan çıkılmalıdır.
C++ başlangıç sürümlerinde sakalama yeri yerleşimi yapılamadığında,
new den sıfıra dönmek pratik bir uygulamaydı. Hiç olmazsa yapıcıyı
engelliyordu. Bununla birlikte, new'den sıfıra dönüşü Standart yapılı bir
derleyici ile yapmaya çalışırsanız, onun yerine size, bad_alloc fırlatmanızı
söylemesi lazım.
new
ve delete yerleşimi
Daha az kullanılan iki tane daha, operator new bindirimi bulunmaktadır.
1--Bellekte belli bir yere bir nesne yerleştirmek isteyebilirsiniz. Nesnenin
donanımın bir parçası ile eşanlamlı olduğu, donanım yönelimli tekleşik
(embedded) dizgelerde bu özellikle önemlidir.
2--new çağrımı sırasında farklı yerleştiriciler arasından seçim yapabilmek
isteyebilirsiniz.
Yukarıdaki her iki durum aynı mekanizma ile çözülür; bindirilmiş
operator new birden fazla değişken alabilir. Daha önce gördüğünüz gibi
ilk değişken her zaman nesne büyüklüğüdür, derleyici tarafından gizlice
hesaplanır ve aktarılır. Ama öteki değişkenler istediğiniz herhangi bir şey
olabilir, -nesnenin yerleştirileceği yer, bellek yerleşim işlev ya da nesnenin
dayancı veya size uyan başka bir şey olabilir.
Çağrı sırasında operator new'e ilave değişkenleri aktarmak başlangıçta
acayip görünebilir. Değişken listesi new anahtar kelimesinden sonra,
(size_t değişkeni olmaksızın, zira o derleyici tarafından kullanılır)
oluşturulacak nesnenin sınıf adından önce yazılır. Örnek verelim;
X* xp=new(a) X ;
a, operator new işlecine ikinci değişken olarak aktarılır. Tabii, sadece
eğer böyle bir operator new işleci bildirilirse, bu çalışabilir.
Şimdi belli bir yere nasıl nesne yerleştirileceğini örnekle gösterelim;
//:B13:PlacementOperatorNew.cpp
//operator new ile yerleşim
#include <cstddef> //size_t
#include <iostream>
using namespace std ;
class X{
int i ;
public:
X(int ii=0):i(ii){
cout<<"this= "<<this<<endl ;
}
~X( ){
cout<<"X::~X( ): "<<this<<endl ;
}
void* operator new(size_t, void* loc){
return loc ;
}
};
int main( ){
int l[10] ;
cout<<"l= "<<l<<endl ;
X* xp=new(l) X(50) ;
//X l alanında yer alıyor
xp->X::~X( ) ;
//aleni yıkıcı işlev çağrısı
//sadece yerdeğişimi ile kullan
}///:~
Farkettiğiniz üzere operator new sadece kendine aktarılan göstergeçi geri
döndürür. Böylece çağıran nesnenin nereye yerleşeceğine karar verir.Ve
yapıcı işlev new açıklamasının parçası olarak bellek yeri için çağrılır.
Örneğimizde sadece bir ilave değişken olmasına rağmen, siz gerek olursa
daha fazla değişken kullanabilirsiniz. Sizi engelleyen bir şey yoktur.
Nesneyi yoketmeye kalktığınız zaman ikilemle karşılaşırsınız. Zira delete
sadece bir sürüme sahiptir. Ve sizin "bu nesne için benim özel yer
temizleyicimi kullan " deme olanağınız yok. Yıkıcı işlevi çağırmanız
lazım, ama belleğin dinamik bellek yerleşim mekanizması ile
boşaltılmasını istemezsiniz, zira küme üzerine yerleşim olmamıştır. Yanıt
çok özel bir yazım biçimidir. Aşağıdaki gibi açık biçimde yıkıcı işlevi
çağırırsınız;
xp->X::~X( ) ;
//açık yıkıcı işlev çağrısı
Yazım biçiminin katı bir düzeni vardır. Bazı insanlar bu uygulamayı bazı
durumlarda görünümün bitmesinden önce nesnelerin yokedilmesi yolu
olarak görürler. Aslında eğer nesne ömrü çalışma sırasında belirlenecekse,
ya görünüm ayarlanmalı ya da (daha doğrusu) dinamik nesne oluşumu
kullanılmalıdır. Yığıt üzerinde oluşturulmuş nesneler için bu yolla yıkıcı
işlevi çağırmak sorun çıkarır, zira yıkıcı işlev görüntü sonunda tekrar
çağrılacaktır. Küme üzerinde oluşturulmuş nesne için bu yolla yıkıcı işlevi
çağırırsanız, yıkıcı işlev çalışır fakat bellek boşalmaz.Bu herhalde
istemediğiniz bir durumdur. Bu yolla açık bir biçimde yıkıcı işlevin
çağrılabilmesinin tek nedeni; operator new yerleştiren yazım biçimini
desteklemektir.
new açıklamasını yerleştiren yapıcı işlev bir istisna fırlatırsa sadece
çağrılan bir ayrıca yerleştiren operator delete bulunur.( istisna sırasında
otomatik olarak bellek silinir) .Yerleşen operator delete yerleşen operator
new de bulunana değişkenlere karşılık gelen değişkenler listesine
sahiptir.operator new yapıcı işlev istisna fırlatmadan önce çağrılır
unutmayalım. Bu konu ikinci kitapta istisna yönetiminde daha ayrıntılı
incelenecek.
Özet
Yığıtta nesnelerin otomatik olarak oluşturulması hem uygun hemde
verimlidir. Ama program çalışırken herhangi bir anda, nesnelerin
oluşturulması ve silinmesi programlama genel sorunu, mutlaka
çözülmelidir, özellikle dışarıdan kaynaklanan bilgilenmeye yanıt verme
açısından. C dili dinamik bellek yerleşimi için kümeden saklama alanı
kullanmasına rağmen bu C++ dili için gereken güvenli yapı ve kullanım
kolaylığını sağlamaz. C++ dilinde dinamik nesne yapılanmaları dil
çekirdeğine yerleştirilen new ve delete anahtar kelimeleri ile yapılır.
Böylece nesneler, yığıtta gibi kolayca, kümelerde yapılandırılırlar. Ayrıca
büyük bir esneklikte sağlanmış olur. Eğer ihtiyaçlarımızı yerine
getirmezler ve yeterli derecede verimli olmazlarsa new ve delete'in
davranışlarını değiştirebiliriz. Ayrıca kümenin saklama değerleri aşılırsa
olanları değiştirebilirsiniz.
14. BÖLÜM
Kalıtım ve Harç
C++ dilinin en yararlı özelliklerinden biri kodun yeniden kullanımıdır.
Yeni bir dilde devrim yapmak için, kod kopyalamaktan daha fazlası,
değiştirme de yapılabilmelidir. C dilinin yaklaşımı buna uygun düşmez.
C++ dilinde ise, herşeyde olduğu gibi çözüm; sınıf etrafında olur. Yeni
sınıflar oluştururken kodlar yeniden kullanılır, ama sıfırdan oluşturmak
yerine, başka birinin daha önceden hazırlayıp bakımını yaptığı hazır
sınıflar kullanılır.
Sınıfları kullanırken dikkat edilmesi gereken husus; onları bozmamaktır.
Bu bölümde bunu başarmanın iki yolunu açıklayacağız. Birinci yol
oldukça kolaydır; varolan sınıfın nesnelerini yeni sınıf içinde kullanmaktır.
Yeni sınıf, varolan sınıfların nesnelerinden oluşturulduğu için buna
"harç=tertip" denir.
İkinci yol ise daha fazla ayrıntı gerektirmektedir. Bu usulle yeni sınıf,
varolan sınıfın tipi olarak oluşturulur. Hazır sınıfın yazılı kurgusu alınır, ve
ona yeni kodlar eklenir. Dikkat edilecek husus; hazır sınıfta değişiklik
yapmamaktır. Bu afsunlu etkinlik kalıtım olarak adlandırılır. Aslında işin
çoğu derleyici tarafından yapılır. Kalıtım nesne yönelimli programlamanın
köşetaşlarından birisidir, konuyla ilgili daha fazla bilgi, bir sonraki
bölümde verilecektir.
Görüleceği üzere hem harç, hem kalıtım da yazım biçimi ve davranışlar
oldukça benzerler. (her ikisi de varolan tiplerden yeni tipler oluştururlar).
Bu bölümde kodların yeniden kullanım mekanizmalarını inceleyeceğiz.
Yeniden kod kullanımdan kastettiğimiz uzantısı .o olan hedef kodların
yeniden kullanımıdır. Doğrudan kaynak kodların yeniden kullanımı ise,
kalıp (template=şablon) sınıfların konusu olup, en son bölümde ele
alınacaktır.
Harcın yazım biçimi
Aslında sınıf oluşturma süreci boyunca hep harç kullanırsınız. İlk sınıflar,
başlangıçta yerleşik tipler harçlanarak oluşturulur. (ve bazan da string ile).
Kullanıcı tanımlı tiplerle de harçlama oldukça kolaydır. Yararlı olacak bir
sınıf oluşturalım;
//:B14:Useful.h
//yeniden kullanılacak bir sınıf
#ifndef USEFUL_H
#define USEFUL_H
class X{
int i ;
public:
X( ){i=0;}
void set(int ii){i=ii;}
int read( ) const{return i;}
int permute( ){return i=i*50;}
};
#endif //USEFUL_H///~
Yukarıdaki sınıfta veri üyeleri private olduğu için, yeni bir sınıfta X
tipinin nesnesini public olarak yerleştirmek tam anlamı ile güvenlidir. Bu
uygulama arayüzü de basitleştirir;
//B:14:Composition.cpp
//harçlama ile kodun yeniden kullanımı
#include "Useful.h"
class Y{
int i ;
public:
X x ; //nesne gömülmesi
Y( ){i=0 ;}
void f(int ii){i=ii;}
int g( ) const {return i ;}
};
int main( ){
Yy;
y .f(50) ;
y.x.set(40) ; //gömülü nesneye erişim
}///:~
Gömülü nesnenin üye işlevlerine erişim (altnesne olarak bilinir) ikinci
nesne seçimini gerektirir.
Gömülü nesneleri private yapmak daha yaygın rastlanır. Bu sayede onlar
da uygulamanın bir parçası olurlar. (isterseniz uygulamayı
değiştirebilirsiniz demektir.). Yeni sınıfın public arayüz işlevleri gömülü
nesne kullanımını da kapsar. Fakat nesne arayüzünü taklit etmeleri
gerekmez.:
//:B14:Composition2.cpp
//private gömülü nesneler
#include "Useful.h"
class Y{
int i ;
X x ; //gömülü nesne
public:
Y( ){i=0 ;}
void f(int ii){i=ii ; x.set(ii) ;}
int g( ) const {return i*x.read( ) ; }
void permute( ){x.permute( ) ;}
};
int main( ){
Yy;
y.f(50) ;
y.permute( ) ;
}///:~
Burada görüldüğü üzere permute( ) işlevi yeni sınıfın arayüzüne başarı ile
taşınmıştır, ama X in öteki üye işlevleri Y üyeleri içinde kullanılmıştır.
Kalıtım yazım biçimleri
Harçlama sırasında kullanılan yazım biçimi gayet basit ve açıktı ama,
kalıtımı gerçekleyen yazım biçimi yeni ve farklıdır.
Kalıtımı kullandığınız zaman söylediğiniz; yeni sınıf eskisi gibidir. Bu
genellikle sınıfın ismini kod içine yerleşirerek, sınıf gövdesini açan zincirli
ayraçtan önce yapılır. Önce iki nokta üstüste konur daha sonra ana sınıfın
adı yazılır (veya çoklu kalıtım için virgülle ayrılmış ana sınıfların adları
yazılır). Bu işlem yapılınca ana sınıfın veri üyeleri ve üye işlevleri,
otomatik olarak yeni sınıfa alınmış olur. Örnek verelim:
//:B14:Inheritance.cpp
//basit kalıtım
#include "Useful.h"
#include <iostream>
using namespace std ;
class Y:public X{
int i ;
//X in i sinden farklı
public:
Y( ){i=0 ;}
int change( ){
i=permute( ) ; //farklı isim çağrısı
return i ;
}
void set(int ii) {
i=ii ;
X::set(ii) ; //aynı adda işlev çağrısı
}
};
int main( ){
cout<<"sizeof(X)= "<<sizeof(X)<<endl ;
cout<<"sizeof(Y)= "<<sizeof(Y)<<endl ;
YD;
D.change( ) ;
//X işlev arayüzü gelir
D.read( ) ;
D.permute( ) ;
//yeniden tanımlanan işlevler ana sınfınkileri gizler
D.set(22) ;
}///:~
Y, gördüğünüz gibi X sınıfından türetiliyor. Bunun anlamı Y sınıfı X
sınıfının bütün veri üyelerini ve üye işlevlerini kullanabilir demektir.
Aslında Y, sanki X ten türetilme değilde Y içinde X in üye nesnesi
oluşturulmuş gibi X in bir alt nesnesini içeririr. Hem üye nesneler hem de
ana sınıf saklama sınıfı, alt nesne olarak kabul edilir.
X te private olarak bulunan bütün ögeler Y de de private'dir. Bunun
anlamı; X ten türetildiği için Y koruma mekanizmasını değiştirmez. X in
private ögeleri hala yerlerinde bulunur, ve onların kapladığı bellek
alanlarına doğrudan erişemezsiniz.
main( ) de gördüğünüz gibi Y nin veri ögeleri ile X in veri ögeleri
biraradadırlar. Zira sizeof(Y), sizeof(X) in iki katı büyüklüktedir.
Farkettiğiniz üzere ana sınıf adından önce public kelimesi var. Kalıtım
sırasında ana sınıftan önce public ifadesi yazılmazsa, herşey private
olarak varsayılır, yani ana sınıfta public olan bütün üyeler türetilmiş sınıfta
private olurdu. Bu hemen hemen hiç bir zaman istenmez. Arzulanan; ana
sınıfta public olan bütün üyelerin, türetilmiş sınıfta da public olmalarıdır.
İşte bu, kalıtım sırasında ana sınıf adı önüne public yazılarak sağlanır.
change( ) işlevinde ana sınıf işlevi permute( ) çağrılmaktadır. Türetilmiş
sınıf ana sınıfın bütün public işlevlerine doğrudan erişir.
Türetilmiş sınıfta bulunana set( ) işlevi ana sınıtaki set( ) işlevinin yeniden
tanımlanmışıdır. Yani Y tipinin bir nesnesi için read( ) ve permute( )
işlevlerini çağırırsanız, bu işlevlerin ana sınıf sürümlerini kullanırsınız.
( bunun main( ) işlevi içinde oluştuğu görülür). Fakat Y nesnesi için set( )
işlevini çağırırsanız, o zaman yeniden tanımlanmış sürümü kullanırsınız.
Bunun anlamı; kalıtım sırasında, hoşlanmadığınız bir işlev sürümünü
değiştirebilirsiniz demektir. ( veya tamamen farklı yeni işlevler
tanımlayabilirsiniz change( ) gibi).
Bununla birlikte bir işlevi yeniden tanımladığınız zaman bile, hala ana
sınıf sürümünü çağırabilirsiniz . Eğer set( ) işlevi içinde set( ) işlevini
çağırırsanız işlevin yörel sürümünü elde edersiniz, buna kendini çağıran
işlev veya özyinelemeli işlev denir. Ana sınıf sürümünü çağırmak için ana
sınıf adını açıkça belirtmeli ve görünüm duyarlık işlecini kullanmalısınız.
Bu zorunludur.
Yapıcı işlev ilk değer atama listesi
C++ da ilk değer atamalarının ne kadar önemli olduğunu görmüştünüz,
aynı önem harçlama ve kalıtım içinde farklı değildir.Bir nesne
oluşturulduğu zaman, derleyici onun bütün alt nesneleri için yapıcı
işlevleri çağırmayı güvenceye alır. Örneklerde bütün alt nesneler için
varsayılan yapıcı işlevler bulunmaktadır ve bunları derleyici otomatik
olarak çağırır. Peki eğer alt nesneler varsayılan yapıcı işlevlere sahip
değilse ne olur, veya yapıcı işlevde varsayılan değişkeni değiştirmek
isterseniz ne olur?. Bu bir sorundur zira, yeni sınıf yapıcı işlevi alt
nesnenin private veri ögelerine erişim iznine sahip değildir, bu yüzden
bunlara ilk değer atamalarını yapamaz.
Çözüm aslında basittir; alt nesne için yapıcı işlevi çağırın. C++ dilinde bu
iş için özel yazım biçimi vardır; yapıcı işlev ilk değer atama listesi. İlk
değer atama listesinin biçimi kalıtım eylemini yansıtır. Kalıtımla, iki
noktadan sonra ve sınıf gövdesinin başladığı zincirli ayraçtan önce ana
sınıflar konur. Yapıcı işlev ilk değer atama listesinde, alt nesne yapıcı
işlevlerine çağrıları, yapıcı işlev değişken listesinden ve iki noktadan
sonra, ama işlev gövdesini açan zincirli ayraçtan önce konur. Bar'dan
türetilmiş MyType sınıfı için yazım aşağıdaki gibidir;
MyType::MyType(int i) : Bar(i) {///...
Eğer Bar daki yapıcı işlevde tek int değişken varsa.
Üye nesne ilk değer ataması
Gördüğünüz gibi bu usul harçlamada ki üye nesne ilk değer ataması için
kullanılan yazım biçimine çok benzemektedir. Harçlamada sınıf adları
yerine nesne adları kullanılmaktadır.Eğer ilk değer atama listesinde birden
fazla yapıcı işlev çağrısı varsa, çağrılar virgüllerle ayrılır.
MyType2::MyType2(int i) : Bar(i), m(i+1){///...
Bu Bar'dan türetilmiş MyType2 sınıfının yapıcı işlev başlangıcı olup, m
adında üye nesneyi de yanında bulundurmaktadır. Yapıcı işlev ilk değer
atama listesindeki ana sınıfın tipini görebiliyorken, sadece üye nesne
belirtecini görebilirsiniz.
İlk değer atama listesindeki yerleşik tipler
Yapıcı işlev ilk değer atama listesi üye nesneler için yapıcıların açıkça
çağrılmasını sağlar. Aslında zaten, yapıcıları çağırmanın başka bir yolu da
bulunmamaktadır. Burada ana fikir; yeni sınıfın yapıcı gövdesinden önce
yapıcıları çağırmaktır. Bu sayede alt nesnelerin üye işlevlerine yapılan her
çağrı her zaman ilk değeri atanmış nesnelere gider. Yapıcı işlevin zincirli
ayraç açılışında bütün üye nesneler ve ana sınıf nesneleri çağrılarının biraz
yapıcısız elde yolu bulunmamaktadır, hatta derleyici varsayılan yapıcı
işleve gizli çağrı yapsa bile. Bu C++ dilinin ileri düzeyde sağladığı
güvencedir. Hiçbir nesne (veya nesne parçası) yapıcı işlevi çağrılmaksızın
başlangıç kapısının dışına çıkamaz.
Yapıcı işlevin zincirli ayraçının açılması sırasında bütün üye nesnelere ilk
değer atamaları yapılması, dilin sağladığı bir kolaylıktır. Bir kez zincirli
ayraç açıldığında bütün alt nesnelerin ilk değerlerinin doğru atandığı kabul
edilebilir ve yapıcı işlevlerde yapılması istenilen özel işlere odaklanılır.
Bununla beraber aksayan bir şey var; yapıcı işlevleri olmayan yerleşik veri
tiplerinin üye nesneleri ne olacak?.
Yazım biçimini tutarlı yapmak için, yerleşik tiplerin tek değişken alan, tek
yapıcı işlevi varmış gibi işlem yapmasına izin verilir; aynı tipin bir
değişkeni ilk değerini atadığınız değişkeni gibidir. Böylece aşağıdakilerini
söyleyebilirsiniz,
//:B14:PseudoConstructor.cpp
class X{
int i ;
float f ,
char c ;
char* s ;
public:
X( ) : i(8), f(3.56), c('z'), s("nehaber"){}
};
int main( ){
Xx;
int i(500) ; //normal tanıma uygulanır
int* ip=new int(150) ;
}///:~
Yardımcı yapıcı işlev (Pseudoconstructor) çağrıları basit atama işlemlerini
gerçekler. Uygun bir teknik olup iyi kodlama biçimidir. Bu yüzden sıklıkla
kullanılır.
Hatta sınıf dışında yerleşik tip değişkeni oluştururken de yardımcı yapıcı
işlev yazım biçimini kullanmak olasıdır;
int i(500) ;
int* ip=new int(150) ;
Bu yerleşik tiplerin davranışını biraz daha fazla nesnelere benzetir. Yalnız
bunların gerçek yapıcı işlev olmadığını unutmayın. Özellikle eğer bir
yardımcı yapıcı işlev çağrısı yapmazsanız, ilk değer ataması yapılmaz.
Harç ve kalıtımın birleştirilmesi
Elbette harç ve kalıtım bir arada kullanılabilir. Aşağıdaki örnek, her
ikisinin birarada bulunduğu karmaşık bir sınıfı
göstermektedir;
//:B14:Combined.cpp
//harç ve kalıtım
class A{
int i ;
public:
A(int ii) : i(ii) {}
~A( ) ;
void f( ) const {}
};
class B{
int i ;
public:
B(int ii) : i(ii){}
~B( ) ;
void f( ) const {}
};
class C:public B{
Aa;
public:
C(int ii) : B(int ii), a(ii){}
~C( ) ;
//~A( ) ve ~B( ) çağrılır
void f( ) const {//yeniden tanım
a.f( ) ;
B::f( ) ;
}
};
int main( ){
C c(150) ;
}///:~
C sınıfı B sınıfından türetilmiştir ve A sınıfının üye nesnesini (yani harç
olmuştur) bünyesinde bulundurmaktadır. Yapıcı işlev ilk değer atama
listesi; hem ana sınıf yapıcı işlev hemde üye nesne yapıcı işlev çağrılarını
kapsar.
İşlev C::f( ) işlev B::f( ) yi yeniden tanımlar, zaten kendisi ondan
türetilmiştir, ayrıca aynı bölümde ana sınıf sürümü de çağrılır. Ek olarak
a.f( ) de çağrılır. Farkedeceğiniz gibi işlevlerin yeniden tanımı ile ilgili
konuşulacak tek yer kalıtımdır; üye nesne ile sadece nesnenin public
arayüzü ile işlemleme yapabilirsiniz, yeniden tanımlayamazsınız. Ek
olarak, C sınıfının bir nesnesi için f( ) çağrımında, eğer C::f( )
tanımlanmamışsa, a.f( ) çağrılamazdı, ama B::f( ) çağırabilirdi.
Yıkıcı işlevin otomatik çağrımı
İlk değer atama listelerinde genellikle açık biçimde yapıcı işlev çağrıları
olmasına rağmen, herhangi bir sınıf için sadece tek yıkıcı işlev olduğundan
açık biçimde yıkıcı işlev çağrıları hiçbir zaman gerekmez, ayrıca yıkıcılar
herhangi bir değişkende almazlar. Bununla birlikte derleyici hala yıkıcı
işlev çağrımlarını güvence altında tutar, yani bunun anlamı; hiyerarşinin
bütününde yeralan yıkıcı işlevlerin tamamını en son türetilmiş olandan en
baştaki köke kadar hepsi derleyici güvencesindedir. Burada önemle
vurgulamalıyız ki hiyerarşide yeralan her öge için yapıcıların ve yıkıcıların
çağrımı pek alışılmış değildir, oysa normal üye işlevle sadece o işlev
çağrılır, ama ana sınıf sürümlerinden herhangi biri değil. Eğer ayrıca çok
kullandığınız normal bir üye işlevin ana sınıf sürümünü çağırmak
isterseniz, bunu mutlaka açık biçimde belirtmelisiniz.
Yapıcı ve yıkıcı işlevlerin düzenlenmesi
Bir nesne çok sayıda altnesneye sahip olduğu zaman yapıcı ve yıkıcı işlev
çağrılarının sıra ve düzenlerini bilmek ilginçtir. Aşağıdaki örnek bunları
göstermektedir;
//:B14:Order.cpp
//yapıcı/yıkıcı sırası
#include <fstream>
using namespace std ;
ofstream out("Order.out") ;
#define CLASS(ID) class ID{\
public:\
ID(int){outy<<#ID" constructor \n" ;}\
~ID( ){out<<#ID"destructor \n" ;}\
};
CLASS(Base1);
CLASS(Member1) ;
CLASS(Member2) ;
CLASS(Member3) ;
CLASS(Member4) ;
class Derived1 : public Base1{
Member1 m1 ;
Member2 m2 ;
public:
Derived1(int) : m2(1), m1(2), Base1(3){
out<<"Derived1 constructor\n" ;
}
~Derived1( ){
out<<"Derived1 destructor\n" ;
}
};
class Derived2 : public Derived1{
Member3 m3 ;
Member4 m4 ;
public:
Derived2( ) : m3(1), derived2(2), m4(3){
out<<"Derived2 constructor\n" ;
}
~Derived2{
out<<"Derived2 destructor\n" ;
}
};
int main( ){
Derived2 d2 ;
}///:~
Önce bir ofstream nesnesi bütün çıktıların bir dosyaya gönderilmesi için
oluşturulur. Daha sonra yazımı korumak ve ilerde ki bölümlerde daha da
geliştirilecek olan makro tekniğini göstermek için, bazı sınıfları inşa etmek
amacı ile bir makro oluşturulur ve daha sonra harç ile kalıtımda kullanılır.
Yapıcı ve yıkıcıların herbiri, kendileri hakkında bilgileri iz dosyalarına
aktarırlar. Buradaki yapıcılar varsayılan yapıcı işlevler değildir; herbiri bir
int değişken bulundurur. Değişkenin kendisinde belirteç bulunmaz; varlık
nedeni sadece sizi, ilk değer atama listesindeki yapıcıların çağrımına
zorlamak içindir. (Belirteç olmaması, derleyicinin uyarı göndermesini
engellemek içindir.). Yukarıdaki programın çıktısı aşağıdaki gibidir;
Base1 constructor
Member1 constructor
Member2 constructor
Derived1 constructor
Member3 constructor
Member4 constructor
Derived2 constructor
Derived2 destructor
Member4 destructor
Member3 destructor
Derived1 destructor
Member2 destructor
Member1 destructor
Base1 destructor
Gördüğünüz gibi yapım aşamaları sınıf hiyerarşisinde ki kökten
başlamaktadır, ve her seviyede önce ana sınıf yapıcı işlevi çağrılmakta, üye
nesne yapıcı işlevler takip etmektedir. Yıkıcı işlevler ise yapıcı işlevlerin
sıralamasının tam tersi düzende çağrılmaktadır. -olası bağımlılıklardan
ötürü bu çok önemlidir (türetilmiş sınıfta yapıcı veya yıkıcı da, mutlaka
ana sınıf altnesnelerin hala kullanılabileceğini kabul etmelisiniz , ve yapıcı
ile inşa edilebilir olmalılar ama henüz yokedilmemiş olmalılar)
Üye nesneler için yapıcı işlevlerin çağrılma sıraları yapıcı işlev ilk değer
atama listelerindeki sıradan etkilenmeden olması ayrıca oldukça ilginçtir.
Sıra, sınıfta bildirilmiş üye nesnelerin sıralamasına göre belirlenir. Eğer
yapıcı işlev ilk değer atama listesindeki sıralama ile yapıcı işlev çağrı sırası
değiştirilseydi, iki farklı yapıcıda iki farklı çağrı sırası ortaya çıkardı.
Böylece zavallı yıkıcı işlev silme işlemi sırasında, yapıcıya göre ters
düzendeki işlemini nasıl yapacağını bilemezdi. Ve sonunda bağımlılık
sorunu ortaya çıkardı.
İsim gizleme
Bir sınıf türetilip, üye işlevlerinden birine yeni bir tanım verilirse, ortaya
iki olasılık çıkar. Birincisi; ana sınıfta olduğu gibi türetilmiş sınıf
tanımında da geri dönüş tipi ve tam açıklaması verilir. Bunun adı normal
üye işlevlerin yeniden tanımı (redefining) ve ana sınıf işlevleri virtual
işlev olduğu zaman da baskınlıktır (overriding). virtual işlevler bir sonraki
bölümde incelenecek. Ama eğer türetilmiş sınıfta üye işlev değişken listesi
veya geri dönüş tipi değiştirilirse ne olur?. İşte bir örnek:
//:B14:NameHiding.cpp
//bindirilmiş adların kalıtım sırasında gizlenmesi
#nclude <iostream>
#include <string>
using namespace std ;
class Base{
public:
int f( ) const{
cout<<"Base::f( )\n" ;
return 1 ;
}
int f(string) const{return 1 ;}
void g( ){}
};
class Derived1 :public Base{
public:
void g( ) const{}
};
class Derived2 : public Base{
public:
//yeniden tanım
int f( ) const{
cout<<"Derived2::f( )\n" ;
return 2 ;
}
};
class Derived3 : public Base{
public:
//geri dönüş tipini değiştir
void f( ) const{cout<<"Derived3::f( ) \n" ;}
};
class Derived4 : public Base{
public:
//değişken listesini değiştir
void f(int) const{
cout<<"Derived4::f( )\n" ;
return 4 ;
}
};
int main( ){
string s("merhaba") ;
Derived1 d1 ,
int x=d1.f( ) ;
d1.f(s) ;
Derived2 d2 ,
x=d2.f( ) ;
//! d2.f(s) ;
//gizlenmiş string sürümü
Derived3 d3 ;
//! x=d3.f( ) ; //int geri dönüş tipi gizlenmiş
Derived4 d4 ;
//! x=d4.f( ) ;
//f( ) sürümü gizlenmiş
x=d4.f(1) ;
}///:~
Base sınıfında bindirilmiş f( ) işlevi bulunmaktadır. Derived1 sınıfı Base
de herhangi bir değişiklik yapmaz. Ama g( ) işlevini yeniden tanımlar.
main( ) işlevinde, bindirilmiş f( ) işlevinin her iki sürümü de Derived1 de
bulunur. Bunun yanında, Derived2 de bindirilmiş f( ) işlevinin bir
sürümünün yeniden tanımı verilir, ama sadece biri, öteki yeniden
tanımlanmaz. Ve sonuç olarak ikinci bindirilmiş biçim elde bulunmaz.
Derived3 te değişen geri dönüş tipleri her iki ana sınıf sürümünü de gizler.
Derived4 te ise değişen değişken listesi her iki ana sınıf sürümlerini de
gizler. Genel olarak ana sınıf bindirilmiş işlevleri, herhangi bir zamanda
yeniden tanımlanabilir. Öteki sürümlerin hepsi, yeni sınıfların içinde
otomatik olarak gizlenir. Bir sonraki bölümde virtual anahtar kelimesinin
işlev bindirimi üzerindeki etkisi incelenecek.
Ana sınıfın arayüzünü ana sınıftaki üye işlevin geri dönüş tipi /veya tam
açıklamasında yapılacak değişiklikle değiştirilirse, o zaman sınıf kalıtımda
planlanandan daha farklı kullanılır. Ayrıca bu uygulama illa yanlış
yapıldığını göstermez, kalıtımın zaten nihai hedefi çokbiçimliliği
desteklemektir, eğer işlev açıklaması/veya geri dönüş tipi değiştirilirse o
zaman aslında ana sınıfın arayüzü değiştirilmiş demektir. Planlan baştan bu
ise, o zaman temel olarak kalıtım, kodun yeniden kullanımı için
hazırlanmış demektir, yani ana sınıfın ortak arayüzünü sürekli kullanmak
için değil. (çokbiçimliliğin temeli budur) . Genel olarak kalıtım bu şekilde
kullanılırsa, anlamı; genel amaçlı bir sınıf alınır ve o özel bir gereksinimi
yerine getirir, -genellikle, ama her zaman değil, yani harç gerçeği
gözönüne alınırsa değil.
Örnek olarak 9. bölümdeki Stack sınıfını ele alalım. Bu sınıfta karşılaşılan
sorunlardan biri; kaptan her göstergeç getirildiğinde tip dönüştürülmesi
(yani döküm yapılması) yapılma zorunluluğuydu. Bu işlem sadece angarya
değil, aynı zamanda emniyetsizdi. Göstergeç istenirse, istenilen herhangi
bir tipe dönüştürülebilirdi.
İlk bakışta daha iyi görünen şimdiki yaklaşım genel Stack sınıfını kalıtım
kullanarak bir yönde uzmanlaştırıyor. Örneğimiz 9 .bölümün sınıfını
kullanıyor;
//:B14:InheritStack.cpp
//Stack sınıfını uzmanlaştırmak
#include "../B09/Stack4.h"
#include ".../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
class StringStack : public Stack{
public:
void push(string* str){
Stack::push(str) ;
}
string* peek( ) const{
return (string*) Stack::peek( ) ;
}
string* pop( ){
return (string*) Stack::pop( ) ;
}
~StringStack( ){
string* top=pop( ) ;
while(top){
delete top ;
top=pop( ) ;
}
}
};
int main( ){
ifstream in("InheritStack.cpp") ;
assure(in,"InheritStack.cpp") ;
string line ;
StringStack textlines ;
while(getline(in,line)){
textlines.push(new string(line)) ;
string* s ;
while((s=textlines.pop( ))!=0){//döküm yok
cout<<*s<<endl;
delete s ;
}
}///:~
Stack4.h de bulunan bütün üye işlevler satıriçi olduğu için ilişim için
hiçbir şeye gerek olmaz.
StringStack, Stack'i öyle uzmanlaştırırki, push( ) işlevi sadece String
göstergeçlerini kabul eder. Daha önce Stack void göstergeçleri kabul
ediyordu, böylece kullanıcı uygun göstergeç konup konmadığını anlamak,
için tip sınaması yapmıyordu. İlaveten pop( ) ve peek( ) işlevleri void
yerine String göstergeçler geri döndürür, böylece göstergeçler için tip
dönüşümü (döküm) gerekmez.
Bu ek tip sınama peek( ) pop( ) ve push( ) için rahatlıkla yapıldığı gibi
verilen güvence yeterlidir de. Derleyiciye verilen ek tip bilgileri derleme
sırasında kullanılır ama, işlevler satıriçi olduklarından ek kod üretilmez.
Burada isim gizleme rol oynar zira özellikle push( ) işlevi farklı bir
açıklamaya sahiptir; değişken listesi farklıdır. Eğer aynı sınıf içinde push( )
işlevinin iki farklı sürümü olsaydı, bindirim olurdu, fakat bu durumdaki
bindirim istenilen bir şey olmazdı, zira hala push( ) işlevine herhangi bir
göstergeç, void* olarak aktarılabilmektedir. Allahtan C++, türetilmiş
sınıfta tanımlanmış yeni sürümün yardımı ile ana sınıfta push(void*)
sürümünü gizler, ve bundan dolayı push( ), sadece string göstergeçleri
StringStack gönderir.
Kabın içinde bulunan nesnelerin tipi tam olarak belirlenebildiği için, yıkıcı
işlev doğru çalışır ve aidiyet sorunu çözülür- veya en azından bir yaklaşım
aidiyet sorununa olur . Burada bir string göstergeç push( ) işlevi ile
StringStack'e aktarılırsa, o zaman(StringStack anlamına göre) ayrıca
göstergeçin aidiyeti StringStack'e aktarılır. Göstergeç pop( ) işlevi çekilip
alınırsa, sadece göstergeç alınmaz, ayrıca göstergeçin aidiyeti de alınmış
olur. Yıkıcı işlev çağrıldığında, StringStack'te bulunan herhangi bir
göstergeç, o yıkıcı tarafından silinir. Ve bunun için buradaki göstergeçler
string göstergeçlerdir, ve delete deyimi void göstergeçler yerine string
göstergeçler için çalışır. Böylece uygun yıkım gerçekleşmiş ve herşey
doğru çalışmıştır.
Yalnız burada bir eksiklik bulunmaktadır; bu sınıf sadece string
göstergeçler için iş görür. Eğer Stack'in öteki tip nesneler içinde iş
görmesi istenirse, yine sadece yeni nesnenizle çalışabilmesi için, mutlaka
sınıfın yeni sürümü yazılmalıdır. Böyle bir uygulama oldukça yorucudur,
bunun çözümü için şablonlar kullanılır, son bölümde ayrıntılarını
göreceğiniz şablonlar (standart templates) bu uygulamaları oldukça
kolaylaştırır.
Yukarıdaki örnekle ilgili olarak şu gözlemde de bulunabiliriz; kalıtım
sürecinde Stack'in arayüzü değişmektedir. Eğer arayüz farklı ise, o zaman
StringStack gerçekte bir Stack değildir, ve hiçbir zaman StringStack'i
doğru biçimde Stack olarak kullanamazsınız. Bu da burada kalıtım
kullanımını sorgulanabilir yapmaktadır; StringStack'i eğer Stack tipi
olarak oluşturmuyorsanız o zaman neden kalıtım kullanılıyor. Buna yanıt
olarak StringStack'in daha uygun sürümü bu bölümün ilerleyen
kesimlerinde verilecek.
Otomatik olarak aktarılamayan işlevler
Ana sınftaki bütün işlevler otomatik olarak türetilmiş sınıfa aktarılmaz. Bir
nesnenin oluşumu ve yıkımından yapıcı ve yıkıcı işlevler sorumludur. Bu
işlevler ilgili sınıfın nesnesi ile ne yapacaklarını bilirler ve bu yüzden
hiyerarşideki bütün yapıcı ve yıkıcı işlevler nesneler için mutlaka
çağrılmalıdırlar. Bu yüzden yapıcı ve yıkıcı işlevler aktarılmaz ve mutlaka
her türetilmiş sınıf için özel olarak oluşturulmalıdırlar.
İlave olarak operator= yapıcı işlev gibi bir görev yaptığı için aktarılmaz.
Yani bilindiği gibi, = işaretinin sol yanında yeralan nesnenin bütün
üyelerine, sağ yanda yeralan nesne üyelerinden yapılan aktarmalar
kalıtımdan sonra da hala, atama anlamına gelmez.
Kalıtımın bedeli olarak eğer siz bu işlevleri oluşturmazsanız, derleyici
bunları yapar. (yapıcılarla ilgili olarak; siz derleyicinin yapıcı ve kopya
yapıcı işlevleri oluşturması için herhangi bir yapıcı oluşturamazsınız). Bu
konudan 6. bölümde bahsedilmişti. Derleyici tarafından kurgulanan yapıcı
işlev, ilk değer atamalarını üye sıralamasına göre yapar ve derleyici
tarafından kurgulanmış operator= işlecinde atamalar üye sıralamasına
göre yapılır. Şimdi derleyici tarafından kurgulanmış işlevlerin bulunduğu
bir örnek verelim;
//:B14:SynthesizedFunctions.cpp
//derleyici tarafından kurgulanmış işlevler
#include <iostream>
using namespace std ;
class GameBoard{
public:
GameBoard( ){cout<<"GameBoard( )\n" ;}
GameBoard(const GameBoard&){
cout<<"GameBoard(const GameBoard&)\n" ;
}
GameBoard& operator=(const GameBoard&){
cout<<"GameBoard::operaor=( )\n" ;
return *this ;
}
~GameBoard( ){cout<<"~GameBoard( )\n" ;}
};
class Game{
GameBoard gb ; //harç
public:
//varsayılan GameBoard yapıcı işlevi çağrılır
Game( ){cout<<"Game( )\n " ;}
//GameBoard'ı açık biçimde çağırmalısınız
//yerine kopya yapıcı veya varsayılan yapıcı otomatik olarak gelir :
Game(const Game& g) : gb(g.gb){
cout<<"Game(const Game&)\n" ;
}
Game(int){cout<<"Game(int)\n" ;}
Game& operator=(const Game& g){
//GameBoard'ı açık biçimde çağırmak zorundasınız
//gb için atama işleci veya atamasız olur
gb=g.gb ;
cout<<"Game::operator=( )\n" ;
return *this ;
}
class Other{}; //öteki sınıf
//otomatik tip dönüşümü(döküm)
operator Other( ) const{
cout<<"Game::operator Other( )\n ;
return other( ) ;
}
~Game( ){cout<<"~Game( )\n" ;}
};
class Chess :public Game{ };
void f(Game::Other){}
class Checkers:public Game{
public:
//varsayılan ana sınıf yapıcı işlevi çağrılır
Checkers( ){cout<<"Checkers( )\n" ;}
//mutlaka ana sınıf kopya yapıcıyı çağırmalısınız
//veya otomatik olarak yerine varsayılan yapıcı çağrılır
Checkers(const Checkers& c):Game(c){
cout<<"Checkers(const Checkers& c)\n" ;
}
Checkers& operator=(const Checkers& c){
//operator=( ) işlecinin ana sınıf sürümünü açık biçimde çağırmalısınız
//veya ana sınıf ataması gerçeklenmez
Game::operator=(c) ;
cout<<"Checkers::operator=( )\n" ;
return *this ;
}
};
int main( ){
Chess d1 ;
//varsayılan yapıcı
Chess d2(d1) ; //kopya yapıcı
//! Chess d3(1) ; //hata int yapıcı yok
d1=d2 ;
//operator= kurgulandı
f(d1) ;
//tip dönüşümü aktarıldı
Game::Other go ;
//! d1=go ;
//hata; operator= öteki tipler için kurgulanmadı
Checkers c1, c2(c1) ;
c1=c2 ;
}///:~
GameBoard ve Game için yapıcılar ile operator= işleci kendilerini ilan
ederler, böylece derleyici tarafından kullanıldıklarını görülür. İlave olarak
operator Other( ), Game nesnesinden yuvalanmış sınıf Other nesnesine
tip dönüşümü (dökümü) yapar. Chess sınıfı basit bir şekilde Game
sınıfından türetilir ve herhangi bir işlev üretmez.(buna derleyicinin verdiği
yanıtı gözleyin). Otomatik tip dönüşümünü sınamak için f( ) işlevi Other
nesnesini kullanır.
main( ) işlevinde, türetilmiş sınıf Chess için kurgulanmış varsayılan yapıcı
işlev ve kopya yapıcı işlev çağrılır. Bu yapıcıların Game sürümleri, yapıcı
işlev çağrı hiyerarşisi olarak çağrılır. Her ne kadar kalıtım gibi gözükse de,
yeni yapıcılar aslında derleyici tarafından oluşturulurlar. Beklendiği gibi
değişkenleri olan hiçbir yapıcı, derleyici tarafından otomatik olarak
oluşturulamaz, zira değişkenler derleyici tarafından sezgi yolu ile
belirlenemez
Üye sıralama atamaları kullanılarak, operator= işleci de, Chess sınıfında
yeni bir işlev olarak kurgulanır. (böylece ana sınıf sürümü çağrılır),
çünkü; o işlev yeni sınıfta açık biçimde yazılmamıştı.
Ve doğal olarak yıkıcı işlev de derleyici tarafından otomatik olarak
kurgulanır.
Nesne oluşumunu yöneten işlevlerin yeniden yazılması ile ilgili bütün bu
kurallar nedeni ile, otomatik tip dönüşüm işlecinin kalıtımla aktarılması
biraz garip görünebilir. Eğer Other nesnesi oluşturmak için Game de
yeterli parçalar varsa, bu parçalar Game'den türetilmiş herhangi birşeyde
hala orada duruyorsa, ve tip dönüşüm işleci hala geçerli ise, o zaman biraz
önceki aktarım uygulaması çokta garip değildir. Bu normalde de yapılmak
istenir, zira aksi , kopya yapıcı işlev durumudur, yerine varsayılan üye
nesne yapıcısı kullanılır, ve atama işleci durumunda ise; üye nesneler için
hiçbir zaman atama gerçeklenmez.
Son olarak Checkers'e bakalım; varsayılan yapıcı, kopya yapıcı ve atama
işleçleri açık biçimde yazılmıştır. Varsayılan yapıcı durumunda; varsayılan
ana sınıf yapıcı işlevi otomatik olarak çağrılır, ve aslında yapılmak istenen
tipik olarakta budur. Ama esas önemli noktaya gelelim; kopya yapıcı işlev
ve atama işleçleri yazılmaya karar verildiği zaman, derleyici ne yaptığınızı
bildiğinizi kabul eder, ve bunların ana sınıf sürümlerini otomatik çağırmaz,
aynı kurgulanmış işlevlerde olduğu gibi davranır. Ana sınıf sürümleri
çağrılmak istenirse, o zaman kendiniz açık biçimde çağrıları yapmalısınız.
Checkers kopya yapıcı işlevinde, bu çağrı yapıcı işlev ilk değer atama
listesinde görünür;
Checkers(const Checkers& c) : Game(c){
Checkers atama işlecinde, işlev gövdesindeki ilk satır ana sınıf çağrısıdır;
Game::operator=(c) ;
Bu çağrılar, bir sınıftan türetme olan herhangi bir zamanda kanonik
parçalar olmaları lazımdır.
Kalıtım ve static üye işlevler
static üye işlevler aynı, static olmayan üye işlevler gibi davranırlar.
1--türetilmiş sınıflara aktarılırlar.
2--static üye yeniden tanımlanırsa, ana sınıfta yer alan bütün öteki
bindirilmiş işlevler gizlenir.
3--Ana sınıfta yer alan bir işlevin tamamı değiştirilirse, işlev adını taşıyan
bütün ana sınıf sürümleri gizlenir.( aslında bir önceki noktanın başka bir
uygulamasıdır).
Bununla birlikte hemen belirtelim; static üye işlevler virtual işlev
olamazlar. (virtual işlevlerle ilgili ayrıntılar 15. bölümde verilecek.)
Harç ve Kalıtım arasında seçim yapmak
Hem harç hem de kalıtım yeni sınıfa alt nesneler ilave eder. Alt nesneleri
oluşturmak için her ikiside yapıcı işlev ilk değer atama listelerini
kullanırlar. Şimdi merak edilebilir, ikisi arasında ne fark var?. Birini
diğerine ne zaman tercih etmelidir?.
Harç genellikle varolan bir sınıfın özelliklerini yeni oluşturulacak sınıfta
kullanmak için tercih edilir, ama burada varolan sınıfın arayüzü
kullanılmaz. Yani yeni sınıfa, varolan sınıfın özelliklerini aktarmak için
eski sınıfın nesnesi/veya nesneleri gömülür. Ama yeni sınıfın kullanıcısı,
arayüz olarak orijinal sınıftan farklı, sizin tanımladığınız yeni arayüzü
görür. Bunu gerçeklemek için yapılması gereken yeni sınıfa varolan sınıfın
nesnelerini private olarak gömmektir.
Arasırada olsa bazan, sınıf kullanıcısına yeni sınıf harcına doğrudan erişim
lazım olabilir, yani üye nesneler public yapılır. Üye nesneler erişim
denetimini kendileri kullanırlar. Böylece kullanıcı biraraya getireceği
parçaları bildiğinde, durum güvenceye alınmış olur, ayrıca arayüzün
anlaşılması da kolaylaşmış olur. İyi bir örnek olarak Car sınıfı aşağıda
verildi;
//:B14:Car.cpp
//public harç
class Engine{
public:
void start( ) const{}
void rev( ) const { }
void stop( ) const {}
};
class Wheel{
public:
void inflate(int psi) const {}
};
class Window{
public:
void rollup( ) const {}
void rolldown( ) const {}
};
class Door{
public:
Window window ;
void open( ) const {}
void close( ) const { }
};
class Car{
public:
Engine engine ;
Window window[4] ;
Door left,right ;
//2 kapılı
};
int main( ){
Car car ;
car.left.window.rollup( ) ;
car.wheel[0].inflate(70) ;
}///:~
Sorunun çözüm parçası Car harcı olduğu için, (esas tasarımın ana parçası
değil) üyeleri public yapmak müşteri programcının sınıfı nasıl
kullanacağını anlamasına yardımcı eder. Ve sınıf oluşturucuya da çok daha
az karmaşık kodlama imkanı verir.
Ufak bir zihin jimnastiği ile vehicle nesnesi ile Car sınıf harcının anlamsız
olacağını görürsünüz.- bir Car, vehicle kapsamaz. O bir vehicle dır.
Aralarınadaki ilişki kalıtımla açıklanabilir. Sahip oldukları ilişki ise
karışımdır.
Alt tipleme
Şimdi ifstream nesnesinin bir tipini oluşturalım, sadece dosya açmasın
ayrıca dosya adını da tutsun. Bu uygulamada harç kullanılabilir ve hem
ifstream hem de string yeni sınıfa gömülebilir;
//:B14:FName1.cpp
//dosya adı ile birlikte ifstream
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
class FName1{
ifstream file ;
string fileName ;
bool named ;
public:
FName1( ) : named(false){}
FName1(const string& fname)
:fileName(fname), file(fname.c_str( )){
assure(file, fileName) ;
named=true ;
}
string name( ) const {return fileName;}
void name(const string& newName){
if(named) return; //üzerine yazma
fileName=newName ;
named=true ,
}
operator ifstream&( ){return file ;}
};
int main( ){
FName1 file("FName1.cpp") ;
cout<<file.name( )<<endl ;
//Hata:close( ) üye değil
//!file.close( ) ;
}///:~
Bununla beraber burada bir sorun bulunmaktadır. FName1 nesnesinin
herhangi bir yerde kullanımını sağlamak için ifstream& e FName1 den
eklenmiş otomatik tip dönüşüm işleci ile ifstream kullanımı sağlanır. Ama
main( ) işlevinde;
file.close( ) ;
satırı derlenemez, zira otomatik tip dönüşümleri sadece işlev çağrılarında
gerçeklenir, üye seçimleri esnasında değil.Bundan dolayı bu yaklaşım
çalışmaz.
İkinci yaklaşım ise close( ) işlev tanımını FName1'e eklemektir.
void close( ){file.close( );}
Bu yaklaşım ifstream sınıfından sadece birkaç işlev getirilecek olursa,
gayet iyi çalışır. Görüldüğü gibi sınıfın sadec bir kısmı kullanılıyor, bu
yüzden harç burada daha uygundur.
Fakat sınıftaki herşey kullanılmak istenirse ne olacak?. Varolan bir tipten
yeni bir tip oluşturulduğu için buna alt tipleme denilir. Ve ayrıca varolan
sınıfın arayüzü yeni sınıfta aynen kullanılabilir (istenirse yeni sınıfa başka
işlevlerde arayüz için eklenebilir). Bu tip, daha önceki tip gibi her yerde
kullanılabilir. İşte kalıtımın olması gereken esas yer burası. Önceki örnekte
bulunan sorunu, alt tipleme ile mükemmel bir şekilde çözmek
mümkündür.;
//:B14:FName2.cpp
//sorun alt tipleme ile çözülür
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
class FName2 : public ifstream{
string fileName ;
bool named ;
public:
FName2 : named(false){}
FName2(const string& fname)
: ifstream(fname.c_str( )), fileName(fname){
assure(*this, fileName) ;
named=true ;
}
string name( ) const{return fileName ;}
void name(const string& newName){
if(named) return ;
//üzerine yazma
fileName=newName ;
named=true ;
}
};
int main( ){
FName2 file("FName2.cpp") ;
assure(file, "FName2.cpp") ;
cout<<"name : "<<file.name( )<<endl ;
string s ;
getline(file, s) ; //bu da çalışır
file.seekg(-250, ios::end) ;
file.close( ) ;
}///:~
Burada, ifstream nesnesi için kullanılan herhangi bir üye işlev, aynı
zamanda FName2 nesneleri içinde kullanılabilir. Ayrıca getline( ) benzeri
üye olmayan işlevlerde ifstream kullandıkları için FName2 ile de
çalışabilirler. Zira FName2 bir ifstream tipidir, ve ifstream de tek tip
bulunmaz. Bu çok önemli bir konu olup bir sonraki bölümle beraber bu
bölümün sonunda da incelenecek.
private Kalıtım
Ana sınıf listesindeki public anahtar kelimesini kaldırarak bir ana sınıfı
korumalı olarak türetebilirsiniz. (aslında private kelimesini açık olarak
yazmak en doğrusudur, zira ne demek istediğiniz dalgınlığa mahal
vermeden doğru olarak anlaşılır). Özel olarak türettiğiniz zaman onun
koşullarına göre uygulamayı gerçekliyorsunuz, yani yeni sınıfınızı, ana
sınıfın bütün verileri ve davranışlarına sahip olarak oluşturuyorsunuz, ama
işlevleri gizleniyor, bu yüzden sadece esas uygulamanın parçası oluyor.
Sınıf kullanıcısı bu gizlenen işlevlere erişememektedir, o yüzden bir nesne
ana sınıf unsuru olarak işlemlenememektedir. (FName2.cpp de olduğu
gibi)
Herhalde private kalıtımın amacını merak etmişsinizdir. Yeni sınıfta, harç
kullanımına seçenek olarak private nesne oluşturmak, daha uygun gibi
gözükmektedir. private kalıtım dili bütünleştirmek için eklenmiştir. Ama
yinede eğer kafa karışıklığını azaltmadan başka bir nedeniniz yoksa,
private kalıtımdan ziyade harcı tercih edebilirsiniz. Bununla birlikte,
nadiren de olsa bazı durumlarda ana sınıfın aynı arayüz parçası üretilmek
istenebilir, ve sanki ana sınıf nesnesi gibi nesne işlemlenmesine izin
verilmez, böyle durumlarda private kalıtım bunu sağlar.
Özel olarak türetilmiş üyeleri herkese açmak
private kalıtım gerçeklendiği zaman, ana sınıfın bütün public üyeleri yeni
sınıfta private olur. Bunlardan bazıları görünebilir yapılmak istenirse,
sadece bunların adını türetilmiş sınıfın public kesiminde bildirmek
(değişkenleri ve geri dönüş tipi bulunmaz) yeterlidir.
//:B14:PrivateInheritance.cpp
class Pet{
public:
char eat( ) const{return 'a' ;}
int speak( ) const{return 2 ;0
float sleep( ) const{return 3.0 ;}
float sleep(int) const {return 5.0 ;}
};
class Goldfish :Pet{//private kalıtım
public:
Pet::eat ; //adı herşeye açar
Pet::sleep ; //heriki üyede bindirilmiş olur
};
int main( ){
Goldfish ali ;
ali.eat( ) ;
ali.sleep( ) ;
ali.sleep(1) ;
//!ali.speak( ) ; //hata: private üye
}///~
Ana sınıfın işlevsel yanını gizlemek isterseniz, private kalıtım
kullanışlıdır.
Farkettiğiniz üzere bindirilmiş işlev adı, ana sınıf bindirilmiş işlevinin
bütün sürümlerini ortaya çıkarır.
Harç yerine private kalıtımı kullanmadan önce, dikkatle iyice düşünmek
lazım ; private kalıtım, çalışma sırasında yapılacak olan tip tanımlamaları
nedeniyle çeşitli mahzurlar taşır. 2. kitabımızda bunların ayrıntıları
incelenecek.
protected
Şimdi artık kalıtımla tanışmaktasınız, konu ile ilgili son anlamlı anahtar
kelime protected tir. İdeal anlamda private her zaman katı ve kesin olarak
özeldir. Gerçek projelerde bazı durumlarda, herşeyi dış dünyadan gizler
ama, türetilmiş sınıf üyelerinin erişmesine izin verilir. protected anahtar
kelimesi esneklik noktasıdır; bu kelimenin söylediği, "sınıf kullanıcısı
mevzu bahis olduğu zaman bu private'dir, ama kendinden türetilmiş
sınıflar mevzu bahis olunca tamamen public olarak işlev görür"
Veri üyeleri için en iyi yaklaşım bunları private yapmaktır. Her zaman ana
uygulamada bunu değiştirme hakkınız bulunması lazım. Türetilmiş
sınıfların denetimli erişimi protected üye işlevler ile gerçeklenir;
//:B14:Protected.cpp
//protected anahtar kelimesi
#include <fstream>
using namespace std ;
class Base{
int i ;
protected:
int read( ) const {return i ;}
void set(int ii) const {i=ii ;}
public:
Base(int ii=0) : i(ii){}
int value(int m) const {return m*i ;}
};
class Derived : public Base{
int j ;
public:
Derived(int jj=0) : j(jj){ }
void change(int x) {set(x) ;}
};
int main( ){
Derived d ;
d.change(20) ;
}///:~
Bu kitabın ilerleyen bölümlerinde protected ile ilgili örnekler ve ayrıca 2.
kitapta da daha ayrıntı verilecek.
protected kalıtım
Bir sınıf türettiğiniz zaman, ana sınıfın bütün ögeleri varsayım olarak
private'dir. Bunun anlamı ana sınıfın public üyeleri yeni sınıfın
kullanıcısına private olur demektir. Ama genellikle kalıtım public
yapılarak ana sınıfın arayüzü, türetilmiş sınıfa aynen aktarılır. Bununla
birlikte istenirse protected kalıtımı da uygulanabilir.
protected türetimin anlamı; öteki sınıfların koşullarına göre uygulanacak
demektir, ama türetilmiş sınıf ve friends'ler için. Her zaman kullanılmasa
da, programlama dilinin tamamlanmasını sağladığı için burada değindik.
İşleç bindirim ve kalıtımı
Atama işleci hariç, diğer işleçler türetilmiş sınıflara otomatik olarak
kalıtımla geçerler. Bunu B12 de yeralan Byte.h den kalıtımla geçişi
göstereceğiz;
//:B14:OperatorInheritance.cpp
//bindirilmiş işleçlerin kalıtımla aktarımı
#include ".../C12/Byte.h"
#include <fstream>
using namespace std ;
ofstream out("ByteTest.out") ;
class Byte2 : public Byte{
public:
//yapıcılar aktarılmaz
Byte2(unsigned char bb=0) : Byte(bb){ }
//operator= kalıtımla geçmez ama
//üye sıralamasına göre kurgulanır
//Bununla birlikte SameType=SameType
//operator= kurgulanır ama
//ötekiler açık biçimde yazılmalı
Byte2& operator=(const Byte& right){
Byte::operator=(right) ;
return *this ;
}
Byte2& operator=(int i){
Byte::operator=(i) ;
return *this ;
}
};
//C12 yeralan ByteTest.cpp dekilere benzer sınama işlevleri
void k(Byte2& b1, Byte2& b2){
b1=b1*b2+b2%b1 ;
#define TRY2(OP)\
out<<"b1= : " ; b1.print(out) ;\
out<<" , b2= " ; b2.print(out) ;\
out<<" ; b1 " #OP "b2 produces " ;\
(b1 OP b2).print(out) ;\
out<<endl ;
b1=10 ; b2=50 ;
TRY2(+) TRY2(-) TRY2(*) TRY2(/)
TRY2(%) TRY2(^) TRY2(&) TRY2(|)
TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
TRY2(=)
//atama işleci
//koşullular
#define TRY2(OP) \
out<<"b1= " ; b1.print(out) ;\
out<<", b2= " ; b2.print(out) ;\
out<<" ; b1 " #OP "b2 produces ";\
out<<(b1 OP b2) ;\
out<<endl ;
b1=10 ; b2=50 ;
TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
TRYC2(>=) TRYC2(&&) TRYC2(||)
//ardışık atamalar
Byte2 b3=95 ;
b1=b2=b3 ;
}
int main( ){
out<<"member functions "<<endl ;
Byte2 b1(50), b2(10) ;
k(b1, b2) ;
}///:~
Byte yerine kullanılan Byte2 hariç, sınama kodları C12/ByteTest.cpp
dekilerle aynıdır. Buradaki usul bütün işleçlerin kalıtım yolu ile Byte2 de
de çalışabileceğini ispatlamıştır.
Byte2 sınıfına baktığınız zaman görülen, yapıcı işlevin açıkça yazılmasını
gerekliliğidir, ve sadece operator= işleci Byte2 yi, kurgulanmış Byte2 ye
atanır; başka atama işleçleri gerekirse bunları sizin kurgulamanız gerekir.
ÇOKLU KALITIM
Tek sınıftan yeni sınıf türetebilirsiniz, bu da bize yeni bir sınıfın bir seferde
birden fazla sınıftan da türetilebileceği izlenimi verir. Gerçekten olabilir,
fakat sürekli tartışma konusu olan çoklu kalıtım, tasarım parçasını anlamlı
kılar. Bir konuda ortak fikir bulunmaktadır; uzun bir süre programcılık
yapıp, tecrübe elde edene kadar bunu denememeniz önerilir ve dil tam
anlamı ile kavranmalıdır. O zamana kadar, herhalde gerçeklediğiniz
programlar sırasında, mutlaka kullanmak zorunda kalacağınız çoklu
kalıtım durumları hakkında daha çok düşünürsünüz, hemen hemen her
zaman da tekli kalıtımla işinizi halledersiniz.
Başlangıçta çoklu kalıtım basit gibi görünür; kalıtım sırasında ana sınıf
listesine daha fazla sınıf adını, biribirlerinden virgülle ayırarak, eklersiniz.
Bununla birlikte çoklu kalıtım da, muğlak olasılıklarla karşılaşılabilir.
Bunların nedenleri 2. kitapta incelenecektir.
Artarak geliştirme
Kalıtım ve harcın üstünlüklerinden biri; yeni kod, varolan kodlarda hataya
neden olmadan, artarak geliştirmeye izin verir. Eğer hata ortaya çıkarsa, bu
yeni kodun içinde yalıtılıp bırakılır. Hazırdan türeterek(veya harçlayarak),
işlevsel sınıf ve veri üyeleri ekleyerek ve üye işlevlerle (kalıtım sırasında
varolan üye işlevleri yeniden tanımlayarak), varolan kodu - birisi hala
kullanıyor olabilir- dokunmadan ve hata ile doldurmadan terkedersiniz. Bir
hata varsa bilinirki o hata yeni koddadır, zira eğer varolan kod gövdesinde
değişiklik yapmış olsaydınız, çok daha uzun ve zor bu hatayı bulurdunuz.
Sınıfların birbirinden bu kadar düzgün ayrılmış olmaları oldukça
şaşırtıcıdır. Hatta yeniden kod kullanımında, üye işlevler için kaynak kod
gereksinimi bulunmamamktadır, yalnız sınıfın bulunduğu yeri belirten
başlık dosyası (xxx.h) ve derlenmiş üye işlevlerle birlikte kütüphane
dosyası veya hedef dosya yeterlidir. (Bu hem harç hemde kalırtım için
doğrudur).
Program geliştirmeyi, insanın öğrenmesine benzer şekilde yani artarak
geliştirmek, özellikle önemlidir. Program geliştirme bir artış sürecidir.
İstediğiniz kadar çözümleme yapabilirsiniz, ama bir proje üzerine
yerleştirme yaptığınız zaman hala bütün yanıtları bilmezsiniz. Herşeyi bir
seferde yapmaktan ziyade projeyi organik evrimsel bir yaratık olarak
büyütmeye başlarsanız, çok daha fazla başarı sağlanır- daha fazla geriye
bilgi aktarılır.
Deney için kalıtım kullanılması yararlı olmasına rağmen, bazı noktaların
kararlı olmasından sonra, sınıf hiyerarşisine onu anlamlı yapıyla çökertmek
için, yeniden bir bakış atma gereksinimi vardır. Herşeyin altında
bulunanları hatırlayın, kalıtımın açıklamaya çalıştığı ilişki; "yeni sınıf eski
sınıfın bir tipidir". Program, bitleri ne tarafa göndereceği ile ilgilenmemesi
lazımdır, bunun yerine sorun ortamındaki terimlerle anlatılan modelin
açıklanması için, değişik tipteki nesnelerin oluşturulması ve işlemlenmesi
gerekir.
Yukarıdöküm (upcasting)
Bölümün başlangıcında, ifstream den türetilmiş bir sınıf nesnesinin bütün
ifstream nesne davranış ve karakteristiklerine nasıl sahip olduğu
görülmüştü. FName2.cpp'de, herhangi bir ifstream üye işlevi,
FName2.cpp nesnesi için çağrıldı.
Kalıtımın en önemli özelliği, yeni sınıf için üye işlevler sağlamak değildir.
Yeni sınıf ile ana sınıfın arasında açıklanan ilişkidir. Bu ilişki aşağıdaki
cümle ile özetlenebilir; "yeni sınıf önceden varolan sınıfın bir tipidir."
Bu anlatım kalıtımı açıklayan en acayip tanım değildir- doğrudan derleyici
destekler. Örnek olarak Instrument ana sınıfını ele alalım bu sınıf müzik
enstrümanlarını belirtsin, ve bundan Wind sınıfını türetelim. Kalıtımın
anlamı; ana sınıfta bulunan bütün işlevler türetilmiş sınıfta da
kullanılabildiği için, ana sınıfa gönderilen herhangi bir ileti aynı zamanda
türetilmiş sınıfa da gönderilebilir. Bundan dolayı Instrument sınıfında
play( ) işlevi varsa, o zaman Wind türetilmiş sınıfında da aynı işlev
kullanılabilir demektir. Bunun anlamı; bir Wind nesnesi ayrıca bir
Instrument tipi demektir. Aşağıdaki örnek derleyicinin bu konuyu nasıl
desteklediğini göstermektedir;
//:B14:Instrument.cpp
//kalıtım ve yukarı döküm
enum note{middleC, Csharp, Cflat}; //vs
class Instrument{
public:
void play(note) const{ }
};
//Wind nesneleri Instrument dir
//zira arayüz aynıdır
class Wind : public Instrument { };
void tune(Instrument& i){
//......
i.play(middleC) ;
}
int main( ){
Wind flute ;
flute(tune) ; //yukarı döküm
}///:~
Yukarıdaki örnekte ilginç olan tune( ) işlevidir, Instrument dayancını
kabul eder. Bununla birlikte main( ) işlevinde, tune( ) işlevi Wind
nesnesini dayanç alarak çağrılır.Daha önceleri belirtildiği gibi, C++ dili tip
denetiminde çok özeldir.Başka bir tipi almış bir tipi, bir işlevin kabul
etmesi acayip görünebilir, ta ki bir Wind nesnesinin aynı zamanda
Instrument nesnesi gibi gerçeklenmesine kadar, ve Wind olmayan
Instrument çağıran tune( ) işlevi bulunmaz. (kalıtım bunu güvenceye
alır). tune( ) işlevinde kod, Instrument ve Instrument'ten türetilmiş
herhangi bir şey için çalışır. Wind dayanç veya göstergecini, Instrument
dayanç veya göstergecine çevirmeye yukarı döküm (upcasting) denir.
Neden yukarı döküm
Nedeni geçmişe dayalıdır, yani sınıf kalıtım çizimlerinin geleneksel yolu
takip etmesine dayanmaktadır; kök sayfanın en üstünde, büyüme ise aşağı
doğru yapılmaktadır.(çizimleri siz, size nasıl kolay geliyorsa öyle
yapabilirsiniz). Kalıtım çizimi Instrument.cpp için aşağıdaki gibidir;
Kalıtımda türetilmiş sınıftan ana sınıfa yapılan döküm yukarı doğrudur,
bundan dolayı yukarı döküm diye adlandırılır. Yukarı döküm, daha özel bir
tipten daha genel bir tipe doğru yapıldığı için her zaman güvenlidir. Sınıf
arayüzünde olan tek şey üye işlevlerini kaybetmektir, kazanmak değil. Bu
nedenle derleyici, açık döküme veya başka yazım biçimine gerek
duymadan yukarı döküme izin verir.
Kopya yapıcı işlev ve yukarı döküm
Türetilmiş sınıf için derleyicinin bir kopya yapıcı işlev kurgulamasına izin
verilirse, otomatik olarak ana sınıf kopya yapıcı işlevi çağrılır, ve daha
sonra bütün üye nesneler için kopya yapıcı işlevler çağrılır (yerleşik tipler
için bitkopyalaması gerçeklenir), böylece doğru sonuçlar elde edilir.
Örnek;
//:B14:CopyConstructor.cpp
//kopya yapıcı işlevin doğru oluşturulması
#include <iostream>
using namespace std ;
class Parent{
int i ;
public:
Parent(int ii) : i(ii){
cout<<"Parent(int ii)\n " ;
}
Parent(const Parent& b) : i(b.i){
cout<<"Parent(const Parent& )\n " ;
}
Parent( ) : i(0){cout<<"Parent( )\n " ;}
friend ostream&
operator<<(ostream& os, const Parent& b){
return os<<"Parent: "<<b.i<<endl ;
}
};
class Member{
int i ;
public:
Member(int ii) :i(ii){
cout<<"Member(int ii)\n" ;
}
Member(const Member& m) :i(m.i){
cout<<"Member(const Member&)\n" ;
}
friend ostream&
operator<<(ostream& os, const Member& m){
return os<<"Member: "<<m.i<<endl ;
}
};
class Child :public Parent{
int i ;
Member m ;
public:
Child(int ii) : Parent(int ii), i(ii), m(ii){
cout<<"Child(int ii)\n" ;
}
friend ostream&
operator<<(ostream& os, const Child& c){
return os<<(Parent&)c<<c.m
<<"Child: " <<c.i<<endl ;
}
};
int main( ){
Child c(2);
cout<<"kopya yapici cagrisi"<<endl ;
Child c2=c ;
cout<<"values in2\n"<<c2 ;
}///:~
Burada Child için operator<< işleci, kendi içinde bulunan Parent
parçasının çağrısı yüzünden ilginçtir; Child nesnesi Parent& e
dönüştürülür. (döküm yapılır). Dayanç yerine ana sınıf nesnesine
dönüştürülürse, istenmeyen sonuçlar elde edilir;
return os<<(Parent&)c<<c.m ;
Mademki derleyici o zaman onu Parent olarak gördü, operator<< ün
Parent sürümü çağrılır.
Görüleceği üzere Child açık bir biçimde belirtilmiş kopya yapıcı işleve
sahip değildir. Bu yüzden derleyici, kopya yapıcı işlevleri kendisi
(derleyicinin kurgulayacağı dört işlevden biri varsayıan yapıcı ile beraber
kopya yapıcı işlevdir,-eğer herhangi bir yapıcı ve yıkıcı oluşturmazsanızoperator= ve yıkıcı işlevleri), Parent kopya yapıcı ve Member kopya
yapıcıyı çağırarak kurgular. Bu çıktıda gösterildi;
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor
Parent(const Parent&)
Member(const Member&)
values in c2:
Parent :2
Member: 2
Child : 2
Bununla beraber eğer Child kopya yapıcı işlevini kendiniz yazmak
isterseniz, ve istemeden hata yaparsanız ve kötü işler yaparsınız;
Child(const Child& c) : i(c.i), m(c.m){}
o zaman Child'ın ana sınıf kısmı için, varsayılan yapıcı işlevi çağrılır.
Başka yapıcı işlev çağrım yolu kalmadığı zaman derleyici bu yola
başvurur.( birtakım yapıcı işlevin, her nesne için mutlaka herzaman
çağrılmak zorunda olduğunu, başka bir sınıfın altnesnesi olup olmadığını
gözardı ederek, hatırlayın). Çıktı o zaman ;
Parent(int ii)
Member(int ii)
Child(int ii)
calling copy-constructor
Parent( )
Member(const Member& )
values in c2
Parent : 0
Member : 2
Child : 2
Bu tabii beklediğiniz bir sonuç değil, genellikle kopya yapım parçası
olarak varolan nesnenin ana sınıf bölümü beklenir.
Sorunu çözümlemek için, kendi kopya yapıcı işlevinizi yazdığınız
herhangi bir zamanda, ana sınıf kopya yapıcı işlevini uygun şekilde
çağırmak zorunda olduğunuz hatırlamalısınız, (derleyicinin yaptığı gibi).
Başlangıçta bu biraz acayip görünebilir; yukarı döküme başka bir örnek
verek gösterelim;
Child(const Child& c)
: Parent(c), i(c.i), m(c.m){
<<"Child(Child&)\n" ;
}
Buradaki acayip olan Parent kopya yapıcı işlevinin çağrıldığı noktadır;
Parent(c) .Child nesnesinin Parent yapıcı işlevine aktarılması ne anlama
geliyor?. Ama Child, Parent sınıfından türetildiği için Child dayancı
Parent dayancıdır.Ana sınıf kopya yapıcı işlevi çağrısı, Child'a dayancın
Parent'e dayanç olan yukarı dökümüdür ve onu kopya yapmak için
kullanır. Kendi kopya yapıcı işlevinizi yazmak istediğinizde, hemen her
zaman aynı şeyleri yapmak isteyeceksiniz.
Kalıtımla harç karşılaştırması
Yeni bir sınıf oluştururken harçmı yoksa kalıtımmı kullanılacak belirlemek
için kullanılacak en açık kıstas; yeni sınıftan yukarı döküm gereksinimi,
duyulup duyulmadığını sorgulamaktır. Bu bölümün başlangıç kısmında,
Stack sınıfı kalıtım kullanılarak geliştirilmişti. Bununla birlikte allahtan
StringStack nesneleri sadece string kapları olarak kullanılır ve hiçbir
zaman yukarı döküm olmaz, bu nedenle çok daha uygun seçenek harç olur.
//:B14:InheritStack2.cpp
//kalıtımla harç karşılaştırması
#include ".../B09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
class StringStack{
Stack stack ; //kalıtım yerine gömme yap
public:
void push(string* str){
stack.push(str) ;
}
string* peek( ) const{
return (string*)stack.peek( ) ;
}
string* pop( ){
return (string*)stack.pop( ) ;
}
};
int main( ){
ifstream in("InheritStack2.cpp") ;
assure(in, "InheritStack2.cpp") ;
string line ;
StringStack textlines ;
while(getline(in, line))
textlines.push(new string(line)) ;
string* s ;
while((s=textlines.pop( )) !=0) //döküm yok
cout<<*s<<endl ;
}///:~
Yukarıdaki dosya InheritStack.cpp ile aynıdır, sadece Stack nesnesinin
biri StringStack sınıfına gömülmüş olup ve üye işlevlerin bu gömülü
nesne için çağrılması durumu hariç. Aslında hala ne zaman, nede bellek
alanı yeri masrafı bulunmaktadır zira, altnesne aynı miktarda bellek alanı
kaplamakta olup, ilave tip denetimleri derleme sırasında yapılmaktadır.
Kafa karıştırıcı olmasına rağmen, ayrıca koşulları ile uygulanmıştır
açıklaması için private kalıtım kullanabilirdiniz. Bu ayrıca yerine göre,
sorunu çözebilirdi. Bununla birlikte bir yer, çoklu kalıtım gerekli olduğu
zaman önemlidir. Bu durumda, tasarımda kalıtım yerine harç
kullanılabileceğini görürseniz, çoklu kalıtımdan vazgeçilebilir.
Göstergeç ve dayanç yukarı dökümü
Instrument.cpp de işlev çağrıları sırasında yukarı döküm yapılır, İşlev
dışında ki bir Wind nesnesi sahip olduğu kendi dayancını alır ve işlev
içinde bir Instrument dayancı olur. Yukarı döküm ayrıca göstergeç ve
dayançlara yapılan basit atamalar sırasında da olabilir;
Wind w ;
Instrument* ip=&w ; //yukarı döküm
Instrument& ir=w ; //yukarı döküm
İşlev çağrısına benzer şekilde, yukarıdaki durumların hiçbiri açıkça döküm
istemez.
Bir Sorun
Yukarı dökümün nesne hakkındaki tip bilgisinin kaybolmasına yolaçması
doğaldır. Eğer ;
Wind w ;
Instrument* ip=&w ;
derseniz, derleyici sadece Instrument tipindeki ip göstergeci ile ilgilenir,
başka bir şeyle ilgilenmez. Yani derleyici ip nin aslında Wind nesnesini
gösterdiğini bilemez. Bu yüzden play( ) üye işlevini çağırdığınızda;
ip->play(MiddleC) ;
Derleyici play( ) işlevinin sadece Instrument göstergeci için çağrıldığını
bilir, ve yapması lazım gelen Wind::play( )çağrısı yerine, ana sınıf
sürümünü Instrument::play( ) i çağırır. Böylece doğru sonuç elde
edilmemiş olur.
Bu önemli bir sorundur. Konu, bundan sonraki bölümde yer alan
çokbiçimlilik ile çözümlenecektir. Çokbiçimlilik nesne yönelimli yazılımın
kilittaşlarından olup virtual işlevlerle gerçeklenirler.
Özet
Hem kalıtım ham harç varolan tiplerden yeni tipler oluşturmayı sağlarlar.
Her ikisi de varolan tiplerin alt nesnelerini yeni tipe koyarlar. Esas olarak
harç, yeni tipi oluştururken varolan eski tiplerin kullanılmasıdır. Harçta
eski tipler yeni tipin parçası gibi görev yaparlar. Kalıtımda ise yeni tip eski
tipin benzeri olmaya zorlanır, burada eski tip ana sınıftır.(tip eşdeğerliği
arayüz eşdeğerliğini güvenceye alır). Türetilen sınıf ana sınıf arayüzüne
sahip olduğu için, ana sınıfa yukarı döküm yapılabilir. Bu konu bir sonraki
bölümde görülecek olan çokbiçimlilik için kritik önemdedir.
Harç ve kalıtımda kodların yeniden kullanımı hızlı proje geliştirirken çok
yararlı olmalarına rağmen, genellikle sınıf hiyerarşileri yeniden
tasarlanmak istenir. Buradaki amaç hiyerarşide yer alan her sınıfın belli bir
ödevi olması ve ne çok büyük ne de sıkıntı verecek derecede küçük
boyutta olmasını sağlamaktır.
15. BÖLÜM
Çokbiçimlilik ve Sanal işlevler
Çokbiçimlilik, aynı adlı işlemlerin farklı nesneler için farklı
uygulanmasıdır. Örnek olarak artı (+) işleci sayılar için aritmetik toplama
yapar, string'ler için metinleri arka arkaya ekler. Yani artı işlecinin değişik
işlem biçimleri olabilir.
Çokbiçimlilik (C++ dilinde virtual işlevlerle gerçeklenir) nesne yönelimli
dillerin, veri soyutlama ve kalıtımdan sonra gelen üçüncü temel özelliğidir.
Arayüzün uygulamadan ayrımının başka bir boyutu olup, "nasıl"ın "ne"
den farklılığını ortaya koyar. Çokbiçimlilik gelişmiş kod düzenlemeleri ve
okunabilirliği sağladığı gibi, genişletilebilir kodlarla sadece proje
başlangıcında değil, istenen herhangi bir anda projeye eklemeler
yapılabilmesine imkan verir.
Sarmalama yeni veritiplerini, karakteristikleri ve davranışları biraraya
getirerek oluşturur. Ayrıntılar private yapılarak erişim denetimi ile, arayüz
uygulamadan ayrılır. Buna benzer düzenlemeler, yargı tabanlı
programlama altyapısına sahip kişilere çok daha anlaşılır gelir. Ama sanal
işlevler tip koşullarının farkı ile ilgilidirler. Önceki bölümde bir nesnenin
kendi tipi veya ana tip olarak, kalıtımdaki işlemleme biçimleri görüldü. Bu
özellik çok önemlidir zira, birçok tipin (ana tipten türetilmiş tipler) sanki
tek bir tipmiş gibi işlemlenmesini sağlar, ve tek bir parça kod birden fazla
tip üzerinde eşit şekilde çalışır. Sanal işlevler, bir tipin başka benzer bir
tipten farklılığının belirlenmesini sağlar. Yeterki her iki benzer tip, aynı
ana tipten türetilmiş olsunlar. Farklılık, ana sınıftan çağrılan işlev
davranışlarındaki değişikliklerle açıklanır.
Bu bölümde, sanal işlevler basit örneklerden yola çıkarak anlatılmaya
başlanacak, ve daha sonra bütün ayrıntıları verilecektir.
C++ ve programcıların evrimi
C programcıları C++ diline üç evre ile hakim olmuş görünmektedirler.
Birincisi "daha iyi C" olarak, zira C++ bütün işlevlerin kullanılmadan önce
bildirilmesini ister ve, değişkenlerin kullanımı konusunda çok daha
müşkülpesenttir. Bir C programı, C++ derleyicisi ile derlendiği zaman çok
sayıda hata iletisi ile üretebilir.
İkinci aşama "nesne tabanlı" C++ dır. Bunun anlamı; veri yapılarının
gruplanmış biçimlerinin, kod düzenlemelerindeki yararları ile kolaylıkla
görülür. Veri yapıları, üzerlerinde işlem yapan yapıcı ve yıkıcı,-belki biraz
da basit kalıtım ile, işlevlerle birlikte bulunur. C dili ile çalışmış
programcıların büyük çoğunluğu, bunun ne kadar kullanışlı olduğunu
hemen görürler, zira, aslında kendileri kütüphane oluşturmaya çalışırken
yapmak istedikleri tam olarak budur. C++ dili ile, derleyici yardımı alınmış
olur. Nesne tabanlı düzeyde hızlıca elde edilenler epey şaşırtıcıdır. Ayrıca
çok fazla kafa çalıştırmadan bir sürü kazanım sağlanır. Veri tipi oluşturma
hissini duymak kolaylaşır, sınıf ve nesneler oluşturulur, nesnelere iletiler
gönderilir ve herşey düzenli ve doğru çalışır.
Ama herşey henüz bitmez. Bu noktada kalınırsa, sizi gerçek nesne
yönelimli programlamaya sıçratacak olan, dilin en büyük parçası gözden
kaçırılmış olur. Bu, sadece sanal işlevlerle gerçeklenebilir.
Sanal işlevler, hemen sarmalanmış duvar arkası yapıların içinde kodları
değil, onun yerine, tip unsurunu geliştirir. Bundan dolayı yeni başlayan bir
C++ programcısı için şüphesiz kavranması en zor konudur. Bununla
birlikte, sanal işlevler nesne yönelimli programlamanın anlaşılmasında
dönüm noktasıdır. Sanal işlevleri kullanamıyorsanız, henüz nesne
yönelimli programlamayı kavrayamamışsınız demektir.
Sanal işlevler tip unsuru ile özel olarak bağlantılı olduğu için (ve tipte
nesne yönelimli programlamanın çekirdeğinde bulunur), geleneksel
programlama dillerinde (Fortran, Pascal, C vs ) sanal işlevlerin karşılığı
bulunmamaktadır. Geleneksel dilleri kullanan programcı için sanal
işlevleri düşündürecek herhangi bir benzer unsur yoktur, ve herşey dilin
diğer öteki özellikleri ile gerçeklenir. Geleneksel dillerdeki özellikler
algoritma seviyesinde anlaşılabilir, sanal işlevler ise, sadece tasarım bakışı
ile anlaşılabilir.
Yukarı döküm (yukarıdaki tipe dönüştürme)
14. bölümde bir nesne kendi tipi veya ana tip nesnesi olarak, nasıl
kullanılabilir görüldü. Ek olarak, ana tip adresi ile işlemlenebilir. Nesne
adresini alıp (ya göstergeç yada dayanç), onu ana sınıf adresi olarak
işlemlemeye; yukarı döküm, yani yukarıdaki tipe dönüştürme denilir .
Yukarı kelimesinin kullanım nedeni; kalıtım ağacında ana sınıfın, en
yukarıda yer almasıdır.
Aşağıdaki kodda, gizlenmiş bulunan bir sorun vardı.
//:B15:Instrument2.cpp
//kalıtım ve yukarı döküm
#include <iostream>
using namespace std ;
enum note{middleC, Csharp, Eflat}; //etc..
class Instrument{
public:
void play(note) const{
cout<<"Instrument::play"<<endl ;
}
};
//Wind nesneleri Instrumenttir
//zira aynı arayüzü kullanırlar
class Wind : public Instrument{
public:
//arayüz işlevini yeniden tanımla
void play(note) const{
cout<<"Wind::play"<<endl ;
}
};
void tune(Instrument& i){
//.....
i.play(middleC) ;
}
int main( ){
Wind flute ;
tune(flute) ; //yukarı döküm
}///:~
tune( ) işlevi (dayanç aracılığı ile) Instrument kabul eder. Ama ayrıca
Instrument'den türetilmiş herşeyi, şikayet etmeden kabul eder.Bunu main
işlevinde, tune işlevine aktarılan Wind nesnesi olarak görürsünüz. Tip
dönüştürme gerekmez. Bu olabilir; Instrument arayüzü mutlaka
Wind'tede bulunmalıdır, çünkü Wind, Instrument'den public olarak
türetilir. Wind'ten Instrument'e yapılan tip dönüşümü (yukarı döküm)
arayüzü "inceltir", ama hiçbir zaman bütün Instrument
arayüzündenkilerden daha az değildir.
Göstergeçlerle uygulandığı zaman aynı değişkenler geçerlidir; tek fark,
kullanıcı işleve aktarılan nesne adreslerini mutlaka açık olarak almalıdır.
Sorun
Instrument2.cpp ile ilgili sorun, program çalıştırıldığı zaman ortaya çıkar.
Çıktı Instrument::play dir. Ama açık olarak istenen sonuç bu değildir,
zira bildiğiniz gibi nesne aslında bir Wind olup, Instrument değildir.
Çağrı Wind::play üretmesi lazım. Bu yüzden, Instrument'ten türetilmiş
sınıfın herhangi bir nesnesi, kullanmak için kendi
play() işlevini, duruma bakmaksızın, bulması gerekir.
Instrument2.cpp davranışı, C dilinin işlev yaklaşımı düşünüldüğü zaman
şaşırtıcı değildir. Konuyu kavramak için bağlama unsuru anlaşılmalıdır.
İşlev çağrı bağlamaları
Bir işlev çağrısına işlev gövdesini ilişiklendirmeye bağlama denilir.
Bağlama program çalıştırılmadan (derleyici ve ilişimci tarrafından yapılan)
önce gerçeklenirse buna erken bağlama denilir. Bu terim geleneksel
dillerde (örneğin C dilinde) bilinmiyordu, zira C derleyicilerinde zaten bu
çeşit işlev çağrısı bulunur, başka seçenek yoktur. Yani C dilinde sadece
erken bağlama vardır. Erken bağlamaya statik bağlama da denir.
Yukarıdaki program parçasında yeralan sorunun kaynağı, erken
bağlamadır. Çünkü sadece tek Instrument adresine sahip derleyici,
çağırdığı işlevin doğru olup olmadığını bilemez.
Çözüm; geç bağlamadır. Geç bağlama programın çalışması sırasında
yapılır, nesne tipine dayalı bir uygulamadır. Geç bağlamanın öteki adı
dinamik bağlama veya, çalışma zamanı bağlamasıdır. Bir dil geç
bağlamayı uyguladığı zaman, mutlaka çalışma sırasında nesne tipini
belirleyecek mekanizma olmalı, ve uygun üye işlev çağrılabilmelidir.
Derlemeli dillerde derleyici hala asıl nesne tipini bilmez, ama bulduğu
kodu yerleştirir, ve doğru işlev gövdesini çağırır. Geç bağlama
mekanizması dilden dile değişir, ama siz onu nesnelere yerleştirilmesi
zorunlu tip bilgileri olarak hayal edebilirsiniz. Mekanizmanın çalışma
biçimini ilerleyen bölümlerde göreceksiniz.
Sanal (virtual) İşlevler
Geç bağlamayı C++ dilinde belli bir işlev de gerçeklemek için, ana sınıfta
bildirilmiş işlevlerde virtual anahtar kelimesi kullanılmalıdır. Geç bağlama
sadece virtual işlevlerle olur ve sadece virtual işlevlerin olduğu ana sınıf
adresleri kullanılarak gerçeklenir. Ayrıca, önceki ana sınıfta tanımlanmış
olmalarına rağmen kural budur.
Üye işlevlerden birini sanal olarak bildirmek için, işlev bildiriminin başına
virtual anahtar kelimesini getirmek yeterlidir. Sadece bildirimlerde virtual
anahtar kelimesi kullanılabilir, tanımlarda kullanılmaz. Bir işlev ana sınıfta
virtual olarak bildirilirse, bu işlev bu ana sınıftan türetilmiş bütün
sınıflarda da virtual olarak kabul edilir. Türetilmiş sınıfta yeralan bir
virtual işlevin yeniden tanımlanmasına, genellikle baskınlık (overriding)
denilir.
Gördüğünüz gibi istenen; sadece, işlevin ana sınıfta virtual olarak
bildirilmesidir. Ana sınıf bildirim yapısına uyan bütün türetilmiş sınıf
işlevler, sanal mekanizma kullanılarak çağrılabilir. Türetilmiş sınıflardaki
bildirimlerde de, virtual anahtar kelimesini kullanabilirsiniz (zarar
vermez), ama yazmasanız daha iyi olur, zira kafanız karışabilir.
Instrument2.cpp den istenen sonucu almak için, ana sınıfta yeralan play()
işlevinden önce virtual anahtar kelimesini ekleyelim;
//:B15:Instrument3.cpp
//virtual anahtar kelimesi ile geç bağlama
#include <iostream>
using namespace std ;
enum note{middleC,Csharp,Cflat} ; //vs
class Instrument{
public:
virtual void play(note) const{
cout<<"Instrument::play"<<endl ;
}
};
//Wind nesneleri Instrumenttir
//zira aynı arayüze sahipler
class Wind : public Instrument{
public:
//arayüz baskınlığı
void play(note) const{
cout<<"Wind::play"<<endl ;
}
};
void tune(Instrument& i){
......
i.play(middleC) ;
}
int main( ){
Wind flute ;
tune(flute) ; //yukarıdaki tipe dönüştürme (yukarı döküm)
}///:~
Yukarıdaki Instrument3.cpp ile Instrument2.cpp de, virtual anahtar
kelimesi haricinde herşey aynıdır. Ama davranış tamamen farklıdır. Şimdi
çıktı Wind::play dir.
Genişletilebilirlik
Ana sınıfta virtual olarak tanımlanmış play( ) işlevi ile tune( ) işlevi
değiştirilmeksizin, istendiği kadar yeni tip eklenebilir. İyi tasarlanmış
nesne yönelimli programlamada işlevlerin çoğunluğu, veya hepsi, tune( )
işlev modelini izler, ve sadece ana sınıf arayüzü ile iletişimde bulunur.
Böyle bir program genişletilebilir zira, yeni işlev yetenekleri yeni veri
tiplerinin ortak ana sınıftan kalıtım yolu ile elde edilmesi ile eklenir. Ana
sınıf arayüzünde işlemlenen işlevlerin yeni sınıftakilere uyması için
değiştirilmesi de gerekmez.
Şimdi içinde çok sayıda virtual işlev ve yeni sınıf bulunan Instrument
örneğini, içindeki eski değişmemiş tune( ) işlevi ile nasıl doğru çalıştığını
görelim;
//:B15:Instrument4.cpp
//NYP nin genişletilebilirliği
#include <iostream>
using namepace std ;
class Instrument{
public:
virtual void play(note) const{
cout<<"Instrument::play"<<endl ;
}
virtual char* what( ) const{
return "Instrument" ;
}
//nesnenin değiştiğini kabul edelim
virtual void adjust(int){}
};
class Wind : public Instrument{
public:
void play(note) const{
cout<<"Wind::play"<<endl ;
}
char* what( ) const{return "Wind" ;}
void adjust(int) {}
};
class Percussion : public Instrument{
public:
void play(note) const{
cout<<"Percussion::play"<<endl ;
}
char* what( ) const{return "Percussion" ;}
void adjust(int){ }
};
class Stringend : public Instrument{
public:
void play(note) const{
cout<<"Stringend::play"<<endl ;
}
char* what( ) const{return "Stringend" ;}
void adjust(int){}
};
class Brass : public Wind{
public:
void play(note) const{
cout<<"Brass::play"<<endl ;
}
char* what( ) const{return "Brass" ;}
};
class Woodwind :public Wind{
public:
void play(note) const{
cout<<"Woodwind::play"<<endl ;
}
char* what( ) const{return "Woodwind" ;}
};
//daha önceki ile aynı işlev..
void tune(Instrument& i){
..........
i.play(middleC) ;
}
//yeni işlev
void f(Instrument& i){i.adjust(l) ;}
//dizi ilk değer atamalarında yukarı döküm (yukarıdaki tipe dönüştürme)
Instrument* A[]={
new Wind,
new Percussion,
new Stringend,
new Brass,
};
int main( ){
Wind flute ,
Percussion drum ;
Stringend violin ;
Brass flugelhorn ;
Woodwind recorder ;
tune(flute) ;
tune(drum) ;
tune(violin) ;
tune(flugelhorn) ;
tune(recorder) ;
f(flugelhorn) ;
}///:~
Gördüğünüz gibi Wind altında ikinci bir kalıtım seviyesi eklenmiştir, ama
virtual mekanizması kalıtım ne seviyede olursa olsun doğru çalışır. adjust
( ) işlevi Brass ve Woodwind için yeniden tanımlanmamıştır, yani
baskınlaşmamıştır. Eğer bu olsaydı, kalıtım hiyerarşisinde yeralan "en
yakın" tanım otomatik olarak kullanılırdı,-derleyici virtual işlev için her
zaman bazı tanımların olmasını güvenceye alır, böylece işlev gövdesi
bağlanmamış işlev çağrısı, hiçbir zaman olmaz. Aksi bir durum felakete
yolaçar.
Ana sınıf göstergeçleri A[] dizisinde bulunur, bu yüzden dizi ilk değer
atama süreci boyunca yukarıdaki tipe dönüştürme işlemi gerçeklenir
(yukarı döküm). Bu dizi ve f( ) işlevi, ilerleyen kısımda incelenecek.
tune( ) işlev çağrısında ise, her farklı tip nesnesi için yukarı döküm
gerçeklenir, böylece istenen eylem her zaman gerçekleşebilsin. Buradaki
işlem şöyle açıklanabilir; "nesneye bir ileti gönderilir, ve nesne onunla ne
yapacağı ile ilgili olarak merak içinde bırakılır". Bir projeyi çözümlemeye
çalıştığınız zaman, virtual işlevler mercek olarak kullanılırlar; ana sınıflar
nerede kullanılmalı ve programı ne kadar genişletmek isteyebilirsiniz?.
Bununla birlikte, programın başlangıcında uygun ana sınıf arayüzleri ve
virtual işlevler bulamasanız bile, çoğu kez daha sonra bunları
keşfedebilirsiniz, hatta çok sonra bile olabilir, genişletme yaparken veya
bakım sırasında bile olabilir. Bu çözümleme veya tasarım hatası değildir;
sadece başlangıçta bütün herşeyi bilmediğiniz veya bilemediğiniz anlamını
taşır. Yok eğer kötü bir şey olursa C++ dilinde sınıfın katı yapısı nedeni
ile, bu büyük bir sorun olmaz, zira dizgenin bir bölümünde bir değişiklik
yapılınca, C dilinde olduğu gibi bu değişiklik dizgenin öteki parçalarına
yayılmaz. Yani C++ dilinde hastalık heryere bulaşamaz. Ama C dilinde
hastalık hemen yayılır.
C++ da geç bağlama nasıl yapılır
Geç bağlama nasıl olur?.Bütün iş derleyici tarafından sahneye konur.
İstenilen (istenilme,virtual anahtar kelimesi iledir) gerekli geç bağlama
mekanizması derleyici tarafından düzenlenir. Programcıların C++ da
virtual işlevlerin mekanizmalarını kavraması çoğu kez yararlı olduğu için,
bu bölümde anılan mekanizmayı derleyicinin işlemleme biçimi ayrıntıları
ile irdelenecektir.
virtual anahtar kelimesi derleyiciye, erken bağlama yapılmayacağını
söyler. Bunun yerine geç bağlama yapılması için, gerekli bütün
mekanizmaların otomatik olarak yerleştirilmesini sağlar. Bunun anlamı;
eğer ana sınıf Instrument adresi aracılığı ile Brass nesnesi için play( )
işlevi çağrılırsa, uygun işlev işlemlenir.
Bunun başarılması için derleyici virtual işlevler içeren her sınıf için bir
tablo oluşturur (VTABLE). Derleyici o özel sınıftaki sanal işlevlerin
adreslerini VTABLE a yerleştirir. Sanal işlevlerin bulunduğu her sınıfa,
gizlice vpointer (VPTR olarak kısaltılır) denilen göstergeçler yerleştirilir,
ve bu göstergeçler o nesne için VTABLE ı işaret ederler. Ana sınıf
göstergeci aracılığı ile sanal işlev çağrısı yapıldığı zaman (yani çokbiçimli
çağrı yapıldığı zaman), derleyici VPTR yi getiren kodu yerleştirir ve
VPTR de işlev adresini arayıp bulur, ve böylece doğru işlev çağrılır ve geç
bağlama gerçekleşir.
Bunun hepsi- her sınıf için VTABLE kuruluşu, VPTR nin ilk değer
atamaları, sanal işlev çağrısı için kodun yerleştirilmesi- otomatik olarak
yapılır, bunlarla ilgili olarak sizin merak etmenizi gerektiren bir şey olmaz.
Sanal işlevlerle, derleyici nesnenin özel tipini bilemese bile, nesne için
doğru işlev çağrılır.
Aşağıdaki bölüm konu hakkında daha ayrıntılı bilgi vermektedir.
Tip bilgisinin saklanması
Farkedebileceğiniz üzere herhangi bir sınıfta açık biçimde tip bilgisi
bulunmaz. Ama önceki örneklerde, basit bir mantıkla, nesnelerde bir çeşit
tip bilgisi bulunmak zorundaydı; aksi taktirde çalışma sırasında tip
bilinemezdi. Bu doğrudur, ama tip bilgisi gizlenmiştir. Bunu görmek için,
şimdi sınıf büyüklüklerini sanal işlev bulunan ve bulunmayanlarla
karşılaştırma yaparak irdeleyelim, işte bir örnek;
//:B15:Sizes.cpp
//sanal işlev bulunan/bulunmayan nesne büyüklükleri
#include <iostream>
using namespace std ;
class NoVirtual{
int a ;
public:
void x( ) const {}
int i( ) const {return l;}
};
class OneVirtual{
int a;
public:
virtual void x( ) const{ }
int i( ) const{return l;}
};
class TwoVirtuals{
int a ;
public:
virtual void x( ) const{ }
virtual int i( ) const{return l;}
};
int main( ){
cout<<"int: "<<sizeof(int)<<endl ;
cout<<"NoVirtual: "<<sizeof(NoVirtual)<<endl ;
cout<<"void*: "<<sizeof(void*)<<endl ;
cout<<"OneVirtual: "<<sizeof(OneVirtual)<<endl ;
cout<<"TwoVirtuals: "<<sizeof(TwoVirtuals)<<endl ;
}///:~
Sanal işlevlerin olmadığı durumda nesne büyüklüğü beklendiği gibidir,
yani int büyüklüğündedir. OneVirtual sınıfında bir sanal işlevin
bulunduğu durumda ise nesne büyüklüğü; NoVirtual artı void göstergeç
büyüklüğünün toplamı kadardır. Bir veya daha fazla sanal işlev olursa,
derleyici yapıya sadece bir göstergeç (VPTR) yerleştirir. OneVirtual ile
TwoVirtuals arasında nesne büyüklüğü farkı yoktur, yani büyüklükler
aynıdır. Sebebi; VPTR nin işlev adreslerinin tablosunu göstermesidir.
Sadece bir tabloya gereksinim duyulmaktadır, zira bütün sanal işlevlerin
adresleri tek bir tabloya yerleştirilir.
Bu örnekte en az bir veri üye gerekmektedir. Eğer hiç veri üyesi
olmasaydı, C++ derleyicisi, her nesne mutlaka farklı adrese sahip olmak
zorunda olduğu için, nesneleri sıfırdan farklı büyüklükte olmaya
zorlayacaktı. Eğer sıfır boyutlu nesneler dizisine, yerleştirimi hayal
ederseniz, durum daha iyi anlaşılır. Aksi halde sıfır boyutlu olacağı için,
nesnelere "kukla" (dummy) bir üye yerleştirilir. virtual anahtar
kelimesinden ötürü tip bilgisi yerleştirildiği zaman, bu, "kukla" üyenin
yerini alır. Bunu anlamak için yukarıdaki örnekte int a yı bütün sınıflar
için siz yorumlayın.
virtual işlevleri görselleştirme
virtual işlevlerin kullanıldığı durumlarda, perdenin arkasında ne olduğunu
tam kavramak için, etkinlikleri görselleştirmek çok yararlıdır. Burada
Instrument4.cpp de A[] göstergeçler dizisinin çizimi verilmiştir.
Instrument Göstergeçleri
VTABLE'lar
Nesneler
A[]
Wind nesnesi
vptr
&Wind::play
&Wind::what
&Wind::adjust
&Percussion::play
&Percussion::what
Percussion nesnesi
vptr
&Percussion::adjust
Stringed nesnesi
&Stringed::play
vptr
&Stringed::what
&Stirnged::adjust
Brass nesnesi
&Brass:play
vptr
&Brass:what
&Brass:adjust
Instrument göstergeçler dizisinde belli bir tip bilgisi bulunmaz; herbiri
Instrument Wind, Percussion, Stringend, ve Brass tip nesnelerini işaret
eder ve hepsi bu kategoriye uyar, zira hepsi Instrument'ten türetilmiştir.
(bunlar Instrument arayüzü ile aynı arayüze sahiptirler ve aynı iletileri
gönderirler), bu yüzden bunların adresleri ayrıca diziye yerleştirilebilirler.
Bununla birlikte, derleyici bunların Instrument nesnelerinden başka
birşey olmadıklarını bilmez, böylece normalde bütün işlevlerin ana sınıf
sürümlerinin çağrıldığı kendi araçlarına bırakılırlar. Ama burada, bütün bu
işlevler virtual anahtar kelimesi ile bildirilirler, bu yüzden bazı şeyler
farklı çalışırlar.
Sanal işlevler içeren bir sınıf oluşturulduğu her sefer, veya sanal işlevler
bulunduran bir sınıftan yeni sınıf türetildiğinde derleyici bu sınıf için tek
bir VTABLE oluşturur, diyagramın sağ yanında görülmektedir. Bu
tabloya, ana sınıfta veya bu sınıfta bulunan, sanal olarak bildirilmiş bütün
işlevlerin adresleri yerleştirilir. Ana sınıfta sanal olarak bildirilen
işlevlerden herhangi birine, başka yerlerde baskınlık verilmemişse,
derleyici ana sınıf sürüm adresini türetilmiş sınıfta kullanır. (Bunu Brass
VTABLE da adjust girişinde görebilirsiniz.) Daha sonra VPTR yi sınıfa
yerleştirir. (Sizes.cpp de incelendi). Buna benzer basit kalıtım kullanıldığı
zaman, her nesne için tek VPTR bulunur. VPTR nin ilk değer ataması
mutlaka uygun VTABLE in başlangıç adresini göstermelidir. (daha sonra
ayrıntılarını göreceğiniz üzere, bu işlem yapıcı işlevde olur.)
VPTR nin uygun VTABLE dan ilk değer ataması bir kez yapılınca, etkin
nesne, tipin ne olduğunu bilir. Ama bu kendinden bilgilenme, sanal işlevin
çağrıldığı noktada kullanılmazsa değersizdir. Ana sınıf adresi aracılığı ile
bir sanal işlev çağrıldığı zaman (erken bağlama için derleyicinin gereken
bütün verilere sahip olmadığı bir durumdur), bazı özel şeyler olur. Tipik
işlev çağrımının gerçeklenmesi yerine, belli bir adrese assembly dili işlev
(CALL) çağrısı basit bir şekilde yapılır, derleyici işlev çağrısını
gerçeklemek için farklı bir kod üretir. İşte eğer Instrument göstergeçi ile
Brass nesnesi için adjust işlev çağrımının nasıl olduğu, (Instrument
dayancı da aynı sonucu üretir)
Derleyici nesnenin başlangıç adresini işaret eden Instrument göstergeci
ile işe başlar. Bütün Instrument nesneleri ve ondan türetilmiş nesnelerin
VPTR leri aynı yerde bulunur ( çoğunlukla nesne başlangıcında), böylece
derleyici nesne dışında bir yerde toplar. VPTR, VTABLE başlangıç
adreslerini gösterir. Bütün VTABLE işlev adresleri nesnenin özel tipine
bakılmaksızın aynı sıralama biçiminde yer alır, ilki play( ) işlevi , ikincisi
what( ) ve üçüncüsü de adjust( ) işlevidir. Derleyici nesnenin özel tipini
dikkate almadan adjust( ) işlevinin VPTR+2 de bulunduğunu bilir. Bu
yüzden "Instrument::adjust kesin adresindeki işlevi çağır" (geç bağlama
için hatalı) yerine üretilen kod "VPTR+2 deki kodu çağır" anlamını taşır.
VPTR nin getirilişi ve gerçek işlev adresi programın çalışması sırasında
saptandığı için, geç bağlama işlemi yapılabilir. Nesneye ileti gönderilir,
nesnede ileti ile yapacaklarını belirler.
Göze çarpmayanlar
Sanal işlev çağrısı ile üretilen assembly dili kodlarını izlemek çok
yararlıdır. Bu sayede geç bağlamanın olduğu yer görülebilir.
Derleyicilerden birinin aşağıdaki işlev çağrısı için olan çıktısı;
i.adjust(l) ;
f(Instrument& i) işlevinin içinde
push
push
mov
call
add
l
si
bx, word ptr [si]
word ptr [bx+4]
sp, 4
C++ dilinin işlev çağrı değişkenleri C dili gibidir, ve yığıta sağdan sola
doğru gönderilir. (bu sıralama C dilini C++ da desteklemek amacı ile
yapılmıştır.) Bundan dolayı ilk önce l yığıta gönderilir. İşlevin bu
noktasında si kaydedicisi (Intel X86 işlemcilerinde bulunan bir kaydedici)
i nin adresini bulundurur. İlgili nesnenin başlangıç adresi olduğu için, bu
da yığıta gönderilir. this değerine karşılık gelen başlangıç adresi
hatırlanırsa, her üye işlev çağrımından önce, this değişken olarak
bütünüyle yığıta gönderilir. Böylece üye işlev hangi özel nesne ile
çalıştığını bilir.
Böylece her zaman üye işlev çağrımından önce, birden fazla sayıda
değişken yığıta gönderilir. (static üye işlevler hariç, zira bunların this
göstergeçi yoktur.)
Şimdi asıl sanal işlev çağrısı gerçeklenmesi zorunludur. Önce VPTR
üretilmelidir, böylece VTABLE bulunabilir. Bu derleyici için nesnenin
başlangıcına VPTR yerleştirilir, böylece this içeriği VPTR ye karşılık
gelir. Aşağıdaki satır
mov bx, word ptr [si]
si tarafından gösterilen word (yani, this) getirilir, bu da VPTR dir. VPTR,
bx kaydedicisine yerleştirilir. bx te bulunan VPTR VTABLE in başlangıç
adresini gösterir. Ama çağrılan işlev göstergeci VTABLE in sıfırı yerinde
değildir. Onun yerine ikinci yerde bulunur. (zira listedeki üçüncü işlevdir.)
Bu bellek modelinde her işlev göstergeci 2 bayt uzunluğundadır, bu
yüzden derleyici doğru işlev adresini hesaplayabilmek için VPTR ye 4
ekler. Bu değer sabittir. Derleme sırasında belirlenir. Böylece iki numaralı
yerdeki işlev göstergeci adjust( ) işlevi içindir. Allahtan ki derleyici bizim
için bütün rezervasyonları yapar ve belli bir sınıf hiyerarşisindeki bütün
VTABLE lardaki bütün işlev göstergeçlerini aynı sıralamayla güvenceye
alır. Türetilmiş sınıflardaki baskınlıklar dikkate alınmadan bunlar yapılır.
VTABLE da bulunan uygun işlev göstergeci bir kez hesaplanır, ve o işlev
çağrılır. Böylece adres getirilir ve hepsi birden açıklamaya çağrılır.
call word ptr [bx+4]
Son olarak yığıt göstergeci çağrıdan önce yerleştirilen değişkenleri silmek
için geriye doğru hareket ettirilir. C ve C++ assmbly kodunda çoğunlukla
çağırıcının değişkenleri silmesi görülür, ama bu derleyici ve
mikroişlemciye bağlı olarak değişebilir.
vgöstergecinin yüklenmesi
VPTR nesne sanal işlev davranışını gösterdiğinden, VPTR nin her zaman
doğru VTABLE i göstermesinin önemi anlaşılır. VPTR ilk değer ataması
uygun olarak yapılmadan önce, sanal işlevlere hiç bir zaman çağrı
yapılamaz. Doğal olarak ilk değer atamaları, güvenli olarak yapıcı
işlevlerde yapılır. Ama Instrument örneklerinin hiç birinde yapıcı işlev
bulunmamaktadır.
Burası esas olarak varsayılan yapıcı işlevin bulunduğu yerdir. Instrument
örneklerinde, derleyici varsayılan yapıcıyı sadece VPTR ye ilk değer
ataması yapması üretir. Bu yapıcı işlev Instrument nesneleri ile bir şey
yapmadan önce, onlar için otomatik olarak çağrılır. Böylece sanal işlev
çağrıları güvenceye alınmış olur.
VPTR nin yapıcı işlevde ilk değer atamasının otomatik olarak yapılmasının
içerikleri, ilerleyen bölümlerde incelenecek.
Nesneler farklıdır
Yukarıdaki tipe dönüştürmenin (yukarı döküm) sadece adreslerle
gerçeklenmesi önemlidir. Eğer derleyicinin bir nesnesi varsa, tipini tam
olarak bilir ve bundan dolayı işlev çağrılarında (C++ da) geç bağlama
yapmaz- veya en azından, derleyicinin geç bağlama yapmaya ihtiyacı
olmaz. Verimlilik uğruna derleyicilerin çoğunluğu, bir nesne için sanal
işlev çağrısı yapıldığı zaman erken bağlama yaparlar, zira nesne tipini tam
olarak bilirler. Bir örnek verelim;
//:B15:Early.cpp
//Erken bağlama ve sanal işlevler
#include <iostream>
#include <string>
using namespace std ;
class Pet{
public:
virtual string speak( ) const{return " " ;}
};
class Dog : public Pet{
public:
string speak( ) const{return "Hav" ;}
};
int main( ){
Dog alf ;
Pet* p1=&alf ;
Pet& p2=alf ;
Pet p3 ;
//Her ikisi için geç bağlama
cout<<"p1->speak( )= "<<p1->speak( )<<endl ;
cout<<"p2.speak( )= "<<p2.speak( )<<endl ;
//Erken bağlama da olabilir
cout<<"p3.speak= "<<p3.speak( )<<endl ;
}///:~
p1->speak( ) ve p2.speak( ) te adresler kullanılır. Bu bilgiler yeterli değil
demektir. p1ve p2, Pet veya Pet'ten türetilmiş nesne adreslerini
gösterirler. Bu yüzden sanal mekanizma mutlaka kullanılmalıdır. p3.speak
( ) te ise, hehangi bir müphemlik bulunmaz. Derleyici onun bir nesne
olduğunu ve tipini tam olarak bilir. Bu yüzden Pet'ten türetilmiş bir nesne
olması imkansızdır ve o yalnızca Pet'tir. Bundan dolayı büyük ihtimalle
erken bağlama yapılır. Bununla birlikte derleyici eğer çok sıkı çalışmak
istemezse, hala geç bağlamayı uygulayabilir ve aynı davranışlar elde edilir.
Neden sanal işlevler?
Bu noktada şunu sorabilirsiniz; Bu teknik eğer bu kadar önemli ise ve eğer
"doğru" işlevin çağrılmasını her zaman gerçekliyorsa, neden bir seçenek?.
Niçin onun hakkında bilgi sahibi olmaya ihtiyacımız var?.
İyi bir soru. Yanıt ise, C++ temel felsefesinin parçasıdır; "verimlilik
açısından tam uygun olamadığı için". Önceki assembly dili çıktısından
görüldüğü üzere, sanal işlev çağrısını gerçekleştirmek için esas adrese tek
basit CALL yerine iki -veya daha fazla incelikli- assembly komutu
gerekmektedir. Bu da hem kod alanı hemde çalışma süresi
gerektirmektedir.
Bazı nesne yönelimli dillerin geç bağlamaya yaklaşımı o kadar asli
unsurdur ki, her zaman kullanılması lüzumlu olup seçenek değildir ve
kullanıcının onunla ilgili birşey bilmesi gerekmez. Bu, dil oluşturulurken
verilen tasarım kararıdır. Örnek olarak Smalltalk, Java ve Python bu kararı
başarı ile uygulamaktadırlar. Bununla birlikte C++ verimliliğin esas
olduğu C dilinin soyundan gelmektedir. Hepsinden öte C dili, işletim
sistemi uygulanmasında, assembly dili yerine kullanmak için
oluşturulmuştu. (Unix, önceki işletim sistemlerine göre daha farklı
ortamlara kolaylıkla aktarılabilir). C++ dilinin geliştirilmesinin temel
nedenlerinden biri; C dili programcılarını daha verimli kılmak içindir. Bell
laboratuvarlarında C++ dili geliştirilirken, çok sayıda C programcısı da
bulunuyordu, verimlilik artışı ile ile birlikte şirket milyonlarca dolar
kazanacaktı. C programcıları C++ dili ile karşılaştıkları zaman ilk
sordukları; ne kadar büyüklük ve hız kazancı sağlayabilecek. Eğer yanıt
"herşey, biraz ilave maliyete neden olan işlev çağrıları hariç aynıdır" ise
programcılarının tercihi C++ dan ziyade C dili olurdu. Ek olarak satıriçi
işlevlerin kullanımı olanaklı olamayacaktı, zira sanal işlevlerin VTABLE'a
yerleştirilmeleri için mutlaka bir adresleri olmalıdır. Bu yüzden sanal
işlevler bir seçenektir, ve dilde varsayılanlar ise sanal değildir. Sanal
olmayan bu varsayılanlar, en hızlı düzenlemedir. Stroustroup temel
kılavuzu; "kullanmazsanız bedel ödemezsiniz" dir. Bunu programcı olarak
unutmayın.
Bundan dolayı sanal işlev anahtar kelimesi virtual, verimliliği akort etmek
içindir. Sınıf tasarımları esnasında verimlilik akortu konusunda herhangi
korku duyulmamalıdır. Çokbiçimlilik kullanılacaksa gereken heryerde
sanal işlev kullanmaktan kaçınılmamalıdır. Kodları hızlandırmak için
yapılması gereken, sanal olmayan işlevleri sadece arayıp bulmaktır.
(genellikle çok daha büyük kazançlar, başka alanlar olsa idi, bulunurdu. İyi
bir kesitçi programdaki darboğazları bularak daha iyi iş çıkarır ve tahminle
iş yapmaktan daha yararlıdır.)
Hız ve büyüklük konusunda yaşananlar göstermiştir ki, C++ dili ile C dili
arasında % 10 fark vardır, veya çoğunlukla da birbirine yakındır. Daha
uygun büyüklük ve daha fazla hız verimliliğinin nedeni; C++ programının
C den daha küçük ve daha hızlı yoldan yazılabilmesindendir.
Soyut ana sınıflar ve saf sanal işlevler
Çoğu kez ana sınıf tasarımlarında, türetilmiş sınıflar için sadece tek arayüz
olması istenir. Yani, aslında birinin ana sınıftan nesne üretmesi istenilmez,
sadece yukarıdaki tipe dönüşüm için arayüzün kullanılabilmesi istenir.
Bunu başarmak için o sınıf soyut (abstract) hale getirilir, bunun içinde o
sınıfta en az bir saf sanal işlev bulundurulur. Saf sanal işlevleri, virtual
anahtar kelimesi kullanıp o işlevi sıfıra (=0) eşitleyerek elde ederiz.
Programda soyut sınıfın bir nesnesi oluşturulmak istenirse, derleyici bunu
engeller. Özel tasarımlar için bunu bir araç olarak kullanabilirsiniz.
Soyut bir sınıf türetildiği zaman, bütün saf sanal işlevler mutlaka
uygulamaya konmalıdır, ya da türetilen sınıfta soyut olur. Saf sanal işlev
oluşturmak arayüze üye işlev yerleştirilmesine imkan verir ve ayrıca üye
işlev koduna gereksiz gövde eklenmesi de gerekmez. Saf sanal işlevler
aynı zamanda, türetilmiş sınıflarda kendileri için tanımları zorunlu kılarlar.
Instrument örneklerinin tamamında, ana sınıf Instrument'te bulunan
işlevler her zaman "kukla" (dummy) işlevlerdi. Bu işlevler kazara
çağrılırsa, bazı şeyler hatalı olur. İşte bu nedenle Instrumet sınıfı,
kendinden türetilmiş bütün sınıflara ortak bir arayüz sağlamak amacını
taşımaktadır.
Ortak arayüz oluşturmanın tek nedeni; bu sayede her farklı alt tip için,
farklı açıklamalar sağlanabilmesidir. Böylece bütün türetilmiş sınıflar için
ortak kullanılabilen biçim oluşturulur, başka birşey değil. Instrument bu
nedenle soyut sınıf için uygun adaydır. Soyut sınıf oluşturmanın amacı;
sadece bir grup sınıfı ortak arayüz kullanarak işlemlemektir, ama ortak
arayüzün uygulamaya gereksinimi yoktur. (veya en azından, tam
uygulamaya)
Instrument gibi soyut sınıf olmaya uygun bir unsurunuz varsa, bu unsurun
nesnesi hemen hemen her zaman anlamsızdır. Yani Instrument sadece
arayüzü açıklamak içindir, özel bir uygulamayı değil, sadece Instrument
olan bir nesne oluşturulursa anlamı olmaz, ve büyük olasılıkla kullanıcının
bunu yapması engellenir. Engelleme Instrument'teki bütün sanal
işlevlerin hata iletileri üretmesi ile sağlanır, ama bu da hata iletisinin
çalışma zamanına kadar görünmesini geciktirir ve kullanıcı tarafında
güvenilir yorucu sınamalar gerektirir. Sorunu derleme sırasında yakalama
çok daha iyidir.
Saf sanal bildirim için kullanılan yazım biçimi aşağıdaki gibidir.
virtual void f( )=0 ;
Bu yazılarak VTABLE da bulunan işlev için yer ayrılır ama ayrılan yere
bir adres konmaz. Sınıf içinde sadece bir işlev saf sanal olarak bildirilse
bile, VTABLE tamam değildir.
Eğer bir sınıf için VTABLE tamam değilse, birisi o sınıfın bir nesnesini
oluşturmaya çalıştığı zaman derleyicinin ne yapacağı beklenecek?. Güvenli
bir şekilde soyut sınıfın nesnesi oluşturulamaz, bu yüzden derleyiciden
hata iletisi alınır. Böylece derleyici soyut sınıfın saflığını güvenceye alır.
Bir sınıfın soyut yapılması, o sınıfın müşteri tarafından yanlış kullanımını
engeller.
Şimdi saf sanal işlevler kullanılarak değişime uğratılmış Instrument4.cpp
örneğini inceleyelim. Sınıfta saf sanal işlevler dışında başka birşey
bulunmadığı için, bu sınıfı saf soyut sınıf olarak adlandırabiliriz.
//:B15:Instrument5.cpp
//saf soyut ana sınıflar
#include <iostream>
using namespace std ;
enum note{middleC, Csharp, Cflat}
class Instrument{
public:
//saf sanal işlevler
virtual void play(note) const=0 ;
virtual char* what( ) const=0 ;
//nesneyi değiştirdiği sanılıyor
virtual void adjust(int)=0 ;
};
//geri kalan dosya aynı
class Wind : public Instrument{
public:
void play(note) const{
cout<<"Wind::play"<<endl ;
}
char* what( ) const{return "Wind" ;}
void adjust(int){}
};
class Percussion : public Instrument{
public:
void play(note) const{
cout<<"Percussion::play"<<endl ;
}
char* what( ) const{return "Percussion" ;}
void adjust(int){}
};
class Stringed : public Instrument{
public:
void play(note) const{
cout<<"Stringed::play"<<endl ;
}
char* what( ) const{retrun "Stringed" ;}
void adjust(int){}
};
class Brass : public Wind{
public:
void play(note) const{
cout<<"Brass::play"<<endl ;
}
char* what( ) const{return "Brass" ;}
};
class Woodwind : public Wind{
public:
void play(note) const{
cout<<"Woodwind::play"<<endl ;
}
char* what( ) const{return "Woodwind" ;}
};
//öncekilere eşdeğer işlev
void tune(Instrument& i){
.....
....
i.play(middleC) ;
}
int main( ){
Wind flute ;
Percussion drum ;
Stringed violin ;
Brass flugelhorn ;
Woodwind recorder ;
tune(flute) ;
tune(drum) ;
tune(violin) ;
tune(flugelhorn) ;
tune(recorder) ;
f(flugelhorn) ;
}///:~
Saf sanal işlevler çok yararlıdır, zira sınıfın soyutluğunu açık biçimde
belirtirler. Ve ayrıca hem kullanıcıya hem de derleyiciye kullanımları
düzenlemişlerdir.
Saf sanal işlevler soyut sınıfın işleve değerle aktarım yapmasını
engellerler. Bu ayrıca nesne doğranmasını önlemenin bir yoludur. (kısaca
anlatılacak). Bir sınıf soyut yapıldığında, bir göstergeç veya dayanç her
zaman bu sınıfa yukarı döküm yapılırken (onun tipine dönüştürülürken)
kullanılır.
Saf sanal işlevin VTABLE'in tamamlanmasını engellemesi, başka öbür
işlev gövdelerinin istenmediği anlamına gelmez. Sanal olsa bile çoğu kez
bir işlevin ana sınıf sürümü çağrılmak istenir. Hiyerarşi köküne, ortak koda
olası en yakın olanı yerleştirmek iyi bir düşüncedir. Sadece kod alanı
korunmakla kalmaz, değişikliklerin kolaylıkla sisteme yayılması sağlanır.
Saf sanal işlevler
Ana sınıfta yer alan bir saf sanal işlev için tanım vermek mümkündür.
Aslında derleyiciye hala, soyut ana sınıf nesnelerine izin vermeyeceği
söylenmektedir. Nesne oluşturmak için saf sanal işlevlerin tanımları, hala
türetilmiş sınıflarda verilmek zorundadır. Bununla birlikte istenen kodda
ortak parça olabilir veya bütün türetilmiş sınıf tanımlarında her işlevde
kodların tekrarından ziyade bunlar çağrılır.
Saf sanal tanımları gösteren bir örnek;
//:B15:PureVirtualDefinitions.cpp
//saf sanal ana tanımlar
#include <iostream>
using namespace std ;
class Pet{
public:
virtual void speak( ) const=0 ;
virtual void eat( ) const=0 ;
//satıriçi saf sanal işlevler geçersizdir
//! virtual void sleep( ) const=0 {}
};
//satıriçi tanımlanmadığı için kabul
void Pet::eat( ) const{
cout<<"Pet::eat( )"<<endl ;
}
void Pet::speak( ) const{
cout<<"Pet::speak( )"<<endl ;
}
class Dog : public Pet{
public:
//ortak Pet kodunu kullan
void speak( ) const {Pet::speak( ) ;}
void eat( ) const {Pet::eat( ) ;}
};
int main( ){
Dog simba ;
simba.speak( ) ;
simba.eat( ) ;
}///:~
Pet VTABLE inde ayrılan yer hala boş bulunmaktadır. Fakat aynı isimli
işlev, türetilmiş sınıflarda çağrılabilir. Bu özelliğin başka bir yararı da,
normal sanal işlevin, kod değişimi olmadan saf sanal hale
getirilebilmesidir. (Bu ayrıca sanal işlevin baskınlığını önleyen sınıf
yerleşimi için bir yoldur.)
Kalıtım ve VTABLE
Kalıtım gerçeklenirken ve bazı sanal işlevler baskınlaştırılırken, olanları
hayal edebilirsiniz. Derleyici, yeni sınıf için yeni VTABLE oluşturur. Yeni
işlev adresini ana sınıf adresleri kullanarak baskınlaştırılmamış herhangi
bir sanal işlev için yerleştirir. Bir biçimde veya ikinci bir yolla
oluşturulabilecek nesneler için (yani sınıfında saf sanal işlev bulunmaz)
VTABLE de her zaman işlev adreslerinin tamamı bulunur, bu yüzden
orada bulunmayan bir adrese hiç bir zaman çağrı yapılamaz (felaket
olurdu).
Evet ama, türetilmiş sınıflarda yeni türetimler yaparsanız ve yeni sanal
işlevler eklerseniz ne olur?. Şimdi bu durum için bir örnek verelim;
//:B15:AddingVirtual.cpp
//türetilenlere sanal işlevler ekleme
#include <iostream>
#include <string>
using namespace std ;
class Pet{
string pname ;
public:
Pet(const string& petName) : pname(petName){}
virtual string name( ) const {return pname ;}
virtual string speak( ) const {return " " ;}
};
class Dog : public Pet{
string name ;
public:
Dog(const string& petName) : Pet(petName){
//Dog sınıfında yeni sanal işlev
virtual string sit( ) const{
return Pet::name( ) + " sits" ;
}
string speak( ) const{ //baskın
return Pet::name( )+ "says 'Bark!' " ;
}
};
int main( ){
Pet* p[]={new Pet("generic"), new Dog("bob")} ;
cout<<"p[0]->speak( )= "
<<p[0]->speak( )<<endl ;
cout<<"p[1]->speak( )= "
<<p[1]->speak( )<<endl ;
//! cout<<"p[1]->sit( )= "
//!
<<p[1]->sit( )<<endl ; //geçersiz
}///:~
Pet sınıfında iki tane sanal işlev bulunmaktadır; speak( ) ve name( ). Dog
sınıfı üçüncü sanal işlevi ilave etmektedir. Bu yeni işlev speak( ) işlevinin
anlamına baskın olmaktdır. Gözönüne getirebilmek için unsurları çizgeye
yerleştirmek iyi bir seçenektir.İşte Dog ve Pet sınıfları için derleyicinin
oluşturduğu VTABLE lar.
Derleyici speak( ) işlevinin adres yerini aynı Pet VTABLE da olduğu gibi
Dog VTABLE ın tamamen aynı noktasına zımbalar. Benzer şekilde Dog
sınfından Pug gibi bir sınıf türetilirse, bunun sit( ) sürümünün VTABLE a
yerleşme yeri Dog daki ile aynı noktadır. Bunun nedeni (assmebly
örneğinde görüldüğü gibi) derleyicinin ürettiği kod sanal işlevi seçmek için
VTABLE da basit bir kayma kullanır. Nesnenin ait olduğu alt tip gözardı
edilerek, bunun VTABLE'ı aynı şekilde yerleştirilir, böylece sanal
işlevlerin çağrımları her zaman aynı şekilde yapılır.
Bununla birlikte, bu durumda derleyici sadece ana sınıf nesnelerini işaret
eden göstergeçlerle çalışır. Ana sınıfta sadece name( ) ve speak( ) işlevleri
bulunur, bu yüzden derleyici sadece bu işlevlerin çağrılmasına izin verir.
Sadece ana sınıfaı işaret eden göstergece izin varsa Dog nesnesi ile
çalışmak nasıl mümkün olacak?. Bu göstergeç içinde sit( ) işlevi
bulunmayan başka diğer tipleri de işaret edebilmelidir. VTABLE'ın bu
noktasında başka öteki işlevler olabilir de olmayabilir de, ama herhangi
birinde, VTABLE'ın o noktasına sanal çağrı yapmak istenen bir şey
değildir. Sonuç olarak derleyici, sadece türetilmiş sınıflarda bulunan
işlevlere sanal çağrı yapılmasını önleyerek, işini yapar.
Çok daha az rastlanan bazı durumlar bulunmaktadır; göstergecin aslında
özel bir alt sınıf nesnesini işaret etmesi. Eğer sadece bu özel alt sınıfta
bulunan işleve çağrı yapılırsa, o zaman mutlaka göstergecin tip değişimi
gerekir. Önceki örnek programda üretilen hata iletisini ortadan kaldırmak
için yazılması gereken kod;
((Dog*)p[1])->sit( ) ;
Burada p[1] in, Dog nesnesini gösterdiği bilindiği düşünülüyor, aslında
genellikle bilinmez. Eğer sorun bütün nesnelerin tam tipleri bilinerek
kurgulanırsa, bunu yeniden düşünmeniz lazım, zira sanal işlevleri herhalde
uygun şekilde kullanmıyolar. Bununla birlikte, kaynak kapta bulunan
bütün nesnelerin tam tipleri bilinirse tasarımın en iyi şekilde çalıştığı bazı
durumlar bulunmaktadır (veya başka seçenek yoktur). Bu, çalışma zamanı
tip tanımlaması sorunu olarak adlandırılır. (RTTI=run-time type
identification türkçesi: ÇZTT=çalışma zamanı tip tanımlaması)
ÇZTT ana sınıf göstergeçlerinin türetilmiş sınıf göstergeçlerinin tipine
(aşağı doğru tip dönüştürme) aşağı döküm yapılmasıdır. ("aşağı" ve
"yukarı" ifadeleri sınıf çizelgelerindeki görecelilikten kaynaklanır, ana
sınıf her zaman en yukarıda bulunur). Yukarı döküm yani yukarıdaki tipe
dönüştürme otomatik olarak zorlamaksızın olur, zira tamamen güvenlidir.
Aşağı döküm yani aşağıdaki tipe dönüştürme güvenilmezdir, zira tiplerin
doğruluğu hakkında derleme sırasında bilgi bulunmaz, bu yüzden nesne
tipi tam olarak bilinmek zorundadır. Yanlış tipe dönüştürme yapılırsa,
başınız büyük dertte demektir.
ÇZTT bu bölümün ilerleyen kısımlarında ayrıntıları ile incelenecek, daha
sonraki ciltte ise bu konuya özel bir bölüm ayrılacaktır.
Nesne doğrama
Çokbiçimlilik kullanılırken nesneleri değerle aktarma ile, nesnelerin
adreslerini aktarma arasında büyük fark bulunur. Şimdiye kadar görülen
bütün örneklerde ve görülmesi lazım sanal olanlarda adresler aktarıldı,
değerle aktarım yapılmadı. Bunun nedeni adreslerin hepsi aynı büyüklükte
idi (aslında bütün makinelerde göstergeçlerin hepsi gerçekte aynı
büyüklükte değildir, burada inceleme için böyle kabul edilmişlerdir),
böylece türetilmiş sınıfların nesne (genellikle daha büyük nesneler)
adresini aktarma ile ana sınıfın nesne (genellikle daha küçük nesneler)
adreslerini aktarma, birbirinin aynısıdır.Daha önce açıklandığı gibi
çokbiçimliliğin amacı budur, ana tipi işlemleyen kod geçişgen bir şekilde
türetilmiş tip nesneleri de işlemler.
Eğer göstergeç veya dayanç yerine bir nesneye yukarı döküm yapılırsa, sizi
şaşırtacak bazı olaylar ortaya çıkar; nesne "doğranır" ta ki dökümün varış
tipine denk gelen, geri kalan herşey altnesne olana kadar. Aşağıdaki
örnekte nesne doğranma işleminde olanları göreceksiniz.
//:B15:ObjectSlicing.cpp
#include <iostream>
#include <string>
using namespace std ;
class Pet{
string pname ;
public:
Pet(const string& name) : pname(name){}
virtual string name( ) const{return pname ;}
virtual string description( ) const{
return "This is " +pname ;
}
};
class Dog : public Pet{
string favoriteActivity ;
public:
Dog(const string& name, const string& activity)
: Pet(name), favoriteActivity(activity){}
string description( ) const{
return Pet::name( ) + "likes to " +
favoriteActivity ;
}
};
void describe(Pet p){//nesne doğranır
cout<<p.description( )<<endl ;
}
int main( ){
Pet p("Alfred") ;
Dog d("fluffy", "sleep") ;
describe(p) ;
describe(d) ;
}///:~
describe( ) işlevi Pet tipinin nesnesine değerle aktarılmaktadır. Daha sonra
Pet nesnesi için description( ) sanal işlevi çağrılır. main( ) işlevinde ilk
önce "This is Alfred" i üreten çağrı yapılır, ikinci olarak "fluffy likes to
sleep" üretilir. Aslında her iki çağrı da description( ) ana sınıf sürümünü
kullanılır.
Programda iki şey olmaktadır önce, describe( ) işlevi Pet nesnesi kabul
ettiği için(göstergeç veya dayanç dışında), describe( ) işlevine yapılan
herhangi bir çağrı ile Pet büyüklüğünde bir nesne yığıta gönderilir,
çağrıdan sonra ise silme işlemi yapılır. Bunun anlamı Pet sınıfından
türetilmiş bir sınıf nesnesi describe( ) işlevine aktarılırsa, derleyici bunu
kabul eder, ama sadece nesnenin Pet kısmı kopyalanır. Nesnenin türetilmiş
kısmının dışındakileri doğrar, aşağıdaki gibi;
Şimdi sanal işlev çağrıları hakkında meraklanacaksınız. Dog::description(
) hem Pet (hala bulunur) hem de Dog (doğrandığı için yoktur) kısımlarını
kullanır. Peki o zaman sanal işlev çağrıldığı zaman ne olur?.
Nesne değerle aktarıldığı için felaketten korunulmuş olur. Bu nedenle
derleyici, nesnenin tipini tam olarak bilir, zira türetilen nesne, ana sınıf
nesnesi olmaya mecbur bırakılmaktadır. Değerle aktarım olduğu zaman
Pet nesnesi için kopya yapıcı işlev kullanılır, ve VPTR nin ilk değerlerini
Pet VTABLE'na atar ve sadece nesnenin Pet kısımlarını kopyalar. Burada
açık biçimde kopya yapıcı işlev bulunmaz, derleyici bunu oluşturur. Her
türlü yaklaşımda, doğrama sırasında nesne, gerçekten Pet'tir.
Nesne doğrama aslında, yeni nesneye kopyalama yapılan varolan kısmı
ortadan kaldırır. Adres ve göstergeç kullanıldığı zaman olan adres anlamını
değiştirmek başkadır. Bundan dolayı bir nesneye yukarı döküm sık sık
yapılmaz; aslında genellikle dikkat etmek ve engellemek için bazı şeyler
yapılır. Bu örnekte belirtmeliyiz ki; eğer description( ) işlevi ana sınıfta
saf sanal işlev yapılsaydı (anlamsız olurdu, aslında ana sınıfta herhangi bir
eylem gerçekleştiremezdi), daha sonra derleyici nesne doğramayı önlerdi,
zira ana sınıf nesnesi "oluşturulmasına " izin vermezdi. (değerle yukarı
döküm yapılırken olan şey budur). Saf sanal işlevlerin en önemli yanı
budur; birisi denemeye çalışırsa, nesne doğranmasını derleme zamanı hata
iletisi üreterek engeller.
Bindirim ve Baskınlık (overloading ve overriding)
14. bölümde görülen, ana sınıfta yeralan bindirilmiş işlevin yeniden tanımı
o işlevin öbür bütün ana sınıf sürümlerini gizler. sanal (virtual) işlevler işe
karıştığı zaman, davranış biraz değişir. 14. bölümde yeralan
HidingName.cpp örneğinin değiştirilmiş sürümünü aşağıda inceleyelim;
//:B15:NameHiding2.cpp
//sanal işlevler bindirimi sınırlandırırlar
#include <iostream>
#include <string>
using namespace std ;
class Base{
public:
virtual int f( ) const{
cout<<"Base::f( )\n" ;
return 1 ;
}
virtual void f(string) const{ }
virtual void g( ) const {}
};
class Derived1 : public Base{
public:
void g( ) const{}
};
class Derived2 : public Base{
public:
//sanal işlev baskınlığı
int f( ) const{
cout<<"Derived2::f( )\n" ;
return 2 ;
}
};
class Derived3 : public Base{
public:
//geri dönüş tipi değişmez
//!void f( ) const{cout<<"Derived3::f( )\n" ;}
};
class Derived4 : public Base{
public:
//değişken liste değişimi
int f(int) const{
cout<<"Derived4::f( )\n" ;
return 4 ;
}
};
int main( ){
string s("hello") ;
Derived1 d1 ;
int x=d1.f( ) ;
d1.f(s) ;
Derived2.d2 ;
x=d2.f( ) ;
//! d2.f(s) ; //string sürümü saklı
Derived4 d4 ;
x=d4.f(1) ;
//! x=d4.f( ) ; //f( ) sürümü gizli
//! d4.f(s) ;
//string sürümü gizli
Base& br=d4 ; //yukarıdaki tipe dönüştürme
//! br.f(1) ;
//türetilmiş sürüm sağlanamaz
br.f( ) ;
//türetilmiş sürüm bulunur
br.f(s) ;
//türetilmiş sürüm bulunur
}///:~
Derived3 te farkedilecek ilk özellik baskın işlevin geri dönüş tipinin
değişmesine derleyicinin izin vermemesidir. (buna f( ) sanal değilse izin
verilir). Bu önemli bir kısıtlamadır zira derleyici, işlevin mutlaka ana sınıf
aracılığı ile çokbiçimli olarak çağrılmasını güvenceye alır. Ve eğer ana
sınıf, f( ) işlevinin geri dönüşünü int olarak beklerse, daha sonra f( ) in
türetilmiş sınıf sürümleri mutlaka anlaşmalara uygun olmalı ya da aksi
durumda birçok şey telef olur.
14. bölümdeki kural hala geçerlidir; eğer bindirilmiş ana sınıf üye
işlevlerinden biri baskın yapılırsa, öbür bindirilmiş sürümler türetilmiş
sınıfta gizlenir. Derived4 ü sınayan main( ) işlev kodları, f( ) işlevinin
yeni sürümü aslında varolan sanal işlev arayüzüne baskın olamasa bile,
bunu gösterirler, f( ) işlevinin her iki ana sınıf sürümü f(int) tarafından
gizlenir. Bununla birlikte d4, Base e yukarı döküm yapılırsa, o zaman
sadece ana sınıf sürümleri temin edilebilir (nedeni ana sınıf anlaşması
denilebilir) ve türetilmiş sürümü sağlanamaz. (çünkü ana sınıfta
belirtilmemiş) . Burada hemen bindirilmiş işlev ile baskın işlev arasındaki
temel farkı açıklayalım; bindirilmiş işlev aynı ada sahip fakat işlev
gövdeleri farklı olan işlevlerdir. Baskın işlev ise kalıtım yapısı içinde yer
alan, ana sınıfta sanal olarak bildirilmiş, aynı ada ve aynı işlev gövdesine
sahip işlevdir. Burada yine vurgulayalım baskın işlevler ile bindirilmiş
işlevler tamamen farklıdır.
Geri dönüş tip çeşitleri
Yukarıdaki Derived3 sınıfı, baskınlık işlemi esnasında, sanal işlev geri
dönüş tipinin değiştirilemeyeceğini belirtmektedir. Bu genel olarak
doğrudur, ama bazı özel durumlarda geri dönüş tipini biraz
değiştirebilirsiniz. Eğer ana sınıfa bir göstergeç veya dayanç geri
döndürürseniz, daha sonra işlevin baskın sürümü ana sınıftan geri
dönenden türetilmiş sınıfa bir göstergeç veya dayanç geri döndürebilir.
//:B15:VariantReturn.cpp
//Baskınlık sırasında, türetilmiş tipe göstergeç veya dayanç
//geri döndürmek
#include <iostream>
#include <string>
using namespace std ;
class PetFood{
public:
virtual string foodType( ) const=0 ;
};
class Pet{
public:
virtual string type( ) const=0 ;
virtual PetFood* eats( )=0 ;
};
class Bird : public Pet{
public:
string type( ) const{return "Bird" ;}
class BirdFood : public PetFood{
public:
string foodType( ) const{
return "Bird food" ;
}
};
//ana sınıfa yukarı döküm yap
PetFood* eats( ) {return &bf ;}
private:
BirdFood bf ;
};
class Cat : public Pet{
public:
string type( ) const{return "Cat" ;}
class Catfood : public PetFood{
public:
string foodType( ) const{return "Birds" ;}
};
//tam tipin geri döndür yerine
CatFood* eats( ){return &cf ;}
private:
CatFood cf ;
};
int main( ){
Bird b ;
Cat c ;
Pet* p[]={&b, &c, };
for(int i=0;i<sizeof p/sizeof *p;i++)
cout<<p[i]->type( )<<"eats"
<<p[i]->eats( )->foodType( )<<endl ;
//tam tip geri döndürülebilir
Cat::CatFood* cf=c.eats( ) ;
Bird::BirdFood* bf ;
//tam tip geri dönemez
//!bf=b.eats( ) ;
//mutlaka aşağı döküm yapılmalıdır
bf=dynamic_cast<Bird::BirdFood*>(b.eats( )) ;
}///:~
Pet::eats( ) üye işlevi PetFood'a bir göstergeç geri döndürür. Bird'te ise,
bu üye işlev tam ana sınftaki gibi geri dönüş tipini de kapsayarak bindirime
uğrar. Yani Bird::eats( ) üye işlevi BirdFood'u PetFood'a yukarı döküm
yapar.
Fakat Cat'te is, eats( ) in geri dönüş tipi PetFood'tan türetilmiş bir tip olan
CatFood'a bir göstergeçtir. Gerçek; ana sınıf işlevinin geri dönüş tipinden
türetilmiş olan geri dönüş tipi bunun derlenmesini sağlayan tek unsurdur.
Bu sayede yapılan anlaşma yerine getirilmiş olur; eats( ) her zaman bir
PetFood göstergeci geri döndürür.
Çokbiçimlilik açısından düşünülecek olursa, gerekli gibi görünmez. Niçin
hemen bütün geri dönüş tipleri PetFood* a yukarı döküm yapılmıyor?.
Bird::eats( ) teki gibi?. Bu tipik olarak iyi bir çözüm, ama main( )
işlevinin sonunda fark görülür; Cat::eats( ) PetFood'un tam tipini geri
döndürebilir, Bird::eats( ) in geri dönüş değerini mutlaka tam tipine aşağı
döküm yapıldığı yerdeki gibi yani.
Tam tipi geri döndürebilmiş olmak biraz daha geneldir, ve otomatik olarak
yukarı döküm yapmak özel tip bilgisini kaybettirmez. Bununla birlikte, ana
tipi geri döndürmek sorunları genel olarak çözer ve böylece bu özelleşmiş
bir nitelik olur.
Sanal ve yapıcı işlevler
Sanal işlevler içeren bir nesne oluşturulduğu zaman, VPTR ye mutlaka
VTABLE daki uygun bir yeri işaret eden ilk değer atanmalıdır. Bu işlem
mutlaka sanal işlevin çağrılmasından önce yerine getirilmelidir. Tahmin
edeceğiniz gibi yapıcı işlevin görevi nesne oluşturup ortaya çıkarmaktır,
yapıcı işlevin diğer görevi ise VPTR yi kurgulamaktır. Derleyici VPTR nin
ilk değerlerini üreten kodu yapıcı işlevin başlangıç kısmına yerleştirir. Ve
14. bölümde anlatıldığı gibi bir sınıf için eğer yapıcı işlev açık biçimde
oluşturulmazsa, derleyici onu bizim için oluşturur. Eğer sınıfta sanal
işlevler varsa, oluşan yapıcı işlev uygun VPTR ilk değer atama kodunu da
içerir. Bunun çok sayıda anlamı bulunmaktadır. İlk anlamı verimlilikle
ilgili olandır. inline (satıriçi) işlevlerin kullanılma nedeni küçük işlevler
için çağırma masraflarını azaltmaktır. C++ dili inline işlevlere izin
vermeseydi, önişlemleyici bu "macro" ları oluşturmak için kullanılabilirdi.
Bununla birlikte önişlemleyicinin erişim veya sınıf ile ilgili herhangi
bağlantısı bulunmamaktadır. Bundan dolayı üye işlev "macro"ları
oluşturmak için kullanılamaz. Ayrıca buna ilaveten derleyici tarafından
yerleştirilmiş, gizli kod bulundurmak zorundaki yapıcı işlevlerle
önişlemleyici macro hiç çalışmaz.
Burada anlaşılması gereken; derleyici verimlilik boşluklarını yakalamak
için, yapıcı işleve gizli kodlar yerleştirmektedir. Sadece VPTR ilk
değerlerini atamak zorunluluğu değil, ayrıca this değerini de denetleme
zorunluluğu da bulunmaktadır, (bu durumda operatornew sıfır değerini
geri döndürür) ve ana sınıf yapıcı işlevlerini çağırmalıdır. Hep birlikte ele
alınacak olursa düşünülen koda karşılık gelen küçük bir inline (satıriçi)
işlevdi. Özellikle yapıcı işlevin boyutu, işlev çağrısında azalan masraftan
doğan kazançları yokedebilir. Çok sayıda satıriçi yapıcı işlev çağrıları
yapılırsa, kod boyutu hızda herhangi bir kazanç olmadan artabilir.
Çok doğal olarak küçük çaplı yapıcı işlevleri satıriçi olmayan şekilde
yazmak pek istenmez, zira onları satıriçi olarak yazmak çok daha kolaydır.
Ama kodları ayarlarken satıriçi yapıcı işlevlerin kaldırılmasını düşünün.
Yapıcı işlev çağrılarının sıralaması
Yapıcı ve sanal işlevlerle ilgili ikinci ilginç konuda, yapıcı işlev
çağrılarının sıralamasıdır ve sanal çağrıların yapılma yolu ise yapıcı işlev
içinden olur.
Bütün ana sınıf yapıcı işlevleri her zaman, bir türetilmiş sınıf yapıcı
işlevinde çağrılır. Bunu anlamlı kılan, yapıcı işlevin özel bir görevi
olmasıdır; nesnenin uygun şekilde inşa edildiğini görmek. Türetilmiş sınıf
sadece kendi üyelerine erişmektedir; ana sınfın üyelerine değil. Sadece ana
sınıf yapıcı işlevi kendi üyelerine uygun şekilde ilk değer atamalarını
yapabilmektedir. Bundan dolayı esas olan bütün yapıcı işlevlerin
çağrılmasıdır, aksi durumda bütün nesne doğru şekilde inşa edilmez. İşte
bu yüzden derleyici türetilmiş bir sınıfın bütün bölümleri için yapıcı işlev
çağrısını zorlar. Eğer yapıcı işlev ilk değer atama listesindeki ana sınıf
yapıcı işlevi çağrılmazsa, derleyici varsayılan yapıcı işlevi çağırır. Eğer
varsayılan yapıcı işlev bulunmazsa, derleyici şikayet eder.
Yapıcı işlev çağrılarının sıralaması önemlidir. Türetim yapıldığı zaman,
ana sınıf hakkında herşey bilinir ve ana sınfın public ve protected bütün
üyelerine erişebilir. Bunun anlamı şu demektir; türetilmiş sınıfta bütün ana
sınıf üyeleri geçerlidir. Normal üye işlevde önce inşaat işlemi olur, böylece
nesnenin bütün kısımlarının üyelerinin tamamı inşa edilir. Yapıcı işlevde,
bunlarla beraber, kullanılan bütün üyelerin inşa edilmiş olduğunu kabul
etmeniz gerekir. Bunu güvenceye almanın tek yolu; ilk olarak ana sınıf
yapıcı işlevini çağırmaktır. Daha sonra türetilmiş sınıf yapıcı işlevinde ise,
ana sınıfta erişilen bütün üyelerin ilk değer ataması yapılır. Ayrıca yapıcı
işlevde "bilinen bütün üyeler geçerlidir" den dolayı, olası başka bir
zamanda, yapıcı işlev ilk değer atama listesinde yer alan bütün üye
nesnelerin (yani, harç ile oluşturulmuş sınıflarda bulunan nesneler) ilk
değerlerini atamanız lazımdır. Bu uygulamayı izlerseniz, bütün ana sınıf
üyeleri ve o an ki nesnenin üye nesnelerinin ilk değerlerinin atanmakta
olduğunu kabul edebilirsiniz.
Yapıcı işlevlerde bulunan sanal işlevlerin davranışı
Yapıcı işlev çağrılarının hiyerarşisi, ilginç bir ikilem oluşturur. Yapıcı
işlevde iken ve sanal işlev çağrılırsa ne olur?. Normal bir üye işlev içinde
iken olabilecekleri tahmin edebilirsiniz- sanal çağrı programın çalışması
sırasında saptanır, zira nesne üye işlevin ait olduğu, sınıfı veya ondan
türetilen sınıfı bilemez. Tutarlılık adına bunu, yapıcı işlevlerin içinde
olması lazım gelenler olarak düşünebilirsiniz. Bu dert değil. Yapıcı işlevde
sanal işlev çağrılırsa, işlevin sadece yörel sürümü kullanılır. Yani sanal
mekanizma yapıcı işlevlerde çalışmaz.
Bu davranış iki nedenle anlamlıdır. Temel fikir olarak; yapıcı işlevin
görevi nesneyi varlık ortamına çıkarmaktır. Herhangi bir yapıcı işlevin
içinde, nesne sadece kısmi olarak oluşturulur, sadece ana sınıf nesnelerinin
ilk değerlerinin atanmakta olduğunu bilebilirsiniz, ama hangi sınıfların
sizden türetildiğini bilemezsiniz. Sanal bir işlev çağrısı, bununla beraber,
kalıtım hiyerarşisinin ilerisine veya dışarısına doğru uzanır. Türetilmiş
sınıfta bulunan işlevi çağırır. Bunu yapıcı işlevde gerçekleştirebilseydiniz,
henüz ilk değerleri atanmamış olan üyelerle işlem yapan işlev çağırıyor
olacaktınız. Felaket için kesin tarife denebilir.
İkinci neden tamamen mekanik. Bir yapıcı işlev çağrıldığı zaman, yapılan
ilk şeylerden biri onun VPTR ilk değer atamalarını yapmaktır. Bununla
birlikte sadece bilebilirki o an ki tiptir- yapıcı işlevin yazıldığı tip. Yapıcı
işlev kodu, nesnenin başka bir ana sınıftan olup olmadığını, tamamen
gözardı eder. Derleyici bu yapıcı işlev için kod ürettiği zaman, o yapıcı
işlevin sınıfı için kod üretmiş olur, bir ana sınıf değil ve ondan türetilmiş
bir sınıf değil.(zira bir sınıf onu kimin türettiğini bilemez.). Böylece VPTR
yi kullanan o sınıfın VTABLE olmak zorundadır. Eğer bu yapıcı işlevin
son çağrısı ise, nesnenin geri kalan ömründe, VPTR VTABLE e ilk değer
atama olarak kalır. Eğer daha ileri türetilmiş yapıcı işlev ilerde çağrılırsa,
yapıcı işlev VPTR yi onun VTABLE na yerleştirir, ve böyle sürer gider,
son yapıcı işlev bitene kadar. VPTR nin durumu, en son çağrılan yapıcı
işlev tarafından belirlenir. Bu da yapıcı işlevin, ana sınıftan en altta yer
alan türetilmiş olana, sıralamada çağrılmasının ikinci sebebidir.
Ama bütün bu yapıcı işlev çağrımlarının sıralanması esnasında, her yapıcı
işlev kendi VPTR sini kendi VTABLE ına yerleştirir. Eğer işlev
çağrılarında sanal mekanizma kullanılırsa, kendi VTABLE aracılığı ile
sadece bir çağrı üretir, en sonda yeralan türetilen VTABLE değil.(bütün
yapıcı işlevler çağrıldıktan sonra olduğu gibi). İlave olarak söylemeliyiz
ki ;derleyicilerin çoğunluğu sanal işlev çağrısını yapıcı işlev içinde yapar,
ve erken bağlama uygular zira onlar, geç bağlamanın yörel işleve
uygulandığını bilirler. Beklenen sonucun alınamadığı herhangi bir
durumda, sanal işlev çağrısını yapıcı işlevde gerçekleştirmelisiniz.
Yıkıcı işlevler ve sanal yıkıcı işlevler
virtual anahtar kelimesini yapıcı işlevlerle birlikte kullanamazsınız, ama
yıkıcı işlevler virtual olabilir ve çoğunlukla da olmak zorundadır.
Yapıcı işlevin, parça parça biraraya getirerek nesneyi oluşturma gibi özel
bir görevi vardır, önce ana sınıfın yapıcı işlevi çağrılır, daha sonra kalıtım
sıralamasına göre türetilmiş sınıfların yapıcıları çağrılır.(ayrıca mutlaka
üye nesne yapıcıları da aynı yolla çağrılmalıdır). Aynı şekilde yıkıcı işlevin
de özel bir görevi bulunmaktadır; sınıflar hiyerarşisine ait nesnenin
ortamdan ayrılmasını sağlar. Bunu gerçeklemek için, derleyici yıkıcı
işlevleri çağıran kodları üretir, yalnız bu kez yapıcı işlev çağrılarına göre
ters sıralamada. Yani yıkıcı işlev türetimde en sonda yer alan sınıftan
çağrılara başlar, ve ana sınıfa kadar ilerler. Bu, yapılması istenen güvenli
bir işlemdir, zira o anki yıkıcı işlev, ana sınıf üyelerinin halen diri ve
çalışmakta olduğunu her zaman bilir. Eğer yıkıcı işlev içerisinde ana sınıf
üye işlev çağrısı çağırma gereksinimi duyulursa, böyle bir şeyi
gerçeklemekte güvenlidir. Bu sayede yıkıcı işlev kendi silme işlemini de
gerçekleyebilir, daha sonra bir sonraki yıkıcı işlev çağrılır, kendi silme
işlemini yapar, bu böyle sürer gider.. Her yıkıcı işlev kendi sınıfının hangi
sınıftan türetildiğini bilir, ama kendinden ne türetildiğini bilmez.
Akılda tutulması gereken temel konu; yapıcı ve yıkıcı işlevler, çağrılar
hiyerarşisinin bulunduğu tek yerdir. (ve uygun hiyerarşi derleyici
tarafından otomatik olarak üretilir). Öteki bütün işlevlerde ise sanal olup
olmadığına bakılmaksızın sadece o işlev çağrılır. (ana sınıf sürümleri
değil). Normal işlevlerde çağrılan aynı işlevin ana sınıf sürümleri için
(sanal veya değil) tek yol ise; o işlevin açık biçimde çağrılmasıdır.
Normalde yıkıcı işlevin davranışı oldukça yeterlidir. Ama nesne ana
sınıfına göstergeç ile işlemlenirse ne olur?. (yani nesneyi ana arayüzü ile
işlemlemek). Bu etkinlik nesne yönelimli programlamanın temel gayesidir.
Küme üzerinde new ile oluşturulmuş bir nesneyi bu tip göstergeçle delete
ederken sorun oluşturur. Göstergeç ana sınıfa ise, derleyici delete
esnasında yıkıcı işlevin ana sınıf sürümünü çağıracağını sadece bilebilir.
Nasıl tanıdık geliyormu?. Sanal işlevlerin oluşturulma nedeni olan sorunla
aynısı yani. Allahtan sanal işlevler, yapıcı işlevler hariç, diğer bütün
işlevlerde olduğu gibi yıkıcı işlevler için de çalışabilir.
//:VirtualDestructors.cpp
//sanal olmayan yıkıcı işleve göre, sanal yıkıcı işlevin davranışları
#include <iostream>
using namespace std ;
class Base1{
public:
~Base1( ){cout<<"~Base1( )\n" ;}
};
class Derived1 : public Base1{
public:
~Derived1( ){cout<<"Derived1( )\n" ;}
};
class Base2{
public:
virtual ~Base2( ){cout<<"~Base2( )\n" ;}
};
class Derived2 : public Base2{
public:
~Derived2( ){cout<<"~Derived2( )\n" ;}
};
int main( ){
Base1* bp=new Derived1 ; //yukarı döküm
delete bp ;
Base2* b2p=new Derived2 ; //yukarı döküm
delete b2p ;
}///:~
Program çalıştırıldığı zaman delete bp deyimi sadece ana sınıf yıkıcı
işlevini çağırır, delete b2p ise ana sınıf yıkıcı işlevinin takip ettiği
türetilmiş sınıf yıkıcı işlevini çağırır.Yani delete b2p bizim isteğimizi
yerine getirir. Yıkıcı işlevin sanal yapılması unutulursa, göründüğünden
tehlikeli hatalara yolaçar, zira çoğunlukla program davranışını doğrudan
etkilemez, ama bellek kaçağına neden olması kaçınılmazdır. Ayrıca bir
gerçekte; silme işlemi esnasında sorun daha fazla örtülebilir.
Yıkıcı işlev de olsa,o da yapıcı işlev gibi, istisnai bir işlevdir. Yıkıcı işlev
için sanal olmak mümkündür, zira nesne tipin ne olduğunu önceden bilir.
(yapıcı işleminde olmaz). Nesne bir kez oluşturulduğunda, onun VPTR
sinin ilk değerleri atanır, bu sayede sanal işlev çağrıları olur.
Saf sanal işlevler
Standart C++ dilinde, saf sanal yıkıcı işlevlerin kullanılma izni
bulunurken, yine de kullanma sırasında bazı ek kısıtlar bulunmaktadır; saf
sanal yıkıcı için işlev gövdesi mutlaka sağlanmış olmalıdır. Bu kısıtlama
sanki anlatıma ters gibi durmaktadır; eğer bir işlev, işlev gövdesine
gereksinim duyuyorsa nasıl "saf" sanal işlev olacak?. Ama eğer
hatırlarsanız yapıcı ve yıkıcı işlevleri, özel işlemler daha anlamlı
kılmaktadır, özellikle de eğer sınıf hiyerarşisinde bulunan bütün yıkıcı
işlevlerin her zaman çağrıldığı hatırlanırsa. Saf sanal yıkıcı işlev tanımını
terkedebilseydiniz, silme sırasında hangi işlev gövdesi çağrılacaktı?. Bu
nedenle derleyici ve ilişimci için saf sanal yıkıcı işleve, işlev gövdesi
varlığı mutlaka gereklidir.
Eğer saf ise, ama işlev gövdesi bulunması zorundaysa, peki onun değeri
nedir?. Göreceğiniz üzere, saf sanal yıkıcı işlevler ile saf olmayan sanal
yıkıcı işlevler arasındaki tek fark; saf sanal yıkıcı işlev ana sınıfın soyut
olmasına neden olur, bu yüzden bu ana sınıftan nesne oluşturulamaz.
(ayrıca ana sınıfın herhangi bir üye işlevi de saf sanal olsaydı, bu ana
sınıftan da yine nesne oluşturulamazdı).
Bununla birlikte, içinde saf sanal yıkıcı işlev bulunduran ana sınıftan bir
sınıf türetildiği zaman, bazı şeyler biraz kafa karıştırır. Öbür saf sanal
işlevlere benzemeyen bir biçimde, türetilmiş sınıfta sizden saf sanal yıkıcı
işlev için tanım ,yani işlev gövdesi, istenmez. Bu gerçek, aşağıdaki
derleme ve ilişimle ispatlanabilir;
//:B15:UnAbstract.cpp
//saf sanal yıkıcı işlevler
//garip davranışlar gösterir.
class AbstractBase{
public:
virtual ~AbstractBase( )=0 ;
};
AbstarctBase::~AbstractBase( ){}
class Derived : public AbstractBase{} ;
//Yıkıcı işlevin baskınlığı gerekmez
int main( ){
Derived d ;
}///:~
Normal de, ana sınıfta yer alan saf sanal işlev (bütün öteki saf sanal
işlevlere de) tanım verilmezse türetilmiş sınıfın soyut olmasına neden olur.
Ama burada, duruma uymuyor. Bununla birlikte, derleyicinin her sınıf için
eğer yoksa, bir yıkıcı işlev tanımını otomatik olarak oluşturduğunu
hatılayın. Yani burada olan bu.-Ana sınıf yıkıcı işlevi mutlak olarak baskın
yapılıyor, ve bundan dolayı tanım derleyici tarafından sağlanıyor, ve
Derived aslında soyut olmuyor.
Bunlar bizi ilginç bir soru ile karşı karşıya bırakır; saf sanal işlev görüşü
nedir?. Normal saf sanal işleve benzemeyen bir biçimde, işlev gövdesi
eklemek mecburiyeti var. Türetilmiş sınıfta tanım verilmek zorunluluğu
yoktur, zira derleyici tarafından sağlanır. O zaman düzenli sanal yıkıcı
işlev ile, saf sanal yıkıcı işlev arasında ne fark bulunmaktadır?. Tek fark,
sınıfta yıkıcı işlev tek saf sanal işlev olduğu zaman ortaya çıkar. Bu
durumda yıkıcı işlev saflığının tek etkisi ana sınıf anını engellemektir.
Başka saf sanal işlevler bulınsaydı, ana sınıfı onlarda engellerdi, eğer
başkaları yoksa, daha sonra saf sanal yıkıcı işlev bunu yapar. Böylece
sanal yıkıcı işlev eklenmesi esas olduğunda, saf olup olmaması o kadar
önemli değildir.
Aşağıdaki örneği çalıştırdığınız zaman, saf sanal işlev gövdesi türetilmiş
sürümden sonra çağrılır, başka bir yıkıcı işlev ile olduğu gibi.
//:B15:PureVirtualDestructors.cpp
//saf sanal yıkıcı işlevler
//işlev gövdesi bulundurmalıdır
#include <iostream>
using namespace std ;
class Pet{
public:
virtual ~Pet( )=0 ;
};
Pet::~Pet( ){
cout<<"~Pet( )"<<endl ;
}
class Dog : public Pet{
public:
~Dog( ){
cout<<"~Dog( )"<<endl;
}
};
int main( ){
Pet* p=new Dog ; //yukarı döküm
delete p ;
//sanal yıkıcı işlev çağrısı
}///:~
Aklınızda bulunsun; sınıfta sanal işlev bulunduğu zaman, derhal sanal
yıkıcı işlev ekleyin (hiç işe yaramasa dahi). Bu kural ilerde başınıza
gelebilecek sürprizlere karşılık sizi güvenceye alır.
Yıkıcı işlevlerde bulunan sanal işlevler
Silme işlemi esnasında pek beklenmeyen bazı olaylar ortaya çıkar. Eğer
normal bir üye işlevin içinden sanal işlev çağrılırsa, o işlev geç bağlama
mekanizması kullanılarak çağrılır. Bu işlem, yıkıcı işlevler için geçerli
değildir, ister sanal olsun ister olmasın. Yıkıcı işlev içinde, sadece üye
işlevin "yörel" sürümü çağrılır; sanal mekanizma dikkate alınmaz.
//:B15:VirtualsInDestructors.cpp
//yıkıcı işlev içinde sanal çağrılar
#include <iostream>
using namespace std;
class Base{
public:
virtual ~Base( ){
cout<<"Base1( )\n" ;
f( ) ;
}
virtual void f( ){cout<<"Base::f( )\n" ;}
};
class Derived : public Base{
public:
~Derived( ){cout<<"~Derived( )\n" ;}
void f( ){cout<<"Derived::f( )\n" ;}
};
int main( ){
Base* bp=new Derived ; //yukarı döküm
delete bp ;
}///:~
Yıkıcı işlev çağrısı esnasında, f( ) işlevi sanal olsa bile, Derived::f( ) işlevi
çağrılmaz. Neden böyle olur?. Yıkıcı işlevin içinde sanal mekanizmanın
kullanılmış olduğunu düşünün. Daha sonra sanal çağrı için, o anki yıkıcı
işlevden öteye, kalıtım hiyerarşisinde daha ileride yer alan işleve dönmek
mümkün olurdu. Ama yıkıcı işlevler "dışarıdan içeri" çağrılırlar. (yani en
son türetilmiş olandan ana sınıfa doğru), bundan dolayı çağrılan asıl işlev
önceden silinmiş olan nesne parçalarına bel bağlamış olurdu. Bunun yerine
derleyici derleme sırasında çağrıları çözer ve işlevin sadece "yörel"
sürümünü çağırır. Farkettiyseniz aynı şey yapıcı işlev için de doğrudur
(daha önce anlatıldı), ama yapıcı işlev durumunda tip bilgisi bulunmaz,
yıkıcı işlevde ise bilgi (yani VPTR) oradadır, fakat bu bilgi güvenli
değildir.
Nesne tabanlı hiyerarşi oluşturma
Bu kitap boyunca kap (container) sınıflarını tanıtmak amacı ile yinelenen
Stash ve Stack konu olarak "sahiplik sorunu" nun unsurudur. Burada
"sahip", dinamik olarak oluşturulmuş (new kullanarak) nesnelerin delete
edilmesinden sorumlu olan, "ne" veya "kim" dir. Kap sınıflarını
kullanırken çıkan sorun farklı tipteki nesneleri tutmak ihtiyacından doğan
esneklikten kaynaklanır. Bunu gerçekleştirmek için kaplar void
göstergeçler tutarlar ve bundan dolayı tuttukları nesne tipini bilmezler. Bir
void göstergeçi delete etmek için yıkıcı işlev çağrılması gerekmez, bundan
dolayı kap, nesnelerinin silinmesinden sorumlu olamazdı.
B14:InheritStack.cpp örneğinde bir çözüm gösterilmişti, orada Stack'le
sadece string göstergeçler üreten ve kabul eden yeni bir sınıf türetilmişti.
Madem ki sadece, göstergeçler string nesnelerini tutuyorlardı, onları
uygun şekilde de silebilirlerde (delete edebilirler). Bu çözüm gayet
güzeldi. Fakat burada gereken başka bir şey vardı; her tipi tutmak için yeni
bir kap sınıfı türetilmesi gerekiyordu. (Şimdilik bu işlem çok zahmetli
olmasına rağmen, bir sonraki bölümde yer alan şablonlar (templates)
anlatıldıktan sonra gayet iyi çalışacaktır). Buradaki sorun kapta birden
fazla tipin tutulmak istenmesinden kaynaklanmaktadır, ama void göstergeç
kullanmak istenmemektedir. İkinci çözüm; aynı ana sınıftan türetilmiş
bütün nesneleri, çokbiçimliliği kullanarak kapta tutmaktır, daha sonra sanal
işlevleri çağırabilirsiniz- özellikle sahiplik (veya aidiyet) sorununu çözmek
için, sanal yıkıcı işlevleri çağırabilirsiniz.
Bu çözüm, "tekli köklenmiş hiyerarşi" veya "nesne tabanlı hiyerarşiye"
kapşılık gelir.(zira hiyerarşinin kök sınıfı genellikle "nesne" olarak
adlandırılır). Tekli köklenmiş hiyerarşi kullanmanın bir çok yararı ortaya
çıkar; aslında öteki her nesne yönelimli dil ama özellikle C++ böyle bir
hiyerarşinin kullanmını mecbur bırakır- bir sınıf oluşturduğunuz zaman
doğrudan otomatik olarak türetimde bulunulur veya ortak ana sınıftan
dolaylı olarak türetilir, ana sınıf ise dilin geliştiricileri tarafından
kurgulanırdı. C++ da bu ortak ana sınıfın zoraki kullanımının çok fazla
harcamaya neden olacağı düşünüldü, bundan ötürü vaz geçildi. Bununla
birlikte, kendi projeleriniz de ortak ana sınıf kullanmayı tercih
edebilirsiniz, bu konular kitabımızın ikinci cildinde ayrıntıları ile
alınacaktır.
Sahiplik (aidiyet) sorununu çözmede, ana sınıf için oldukça basit bir
Object oluşturabiliriz, ana sınıfta sanal yıkıcı işlev bulunduğunu
hatırlatalım. O zaman Stack, Object'ten türetilen sınıfları tutabilir.
//:B15:OStack.h
//tekli köklenmiş hiyerarşi kullanımı
#ifndef OSTACK_H
#define OSTACK_H
class Object{
public:
virtual ~Object( )=0 ;
};
//tanım gerekir:
inline Object::~Object( ){}
class Stack{
struct Link{
Object* data ;
Link* next;
Link(Object* dat, Link* nxt) :
data(dat), next(nxt){}
}* head ;
public:
Stack( ) : head(0){}
~Stack( ){
while(head)
delete pop( ) ;
}
void push(Object* dat){
head=new Link(dat, head) ;
}
Object* peek( ) const{
return head ? head->data : 0 ;
}
Object* pop( ){
if(head==0)return 0 ;
Object* result=head->data ;
Link* oldHead=head ;
head=head->next ;
delete oldHead ;
return result ;
}
};
#endif //OSTACK_H///:~
Unsurları basitleştirmek için herşeyi başlık dosyasında saklanabilir, burada
da gereken saf sanal yıkıcı işlev tanımı ve pop( ) (satıriçi yapmak için çok
büyük olduğu düşünülebilir) başlık dosyasında satıriçi işlev haline
getirilmişlerdir.
Şimdi Link nesneleri void göstergeçlerden ziyade Object göstergeçlerini
tutarlar, Stack ise sadece Object göstergeçlerini kabul eder ve geri
döndürür. Şimdi Stack oldukça fazla esnek hale geldi, zira artık oldukça
değişik farklı tipleri tutar oldu ama ayrıca Stack'te kalan herhangi nesneyi
de siler. Yeni kısıtlama(16. bölümde anlatılacak olan şablonlar-templatessoruna uygulandığı zaman sonunda kalkar) ise; Stack'e yerleştirilecek olan
unsur, mutlaka Object'ten türetilmelidir. Sınıfa sıfırdan başlayarak
kurgulama yaparsanız herşey gayet iyi ilerler, ama Stack'e yerleştirmek
için, elinizde string gibi bir sınıf varsa ne olacak?. Bu durumda yeni sınıf
mutlaka, hem string hem de Object olmak zorundadır. Bunun anlamı; yeni
sınıf her iki sınıftan birden türetilmelidir. Böyle bir işlem çoklu kalıtım
olarak adlandırılır. Çoklu kalıtım ikinci cildimiz de bir bölümün adıdır.
Bütün ayrıntılar orada verilecektir. O bölüm okunduğu zaman konunun
karmaşıklığı kolaylıkla anlaşılacaktır. O nedenle ayrı bir bölüm olarak
ikinci cilde yerleştirildi. Buradaki sorunda herşey çok basit olarak
kurgulandığı için, çoklu kalıtımın dipsiz kuyusuna düşülmez.
//:B15:OStackTest.cpp
//{T} OStackTest.cpp
#include "OStack.h"
#include ".../require.h"
#include <fsteram>
#include <iostream>
#include <string>
using namespace std ;
//çoklu kalıtım kullanarak
//hem string hem de Object istiyoruz
class MyString : public string, public Object{
public:
~MyString( ){
cout<<"deleting string :"<<*this<<endl ;
}
MyString(string s) : string(s){}
};
int main(int argc, char* argv[]){
requireArgs(argc, 1) ; //dosya adı değişken
ifstream in(argv[1]) ;
assure(in, argv[1]) ;
Stack textlines ;
string line ;
//dosyayı okuyun ve Stack'e satırları yükleyin
while(getline(in, line))
textlines.push(new MyString(line)) ;
//bazı satırları Stack'ten çekip alın
MyString* s ;
for(int i=0; i<10; i++){
if((s=(MyString*)textlines.pop( ))==0)break ;
cout<<*s<<endl ;
delete s ;
}
cout<<"geri kalan işleri yıkıcı işleve bırak"<<endl ;
}///:~
Stack için yazılan sınama programının önceki sürümü ile benzer olmasına
rağmen, farkettiğiniz gibi sadece 10 (on) tane öge Stack'ten çekip alınır,
bunun anlamı bazı nesnelerin Stack'te kalabileceğidir. Stack, Object leri
tuttuğunu bildiğinden, yıkıcı işlev, ögeleri gayet uygun bir biçimde siler,
ve bunu siz program çıktısında görürsünüz, MyString nesneleri silinirken
ileti yazdırırlar.
Object'leri tutan kapları oluşturmak gayet makul bir yaklaşımdır -eğer
tekli köklenmiş hiyerarşi varsa(ya Object'ten türetilmiş sınıfın gereksinimi
olarak veya dilin zorunluluğundan). Bu durumda, herşeyin Object olduğu
güvenceye alınmıştır ve bu yüzden kapların kullanımı çok karmaşık
değildir. Her şeye rağmen C++ dilinde bütün sınıflardan aynı davranış
beklenemez, zira eğer bu yaklaşım seçilecek olursa, çoklu kalıtım engeline
takılınır. 16 . bölümde şablonları (templates) kullanarak bu sorunun
basitçe çözülebileceği görülecektir.
İşleç bindirimi
İşleçler de öteki işlevler gibi başına virtual konularak sanal yapılabilir.
Sanal işleçlerin uygulamaları, çoğunlukla kafa karıştırıcı olur. Ama
bununla birlikte, her ikisinin de tipinin bilinmediği iki nesne üzerinde
işlem olabilir. Bu durum genellikle matematik unsurlarda rastlanır
(çoğunlukla işleçlerin bindirim nedeni). Örnek olarak Matrisler, vektörler
ve skaler büyüklüklerle ilgili bir sistemi ele alalım; her üçü de Math
sınıfından türetilmiştir.
//:B15:OperatorPolymorphism.cpp
//bindirilmiş işleçlerle çokbiçimlilik
#include <iostream>
using namespace std ;
class Matrix ;
class Vector ;
class Scalar ;
class Math{
public:
virtual Math& operator* (Math& rv)=0 ;
virtual Math& multiply(Matrix*)=0 ;
virtual Math& multiply(Scalar*)=0 ;
virtual Math& multiply(Vector*)=0 ;
virtual ~Math( ){}
};
class Matrix : public Math{
public:
Math& operator* (Math& rv){
return rv.multiply(this) ; //ikinci gönderim
}
Math& multiply(Matrix*){
cout<<"Matrix * Matirx"<<endl ;
return *this ;
}
Math& multiply(Scalar*){
cout<<"Scalar * Matrix"<<endl ;
return *this ;
}
Math& multiply(Vector*){
cout<<"Vector * Matrix"<<endl ;
return *this ;
}
};
class Scalar : public Math{
public:
Math& operator* (Math& rv){
return rv.multiply(this) ;
//ikinci gönderim
}
Math& multiply(Matrix*){
cout<<"Matrix * Scalar"<<endl ;
return *this ;
}
Math& multiply(Scalar*){
cout<<"Scalar * Scalar"<<endl ;
return *this ;
}
Math& multiply(Vector*){
cout<<"Vector * Scalar"<<endl ;
return *this ;
}
};
class Vector : public Math{
public:
Math& operator* (Math& rv){
return rv.multiply(this) ;
//ikinci gönderim
}
Math& multiply(Matrix*){
cout<<Matrix * Vector"<<endl ;
return *this ;
}
Math& multiply(Scalar*){
cout<<"Scalar * Vector"<<endl ;
return *this ;
}
Math& multiply(Vector*){
cout<<"Vector * Vector"<<endl ;
return *this ;
}
};
int main( ){
Matrix m, Scalar s, Vector v ;
Math* math[]={&m, &v, &s} ;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++){
Math* m1=*math[i] ;
Math* m2=*math[j] ;
m1 * m2 ;
}
}///:~
Basit olsun diye sadece operator* bindirime uğratıldı, amacımız sadece
iki Math nesnesine çarpma işlemini uygulamak, ve arzulanan sonucu elde
etmektir, burada hemen belirtelim; bir matrisi vektörle çarpma ile, bir
vektörü matrisle çarpma çok farklı uygulamalardır. Sorun main( ) de Math
dayançlarına iki yukarı döküm yapılan m1*m2 açıklamasında ki, tipleri
bilinmeyen iki nesnedir. Sanal bir işlev, sedece tek bir gönderme
yapabilme yeteneğindedir, yani bir bilinmeyen nesne tipini belirtebilir. Her
iki tipi belirtebilmek için örnekte gösterildiği gibi, çoklu gönderme
yapılarak tek sanal işlev çağrısının göründüğü yer ikinci sanal çağrıyı
sağlar. İkinci çağrının yapılması ile her iki nesne tipi belirlenmekte ve
uygun eylem planı işleme konmaktadır. İlk bakışta anlatılanlar pek şeffaf
değildir, ama eğer örneği dikkatlice incelerseniz, anlattıklarımız sizin için
anlam kazanabilir. Bu konu ikinci cildimizdeki tasarım düzenleri(Design
Patterns) bölümünde, daha ayrıntılı olarak ele alınacaktır.
Aşağıdöküm (aşağıdaki tipe dönüştürme)
Tahmnin edebileceğiniz gibi, kalıtım hiyerarşisinde yukarıda bulunan tipe
dönüştürme-yukarıdöküm- olduğu gibi, kalıtım hiyerarşisinde aşağıda
bulunanlara da döküm olması lazımdır, yani aşağıdöküm. Yukarıdöküm
oldukça kolay yapılan bir işlemdir, zira kalıtım hiyerarşisinde yukarı
çıkıldıkça sınıflar daha genelleşmektedir- dolayısı ile azalmaktadır. Yani
yukarıdöküm yapıldığında her zaman açık biçimde bir ana sınıftan türetim
yapılır (tipik olarak bu sınıf bir tanedir, tabii çoklu kalıtım durumu hariç).
Ama aşağıdöküm yapıldığı zaman genellikle dönüştürülecek çok sayıda tip
bulunur. Konuyu daha özelleştirelim, Daire Şekil'in bir tipidir
(yukarıdöküm), ama bir Şekil'i aşağı döküm yapacak olursak Kare, üçgen,
Daire vs olabilirdi. Bu nedenle ikilem güvenli şekilde aşağıdöküm yolunu
belirlemektedir. (çok daha önemli konuyu kendinize sorun neden
aşağıdökümü, hemen çokbiçimliliği kullanarak doğru tipi otomatik olarak
belirleme yolunu seçmediniz. Aşağıdökümden sakınma, kitabımızın ikinci
cildinde yeralmaktadır).
C++ dili dynamic_cast denilen özel bir açık döküm (3. bölümde anlatıldı)
biçimi sağlar, buna tip güvenli aşağıdöküm işlemi denilir. Belli bir tipe
dynamic_cast kullanarak aşağıdöküm yapıldığı zaman, geri dönüş değeri
arzulanan tipe göstergeç olur, ancak eğer döküm uygun ve başarılı ise, aksi
taktirde tipin doğru olmadığını gösteren sıfır değeri geri döner. Şimdi
küçük bir örnek verelim;
//:B15:DynamicCast.cpp
#include <iostream>
using namespace std ;
class Pet{public: virtual ~Pet( ){} };
class Dog : class Pet{ } ;
class Cat : class Pet{};
int main( ){
Pet* b=new Cat ; //yukarıdöküm
//Dog* a döküm yap
Dog* d1=dynamic_cast<Dog*>(b) ;
//Cat* e döküm yap
Cat* d2=dynamic_cast<Cat*>(b) ;
cout<<"d1= "<<(long)d1<<endl ;
cout<<"d2= "<<(long)d2<<endl ;
}///:~
dynamic_cast kullanıldığı zaman, mutlaka doğru çokbiçimli hiyerarşi
kurulmalıdır- birinde sanal işlevler olmalı- zira dynamic_cast, VTABLE
da bulunan bilgileri kullanarak doğru tipi saptar. Burada ana sınıf sanal
yıkıcı işlev bulundurur ve bu da yeterli olur. main( ) işlevinde Cat
göstergeçi Pet'e yukarıdöküm yapar, ve daha sonra bir aşağıdöküm hem
Cat hem de Dog göstergeçlerinin her ikisine birden gerçeklenir. Her iki
göstergeç çıkışa yazdırılır, böylece program çalıştırıldığı zaman yanlış
aşağıdöküm sıfır değeri üretir. Tabii, denetiminden sorumlu olunan
aşağıdöküm sonucunun sıfırdan farklı olması güvenlik sağlar. Ayrıca
göstergecin tamamen aynı olduğunu kabul etmemeniz lazım, çünkü bazen
yukarı ve aşağıdökümler sırasında göstergeç ayarlamaları olur.(özellikle
çoklu kalıtımlarda)
dynamic_cast çalışma sırasında biraz ek masrafa yolaçar; çok değil, ama
eğer çok sayıda dynamic_cast yapılırsa, bu bir başarım unsuru olabilir
(program tasarımında ciddi şekilde sorgulanması lazımdır). Bazı
durumlarda aşağıdöküm sırasında özel bilgi gerekir, bu bilgi ilgili tipin
bilinmesinden emin olmak içindir, bu durumda dynamic_cast'in ek
masrafına gerek duyulmaz, o zaman onun yerine static_cast
kullanabilirsiniz. Şimdi nasıl çalıştığını görelim;
//:B15:StaticHierarchyNavigation.cpp
//static_cast ile sınıf hiyerarşileri arasında gezinti
#include <iostream>
#include <typeinfo>
using namespace std ;
class Shape{public: virtual ~Shape( ){};};
class Circle : public Shape{};
class Square : public Shape{};
class Other{};
int main( ){
Circle c ;
Shape* s=&c ; //yukarıdöküm, normal ve doğru
//daha açık ama gereksiz
s=static_cast<Shape*>(&c) ; //döküm kafa karıştırıcı olur
Circle* cp=0 ;
Square* sp=0 ;
//sınıf hiyerarşilerinin statik gezinimi
//ek tip bilgisi gerektirir
if(typeid(s)==typeid(cp)) //C++ RTTI
cp=static_cast<Circle*>(s) ;
if(typeid(s)==typeid(sp))
sp=static_cast<Square*>(s) ;
if(cp!=0)
cout<<"bu bir daire"<<endl ;
if(sp!=0)
cout<<"bu bir kare"<<endl ;
//static_cast SADECE verimlilik unsurudur
//dynamic_cast daha güvenlidir. Bununla birlikte
//Other* op=static_cast<Other*>(s) ;
//hata iletisi gönderir aşağıdaki
Other* op2=(Other*)s ;
//değilken
}///:~
Bu programda tamamı anlatılmamış yeni bir özellik kullanıldı, bu konu
ikinci cildimizde ayrı bir bölüm olarark incelenecektir. Konu başlığı;
çalışma zamanı tip bilgi mekanizması (ÇZTB) (run time type
information=RTTI). ÇZTB yukarıdöküm esnasında kaybolan tip bilgisinin
ortaya çıkmasını sağlar. dynamic_cast aslında ÇZTB nin bir biçimidir.
Burada typeid (başlık dosyasında <typeinfo> olarak bildirilir) anahtar
kelimesi göstergeç tiplerini saptamak için kullanılır. Eğer uygunluk varsa
görmek için, yukarı döküm tipi Shape göstergeci ardışık olarak Circle ve
Square göstergeçleri ile karşılaştırılır. ÇZTB için typeid'den daha fazla
unsur vardır ve ayrıca kendi tip bilgi dizgenizi sanal işlev kullanarak
oluşturmak oldukça kolaydır.
Bir Circle nesnesi oluşturulur ve adresi Shape göstergecine yukarı döküm
yapılır; açıklamanın ikinci sürümü static_cast uygulaması ise yukarı
dökümün daha açık halidir. Bununla birlikte yukarı döküm her zaman daha
güvenlidir ve daha yaygın olarak kullanılır. Ben ise yukarı döküm için açık
dökümü hem daha dağınık hem de gereksiz buluyorum.
Tip belirlemede ÇZTB kullanılır, static_cast ise aşağıdökümü
gerçeklemede kullanılır. Fakat görmüş olduğunuz gibi bu tasarımda süreç
dynamic_cast kullanımı ile aynıdır ve müşteri programcı dökümün
doğruluğunu anlamak için mutlaka bazı sınamalar yapmalıdır. Bir de bir
konu hatırlatalım dynamic_cast kullanmadan önce tasarımınızı mutlaka
çok dikkatli şekilde inceleyin. dynamic_cast kullanımından önce
static_cast kullanımını da tercih edebilirsiniz.
Bir sınıf hiyerarşisinde eğer sanal işlevler bulunmuyorsa (sorgulanması
gereken tasarım) ya da güvenli şekilde aşağıdöküm yapmak için gereken
başka bilgiler yoksa, aşağıdökümü statik olarak gerçeklemek
dynamic_cast kullanımından biraz daha fazla hız sağlar. İlave olarak şunu
belirtmeliyiz; static_cast hiyerarşi dışına dökümü engeller, yani aynı
geleneksel döküm gibi davranır, bu yüzden daha güvenlidir.
Bununla birlikte sınıf hiyerarşilerini statik olarak dolaşmak her zaman
risklidir, eğer özel bir durum yoksa dynamic_cast tercih edilmelidir.
ÖZET
C++ dilinde sanal işlevlerle gerçeklenen çokbiçimlilik "değişik biçimler"
anlamını taşır. Nesne yönelimli programlamada aynı yüz (ana sınıftaki
ortak arayüz) bulunabilir, fakat o yüz için değişik biçimler
kullanabilirsiniz; yani sanal işlevlerin değişik sürümleri gibi.
Bu bölümde öğrendiklerimizden şunu çıkarabiliriz; çokbiçimliliği
anlamak, hatta oluşturmak, veri soyutlama ve kalıtım olmaksızın mümkün
değildir. Çokbiçimlilik özelliği, dilin öteki unsurlarından yalıtılmış olarak
anlaşılamaz.(const veya switch örneklerinde olduğu gibi). Konu,
konserdeki orkestra gibi birbiri ile uyumlu,sınıf ilişkileri büyük resminin
bir parçasıdır. İnsanların çoğu bunu C++ dilinin öbür-nesne yönelimli
olmayan-özellikleri ile karıştırmaktadırlar. Bazı durumlarda nesne
yönelimli olarak belirtilen, bindirim ve varsayılan değişkenler gibi. Eğer
geç bağlama yoksa çokbiçimlilikte bulunmaz, bunu aklınızdan hiçbir
zaman çıkarmayın.
Çokbiçimliliği programlarınızda etkin bir biçimde kullanmak için- ve
böylece nesne yönelimli teknikleri- programlama bakışınızı sadece her
sınıfın üyeleri ve iletileri ile ilgili olmaktan çıkarıp, buna ayrıca sınıflar
arasındaki ortak yanları ve birbirleri ile ilişkileri de katmalısınız. Özel bir
gayret gerektirmesine rağmen buna değer, zira daha hızlı program
geliştirilmesini sağlar, daha iyi kod düzenlemesine yolaçar, genişletilebilir
program oluşturulabilir, ve son olarak daha kolay kod bakımı mümkündür.
Çokbiçimlilik dilin nesne yönelimli özelliklerini tamamlar, ama C++ da iki
temel özellik daha bulunmaktadır; kalıplar yani değişken sınıflar
(templates, bu konu 16. bölümde incelenecektir) ve istisna yönetimi
(exception handling, ikinci ciltte incelenecektir). Bu özellikler de,
programlama gücünü nesne yönelimli özelliklerin herbiri kadar arttırır;
soyut veri tiplemesi, kalıtım, ve çokbiçimlilik.
16. BÖLÜM
Kalıplara (templates) Giriş
Kalıtım ve harç, hedef (object) kodun yeniden kullanımını sağlamaktadır.
C++ dilinin kalıp özelliği ise, kaynak kodun yeniden kullanılmasını sağlar.
C++ kalıpları (templates) genel amaçlı program araçları olmalarına
rağmen, nesne tabanlı kap sınıfı hiyerarşilerini kullanmaktan vazgeçirmek
ister gibi görünmektedirler. (15. bölüm sonunda gösterildi). Standart C++
kapları ve algoritmaları, kalıplar kullanılarak açık biçimde inşa edilebilir.
Programcılar için bunların kullanımı daha kolaydır. Aslında kalıplar
değişken sınıflardır. Siz kalıbın içine değişik malzemeler koyarsınız, bu
malzemelerden kalıba uygun değişik ürün elde edersiniz. Değişkenliği
buradan kaynaklanır.
Bu bölümde sadece kalıpların esasları anlatılmayacak, ayrıca NYP nin
temel bölümlerinden olan kaplara giriş ve ötekiler, tamamen standart C++
kütüphanesindeki kaplar aracılığı ile gerçeklenecektir. Bölüm boyunca çok
sayıda kap(container) örneklerinin kullanıldığı görülecektir- Stack ve
Stash- ve böylece kaplarla, gayet rahat program yazabileceksiniz. Ayrıca
bu bölüme yineleyici (iterator) de eklendi. Kaplar kalıpların kullanımı için
ideal örnekler olmalarına rağmen, 2. ciltte kalıpların çok sayıda değişik
kullanım örneklerini göreceğiz.
Kaplar (containers)
Yığıt oluşturmak istendiğini farzedelim, zaten kitabımız boyunca bunu hep
yapıyoruz. Bu yığıt sınıfı int tipleri tutsun.
//:B16:IntStack.cpp
//Basit integer yığıtı
//{L} fibonacci
#include "fibonacci.h"
#include ".../require.h"
#include <iostream>
using namespace std ;
class IntStack{
enum{ssize=100};
int stack[ssize] ;
int top ;
public:
IntStack( ) : top(0){}
void push(int i){
require(top<ssize, "cok sayida push( ) ");
stack[top++]=i ;
}
int pop( ){
require(top>0, "cok sayida pop( )")
return stack[--top] ;
}
};
int main( ){
IntStack is ;
//Fibonacci sayilarini ekle
for(int i=0; i<20; i++)
is.push(fibonacci(i));
//cekal ve yazdir
for(int k=0; k<20; k++)
cout<<is.pop( )<<endl ;
}///:~
IntStack, yığıtın içine aşağıdan yukarı doğru öge yerleştirildiği ve sonra
ögelerin yukarıdan aşağı geri çekildiği basit bir örnektir. Basit olması için
sabit öge sayısı kullanılmıştır, ama dilenirse küme dışına taşıracak şekilde
değişiklikler yapılarak otomatik olarak genişletilebilir. Kitabımız boyunca
izleyeceğiniz Stack sınıfında bunu görebilirsiniz.
main( ) işlevi int değerleri yığıta ekler, ve daha sonra onları oradan çekip
alır. Örneği daha ilginç yapmak için tavşan üreme sayılarını veren
fibonacci işlevi kullanılarak int sayılar üretilir. Şimdi işlevi bildiren başlık
dosyasını verelim;
//:B16:fibonacci.h
//fibonacci sayı üreteci
int fibonacci(int i) ;///:~
Şimdi de uygulama;
//:B16:fibonacci.cpp {O}
#include "../require.h"
int fibonacci(int i){
const int sz=100 ;
require(n<sz) ;
static int f[sz] ; //ilk değer sıfır
f[0]=f[1]=1 ;
//dolmamış bellek yerlerini araştır
int i ;
for(i=0; i<sz; i++)
if(f[i]==0)break ;
while(i<=n){
f[i]=f[i-1]+f[i-2] ;
i++ ;
}
return f[n] ;
}///:~
Bu oldukça verimli bir uygulamadır, zira aynı sayıyı bir defadan fazla
üretmez. Burada static int dizisi kullanılmakta olup, derleyici ilk değeri
sıfır olarak atar. Birinci for döngüsünde ilk değeri sıfır olan i dizinine
geçilir, daha sonra while döngüsüne geçilerek arzulanan ögeye varılana
kadar fibonacci sayıları diziye eklenir. Ama bir konuya dikkatinizi
çekelim; eğer n ögesi aracılığı ile Fibonacci sayılarının ilk değerleri
atanmışsa, while döngüsü kullanılmadan tamamen atlanır.
Gereken Kaplar (containers)
Açıkça belirtmeliyiz ki int yığıt çok önemli bir araç değildir. Kaplara
gerçek anlamda gereksinim, kümede new ile yeni bir nesne oluşturma, ve
yine o nesneyi silmek için, delete kullanma sırasında ortaya çıkar. Temel
programlama sorununda, yazılmakta olan programda kaç tane nesneye
gereksinim duyulacağı tam olarak bilinemez. Örnek verelim; hava trafik
kontrol sisteminde sistemin yöneteceği uçak sayısı sınırlandırılmak
istenmez. Zira sayı sabitlenirse, o sayıdan fazla sayıda uçakla karşılaşınca
program çalışması otomatik olarak durur, diğer deyişle sistem çöker. Bir
bilgisayar destekli tasarım sisteminde, çok sayıda şekille uğraşılır, burada
da şekil sayısı tamamen kullanıcıya bağlıdır, yani program geliştirici şekil
sayısını programda sabitleyemez. (program demo programı değilse). Bir
kez bu eğilim saptandığında, programlama yaparken çok sayıda benzer
durumla karşılaşabilirsiniz. "Bellek yönetimi" için sanal belleğe güvenen C
programcıları çoğu kez new ve delete fikrini uygun bulmaktalar ve kap
sınıflarını bozmaktalar. C de uygulamalarda, açık biçimde programda
ihtiyaç duyulandan çok daha büyük global bir dizi oluşturulur. Bu, çok
fazla kafa yorma gerektirmez, (veya malloc( ) ve free( ) farkındalığı
gerektirmez) , ama geliştirilen programın kolaylıkla başka mimarilere
aktarılmasını engeller ve bazı hataların gözden kaçmasına yolaçar.
Bütün bunlara ilaveten, C++ da nesnelerin çok büyük global dizisi
oluşturulursa, yapıcı ve yıkıcı işlev maliyetleri programı gözle görülür
şekilde yavaşlatır. O nedenle bu soruna C++ yaklaşımı, yani kapların
kullanımı, çok daha uygundur. Bir nesne gerekirse, new ile nesne
oluşturulur, ve o nesnenin göstergeçi kaba konur, hepsi bu kadar. İlerde
lazım olduğunda onu dışarı alır, istediğiniz işlemi yaparsınız. Bu sayede
sadece gerek duyulan nesneler oluşturulmuş olur. Programın başlangıcında
yer alan bütün ilk değer atama durumları genellikle bulunmaz, new ile
nesne oluşturulmadan önce, ortamda bazı şeyler gerçeklenene kadar
beklenmesine izin verir.
Böylece, en genel anlamı ile oluşturulan kap, bazı ilgilenilen nesne
göstergeçlerini tutar. Bu nesneler new anahtar kelimesi kullanılarak
üretilir, ham sonuç göstergeçleri yerleştirilir (süreçte, potansiyel olarak
yukarı döküm yapılır), daha sonra o nesne ile bir işlem yapılacağı zaman,
oradan çekip alınır. Bu yöntem, en düzenli ve esnek programın
geliştirilmesini sağlar.
Kalıplara giriş
Şimdi ortaya bir sorun çıkıyor, int değerleri tutan IntStack elinizde
bulunmaktadır. Ama siz uçakları, şekilleri, bitkileri veya başka şeyleri
tutmasını istiyorsunuz. Her seferinde kaynak kodun yeniden icat edilmesi,
yeniden kullanılabilirlik konusunda çığırtkanlık yapan bir dil için pek
zekice değildir. Daha iyi bir yol mutlaka olmalıdır.
Bu durumda kaynak kodun yeniden kullanımı ile ilgili üç teknik
bulunmaktadır; C biçimi, terslik için burada gösterildi; SmallTalk
yaklaşımı, C++ dilini büyük çapta etkiledi; C++ yaklaşımı, kalıplar
(bazıları değişken sınıflar demektedir).
C çözümü; tabii, karışık, hata üretici, ve tamamen berbat olduğu için C
çözümünden uzaklaşmaya çalışacaksınız. Bu yaklaşımda bir Stack için
kaynak kodu kopyalarsınız, elle değişiklikleri yaparsınız bundan dolayı
yeni hataları sisteme eklersiniz. Bu çok verimli bir yol değildir.
SmallTalk çözümü; SmallTalk basit ve düz (bu örneği Java da takip eder)
bir yaklaşım sergiler; kodu yeniden kullanmak için, kalıtım kullanılır.
Bunun uygulamasında her kap sınıfı, üretici ana sınıf Object unsurlarını
tutar (15.bölüm sonundaki örnek gibi). Ama bilindiği gibi SmallTalk
dilinde kütüphaneler çok önemli olduğu için, hiçbir zaman bir sınıfın inşası
sıfırdan kurgulanmaz. Bunun yerine varolan sınıflardan yaralanarak
türetilir. İnşa edilecek sınıfa mümkün olduğunca en yakın olan sınıf tespit
edilir, birkaç değişiklik yapılarak ondan türetilir. Açık olan bir şey var, bu
işlem çok kazançlıdır, zira sınıfı oluşturmak fazla çaba gerektirmez. (Ve bu
da bize etkin bir SmallTalk programcısı olmak için, neden kütüphane
öğrenimi için çok fazla zaman harcanması gerektiğini göstermektedir.)
Fakat hemen belirtelim; bütün SmallTalk sınıfları, tekli kalıtım ağacı
olarak sonlanırlar. Yeni bir sınıf oluştururken, mutlaka bu ağacın bir
dalından türetmelisiniz. Ağacın büyük kısmı elde bulunmaktadır
(SmallTalk sınıf kütüphanesi), ve ağacın köküne Object adı verilir, aynı
sınıf her SmallTalk kabını da tutar. SmallTalk(ve Java) sınıf hiyerarşisinde
yeralan her sınıf Object'ten türetilir, bu yüzden her sınıf her kapta
tutulabilir (kabın kendiside dahil). Tek ağaçlı hiyerarşinin bu tipi temel
üretim tipine (çoğu kez Object olarak adlandırılır, Java da olduğu gibi)
dayanır ve "nesne tabanlı hiyerarşi" olarak bilinir. Duyduğunuz bu terim
(nesne tabanlı hiyerarşi) NYP de çokbiçimlilik gibi, yeni bir temel
unsurdur. Kökünde Object (veya başka bir isim) bulunan sınıf hiyerarşisi
buna karşılık gelir ve kap sınıfları Object tutar.
SmallTalk sınıf kütüphanelerinin geçmişi C++ ya göre daha uzun zaman
ve daha fazla deneyim taşıdığından, ve ilk C++ derleyicilerinde kap sınıf
kütüphaneleri bulunmadığı için, SmallTalk kütüphanesini C++ da
kullanmak iyi bir fikir gibi gözükmüştü. Bu eylem, erken C++
uygulamasında denenmişti (OOPS kütüphanesi, Keith Gorlen NIH te
iken). Kod gövdesi uygun olan bu yapı, birçok kişi tarafından kullanılmaya
başlamıştı da. Kap sınıfları kullanılmaya çalışırken, bir sorunla karşılaşıldı.
Sorun SmallTalk'tan (ve bilinen OOP dillerinin çoğundan)
kaynaklanıyordu, bütün sınıflar otomatik olarak tek bir hiyerarşiden
türetiliyordu, ama bu C++ da doğru değildir. Kap sınıflarının bulunduğu
düzgün sınıf hiyerarşileri elde bulunmaktadır, ama daha sonra şekil
sınıfının bir takımı satın alınır veya kullanmadıkları uçak sınıf hiyerarşileri
geliştirici firmadan satın alınır.(birşey için hiyerarşi kullanımı, C
programcılarının kaçındıkları masrafa neden olur). Nesne tabanlı
hiyerarşide ayrı bir sınıf, kap sınıfına nasıl yerleştirilecek?. Sorunun nasıl
göründüğüne bakalım;
Kap
(Object göstergeçleri
tutulur)
Üçgen
Object
Object
Object
Şekil
Daire
Kare
C++ çok sayıda bağımsız hiyerarşiyi desteklediği için, SmallTalk dilinin
nesne tabanlı hiyerarşisi o kadar iyi çalışmaz. Çözüm açık olarak
gözükmektedir. Çok sayıda bağımsız kalıtım hiyerarşisi olabiliyorsa, o
zaman birden fazla sınıftan türetim yapılabilmesi lazım; Çoklu kalıtım bu
sorunu çözer. Böylece aşağıdaki durum gerçeklenir.(benzer bir örnek 15.
bölümün sonunda verilmişti)
Object
Şekil
OŞekil
Daire
ODaire
Kare
OKare
Üçgen
OÜçgen
Şimdi OŞekil, Şekil sınıfının karakteristik davranışlarını gösterir, ama
ayrıca Object'ten türetildiği için Container'a yerleştirilebilir. ODaire ve
OKare vs ye ilave kalıtım gerekir, böylece bu sınıflar OŞekil sınıfına
yukarıdöküm yaparlar. Ve bütün bunların sonucunda doğru davranışlar
sergilenebilir. Farkedebileceğiniz gibi herşey hızla gitgide karmaşıklaşır.
Derleyici üreticileri kendi nesne tabanlı kap (container) sınıf hiyerarşilerini
geliştirip sistemlerine eklediler, çoğu kalıp(template) sürümleri ile
yerdeğiştirmekte. Siz de genel programlama sorunlarının çözümü için
çoklu kalıtım kullanımını onaylayabilirsiniz, ama kitabımızın ikinci
cildinde de göreceğiniz gibi, çoklu kalıtım, özel durumlar haricinde,
sakınılması gereken çok karmaşık bir yapıdır.
Kalıp (template) çözümü
Çoklu kalıtıma sahip nesne tabanlı hiyerarşi, fikir olarak basit olmasına
rağmen, kullanım esnasında bir sürü sorun çıkarır. Bjarne Stroustrup "The
C++ programming language" isimli kitabında, nesne tabanlı hiyerarşiye
tercih edilebilecek alternatifi gösterdi. Kap sınıfları, değişkenleri bulunan
önişlemleyici macroları büyüklüğünde oluşturulmuştu. Ve bunlar
kullanıcının istediği tipin yerine kullanılabiliyordu. Belli bir tipin tutulması
için bir kap oluşturulduğu zaman, bir çift macro çağrısı yapılırdı.
Maalesef bu yaklaşım, varolan SmallTalk yazınında ve programalama
deneyimlerinde tam kavranamadı, karıştırıldı, ve birazda hantaldı. Aslında
hiçkimse kullanmadı.
Bu arada Bell laboratuvarlarında, Stroustrup ve C++ takımı, temel macro
yaklaşımını değiştirmişti, onu daha basitleştirdiler ve önişlemleyiciden alıp
derleyiciye yerleştirdiler. Bu yeni kod yedeklemeye, template
(kalıp=değişken sınıf) adını verdiler. Böylece kodun tekrar kullanımı ile
ilgili yepyeni bir yol geliştirilmiş oldu. Kalıtım ve harçta yapılan, hedef
(object kod=uzantısında .o olan kod denilebilir) kodun yeniden kullanımı
yerine, bu sefer template ile doğrudan kaynak kod yeniden kullanılır.
Artık kap Object denilen temel ana sınıfı değil, onun yerine tam
belirlenmemiş ögeleri tutar. Kalıp (template) kullanıldığı zaman, öge
derleyici tarafından kesinleştirilir, eski macro yaklaşımına çok benzer, ama
bu sefer kullanım daha temiz ve kolaydır.
Şimdi, kalıp sınıfı kullanılmak istendiği zaman, kalıtım ve harçla ilgili
şüpheler duymak yerine, kabın (container) kalıp(template) sürümü alınmalı
ve sorunun özel sürümü elde edilmelidir. Aynı aşağıdaki gibi;
Şekil
Kabı
Şekil
Şekil
Şekil
Derleyici bizim için gerekenleri yapar, bizim için tam olarak ne iş
yapılacaksa o gerçeklenir. Kalıp yaklaşımının ikinci yararı; çaylak
programcılar için kalıtım kullanımı zor olabilir veya kullanmakta sorun
yaşayabilirler, ama kalıp sınıfları ile işlerini kolaylıkla görebilirler. (bizim
vector sınıfı ile kitap boyunca yaptıklarımız gibi)
Kalıp yazım biçimi
Template anahtar kelimesi derleyiciye, takip eden sınıf tanımının
belirlenmemiş tip veya tipler üzerine işlem yapacağını söyler. Asıl sınıf
kodları kalıptan üretildiği zaman, tipler mutlaka açıkça belirtilmelidir ki
derleyici onları asıl yerlerine koyabilsin.
Yazım biçimini göstermek için aşağıda, sınırlı denetlenir dizi örneği
verildi;
//:B16:Array.cpp
#include "../require.h"
#include <iostream>
using namespace std ;
template<class T>
class Array{
enum{size=100};
T A[size];
public:
T& operator[] (int index){
require(index>=0 && index<size,
"Index out of range");
return A[index];
}
};
int main( ){
Array<int> ia;
Array<float> fa;
for(int i=0; i<20; i++){
ia[i]=i*i;
fa[i]=float(i)*1.41 ;
}
for(int j=0; j<20; j++)
cout<<j<<": "<<ia[j]
<<", "<<fa[j]<<endl;
}///:~
Farkettiğiniz gibi aşağıdaki satır hariç herşey normal bir sınıf gibi yazılır;
template<class T>
Yukarıda T yedek öge olarak belirlendi ve tip adını temsil eder. Ayrıca T
nin, kabın belli bir tipi tutmuş olduğu sınıfta heryerde, kullanıldığı
görülecektir.
Array'de ögeler, bindirilmiş operator [] işleviyle yerleştirilir ve çıkarılır.
Bu bir dayanç geri döndürür, ve bu yüzden eşit işaretinin her iki yanında
kullanılabilir.(yani, hem sağdeğer hem de soldeğer gibi ). Farkedeceğiniz
gibi, eğer dizin belli sınırların dışına çıkarsa, iletiyi yazdırmak için require
( ) işlevi kullanılır. operator [] işlevi inline (satıriçi) olduğu için, bu
yaklaşım dizi sınır hasarları olmamasını güvenceye almak için kullanılır,
daha sonra müşteriye gönderilen kodda require( ) işlevi çıkarılır.
main( ) işlevinde, değişik tipte nesneleri tutmak için Array'lerin ne kadar
kolay oluşturulduğu görülebilir. Aşağıdaki gibi yazdığınızda
Array<int> ia ;
Array<float> fa ;
Derleyici Array kalıbını (buna anlaştırma denir) iki defa açar, böylece
Array_int ve Array_float gibi iki yeni sınıf üretilir. (farklı derleyiciler bu
isimleri farklı şekilde düzenleyebilirler). Bu sınıflar, eğer elle yedekleme
yapmış olsaydınız, aynı sizin üretmiş olacaklarınız gibi olacaktı, bizim ia
ve fa olarak tanımladığımız nesneleri derleyicinin bizim için oluşturması
hariç. Ayrıca iki defa yazılmış sınıf tanımları ya derleyici tarafından önüne
geçilir, ya da ilişimci tarafından birleştirilir.
Satıriçi olmayan işlevlerin tanımları
Tabii, satıriçi olmayan üye işlev tanımları için vakit henüz erken. Bu
durumda derleyici, üye işlev tanımından önce template bildirimini görmek
ister. Şimdi yukarıda verilen örneği değiştirip, satıriçi olmayan üye tanımı
ile verelim;
//:B16:Array2.cpp
//satıriçi olmayan kalıp tanımı
#include ".../require.h"
template<class T>
class Array{
enum{size=100};
T A[size] ;
public:
T& operator[] (int index) ;
};
template<class T>
T& Array<T>::operator[] (int index){
require(index>=0 && index<size,
"Index out of range");
return A[index] ;
}
int main( ){
Array<float> fa ;
fa[0]=1.41 ;
}///:~
Kalıp sınıf adının dayancı, Array<T>::operator[] da ki gibi, kalıp
değişken listesini mutlaka yanında bulundurmalıdır. Bunun içeride
olduğunu düşünebilirsiniz. Kalıp değişken listesindeki değişkenlerle
düzenlenmiş sınıf adı, her kalıplama anı için, bir sınıf adı belirteci üretir.
Başlık dosyaları
Satıriçi olmayan işlev tanımları oluşturulsa da, genellikle bir kalıp için
bütün bildirim ve tanımlar bir başlık dosyasına konulmak istenir. Bu
aslında, normal başlık dosyasındaki "bellekte yer kaplayacak herhangi bir
unsuru yerleştirmeme" kurallarına aykırı görünür (çok sayıda yapılan
tanımlardan kaynaklanan hataları engellemek için) , ama unutmayalım
kalıp tanımları çok özeldir. template<...> ile başlayan herhangi bir şey için
derleyici o noktada bellekte yer ayırmaz, ama bir şey belirtilene kadar
bekler (kalıp anlanması tarafından), ve derleyici ile ilişimci de bir yerlerde,
özdeş kalıbın çok sayıdaki tanımlarını ortadan kaldıran bir mekanizma
bulunur. Bundan dolayı hemen hemen her zaman, kalıp bildirim ve
tanımının tamamı, kullanım kolaylığı olması için başlık dosyasına
yerleştirilir.
Özel gereksinimlerde kalıp tanımlarını, ayrı .cpp uzantılı dosyalara
yerleştirme durumu doğabilir. (örnek olarak windows ta .dll uzantılı tek
dosyada kalıp anlıklarının bulunması). Derleyicilerin çoğunda buna izin
veren bazı mekanizmalar vardır; bunun kullanılabilmesi için derleyicinizin
özel belgelerini iyice okumanız gerekir.
Bazı insanlar uygulamanın kaynak kodlarını başlık dosyasına yerleştirince,
birileri kütüphaneyi satın aldığında, kodları değiştirebilecekleri ve
çalabileceklerini düşünürler. Bu bir etken olabilir ama, herhalde esas olan,
soruna nasıl bakıldığına bağlıdır; bir ürün veya hizmet satın alınmıyormu?.
Eğer bir ürünse, o zaman onu koruyabilmek için herşey yapılır, ve büyük
ihtimalle kaynak kodlar verilmez, sadece derlenmiş kodlar verilir. Ama
çok sayıda insan yazılımı, bir hizmet olarak, hatta daha fazlası, bir
abonmanlık sistemi olarak görür. Satın alan, yazılımcıyı bir uzman olarak
görür, yeniden kullanılabilir kod parça bakımının devamını ister, böylece
kendisi bu işten kurtulur ve esas yapması gereken işe odaklanabilir. Kişisel
düşüncem; satın alanların çoğu yazılımcıları değerli bir kaynak olarak
görür ve ilişkilerini tehlikeye atmak istemezler. Az sayıda bir grupta satın
almak yerine çalmak yolunu tercih edebilir, veya kendine özgü çalışma
yapabilir, ama bu kişiler yazılımcı ile iyi ilişkiler kuramazlar.
Kalıp (template) olarak IntStack
İşte IntStack.cpp'den, kaplar (template) kullanılarak kaynak kap sınıfı
olarak uygulanmış, kap ve yineleyici (iterator);
//:B16:StackTemplate.h
//basit yığıt kalıbı
#ifndef STACKTEMPLATE_H
#define STACKTEMPLATE_H
#include ".../require.h"
template<class T>
class StackTemplate{
enum{ssize=1000};
T stack[ssize] ;
int top ;
public:
StackTemplate( ) : top(0){}
void push(const T& i){
require(top<ssize, "cok sayıda push( ) var");
stack[top++]=i ;
}
T pop( ){
require(top>0, "cok sayida pop( ) var") ;
return stack[--top];
int size( ){return top;}
};
#endif //STACKTEMPLATE_H///:~
Farkettiğiniz gibi bir kalıp (template) tutulan nesnelerle ilgili önkabuller
yapmaktadır. Örnek; StackTemplate, push( ) işlevine T için bazı tür
atamaların yapıldığını kabul etmektedir. Şu söylenebilir; kalıp (template)
"tutabilme yeteneğinde olduğu tipler" için "bir arayüz ima eder".
Bunu söylemenin ikinci yolu; kalıplar C++ dili için bir çeşit zayıf tipleme
mekanizması sağlar, ki bu normalde güçlü tip dili C++ için olağandır.
Kabul edilebilir olması için nesnenin tam tipinde israr etmek yerine, zayıf
tiplemede, sadece çağrılmak istenen üye işlevlerin belli bir nesne için
temini gerekir. Böylece zayıflatılmış tipli kod, üye işlev çağrılarını kabul
eden herhangi bir nesneye uygulanabilir, bundan dolayı çok daha esnek
olur.
Şimdi kalıp (template) sınayan yeniden düzenlenmiş bir örnek verelim ;
//:B16:StackTemplateTest.cpp
//basit yığıt kalıbını sına
//{L} Fibonacci
#include "fibonacci.h"
#include "StackTemplate.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
StackTemplate<int> is ;
for(int i=0; i<20; i++)
is.push(fibonacci(i)) ;
for(int k=0; k<20; k++)
cout<<is.pop( )<<endl ;
ifstream in("StackTemplateTest.cpp");
assure(in, "StackTemplateTest.cpp");
string line ;
StackTemplate<string> strings ;
while(getline(in, line))
strings.push(line) ,
while(strings.size( )>0)
cout<<strings.pop( )<<endl ;
}///:~
Tek farkı is'in oluşumu meydana getirir. Kalıp değişken listesinde, yığıt ve
yineleyicinin tutması gereken nesne tipi belirtilmiş olmalıdır. Kalıbın
bağlantılarını göstermek için string tutmak amacı ile ayrıca bir
StackTemplate oluşturulur. Bu, kaynak kodlardan satırlar okunarak
sınanır.
Kalıplardaki sabitler
Kalıp değişkenleri sadece sınıf tipleri ile sınırlı değildirler; yerleşik
tiplerde kullanılabilir. Kalıbın belli anları için, o zaman bu değişkenlerin
değerleri, derleme zamanı sabitleri olur. Hatta bu değişkenler için
öntanımlı değerler kullanılabilir. Aşağıdaki örnek, Array sınıfının
büyüklüğünü anlık durumlar için belirlemeyi sağladığı gibi, ayrıca
öntanımlı değerlerin kullanılmasına da izin verir.
//:B16:Array3.cpp
//kalıp değişkenleri olarak yerleşik tip kullanımı
#include ".../require.h"
#include <iostream>
using namespace std ;
template<class T, int size=100>
class Array{
T array[size] ;
public:
T& operator[] (int index) {
require(index>=0 && index<size,
"index değeri çok büyük");
return array[index] ;
}
int length( ) const {return size;}
};
class Number{
float f;
public:
Number(float ff=0.0f) : f(ff){}
Number& operator=(const Number& n){
f=n.f ;
return *this ;
}
operator float( ) const {return f;}
friend ostream&
operator<<(ostream& os, const Number& x){
return os<<x.f ;
}
};
template<class T, int size=20)
class Holder{
Array<T, size>* np;
public:
Holder( ) : np(0){}
T& operator[](int i){
require(0<=i && i<size) ;
if(!np) np=new Array<T, size> ;
return np->operator[] (i) ;
}
int length( ) const {return size;}
~Holder( ){delete np;}
};
int main( ){
Holder<Number> h;
for(int i=0; i<20; i++)
h[i]=i ,
for(int j=0; j<20; j++)
cout<<h[j]<<endl ;
}///:~
Daha önceki örnekte olduğu gibi, Array, nesnelerin denetlenmiş dizisidir
ve, dizin değişimlerinin kapsama alanı dışına çıkmasını engeller. Holder
sınıfı Array sınıfına oldukça benzer, Array tipinin tekleşik nesnesi yerine
Array'a göstergeç bulunması hariç. Bu göstergecin ilk değer ataması
yapıcı işlevde gerçekleşmez; ilk erişime kadar ilk değer ataması
geciktirilir.Bunun adına "tembel ilk değer ataması" denir. Çok sayıda
nesne oluşturuyorsanız bu tekniği kullanabilirsiniz, ama onların hepsine
erişmeksizin, böylece bellek istifadeli kullanılır.
Gördüğünüz üzere her iki kalıpta size değeri hiçbir zaman sınıfta içsel
olarak saklanmaz, ama sanki üye işlevlerde bulunan veri üyesiymiş gibi
kullanılır.
Kalıp olarak Stash ve Stack
Kitap boyunca Stash ve Stack kap sınıfları "aidiyet" sorunu, ziyaret
edilerek yinelenecektir. Bunun gerçek nedeni; bu kapların tam olarak
hangi tipi tuttuklarını bilmemeleridir. En yakınına gelen Object kabı
Stack, 15. bölümün sonunda bulunan OStackTest.cpp de görüldü.
Eğer müşteri programcı kapta tutulan bütün nesne göstergeçlerini açık
biçimde silmezse, o zaman kap bu göstergeçleri doğru biçimde ortadan
kaldırması lazım gelir. Yani söylemek istediğimiz; kabın sahip olduğu,
silinmemiş nesnelerin ortadan kaldırılmasından kendisinin sorumlu
olduğudur. Silme işleminin gerçeklenmesi için nesne tipinin bilinmesi
gerekir, ama üretken kap sınıfı oluşturmak için ise nesne tipi bilmek
gerekmez, bu durum beklenmedik bir engel olarak ortaya çıkar. Bununla
birlikte, kalıplarla nesne tipi bilinmeyen kodlar yazılabilir, ve kolayca
istenilen her tipli kabın yeni sürümü anlaştırılır. Her anlık kap tuttuğu
nesnelerin tipini bilir, ve böylece doğru yıkıcı işlevi çağırabilir
(çokbiçimliliğin kullanıldığı genel durumda, sanal yıkıcının sağlanması
önkabuldur).
Bütün üye işlevler satıriçi olduğu için, Stack için bu işlem oldukça basittir.
//:B16:TStack.h
//kalıp olarak yığıt
#ifndef TSTACK_H
#define TSTACK_H
template<class T>
class Stack{
struct Link{
T* data ;
Link* next ;
Link(T* dat, Link* nxt) :
data(dat), next(nxt) {}
}* head;
public:
Stack( ) : head(0){}
~Stack( ){
while(head)
delete pop( ) ;
}
void* push(T* dat){
head=new Link(dat, head) ,
}
T* peek( ) const{
return head ? head->data : 0 ;
}
T* pop( ){
if(head==0) return 0 ;
T* result=head->data ;
Link* oldHead=head ;
head=head->next ,
delete oldHead ;
return result ;
}
};
#endif //TSTACK_H///:~
15. bölümün sonunda yeralan OStack.h ile bu örneği karşılaştıracak
olursak, sadece Object'in T ile yerdeğiştirmesi hariç, Stack'in sanal olarak
özdeş olduğu görülecektir. Yine string ve Object'ten katlı türetim
gerekliliği haricinde, Object devreden çıkarılmaktadır (ve hatta Object'in
kendi gerekliliği), sınama programları da oldukça birbirlerine yakındırlar.
Buarad silme işlemini gerçekleştirmek için MyString sınıfı
bulunmamaktadır, bu yüzden Stack kabına nesnelerini silmesi için küçük
yeni bir sınıf eklenir.
//:B16:TStackTest.cpp
//{T} TStackTest.cpp
#include "TStackTest.h"
#include ".../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
class X{
public:
virtual ~X( ){cout<<"~X( )"<<endl ;}
};
int main(int argc, char* argv[]){
requireArgs(argc, 1) ;
//dosya ismi değişken
ifstream in(argv[1]) ;
assure(in, argv[1]) ;
Stack<stirng> textlines ;
string line ;
//dosyayı oku ve satırları yığıta yerleştir
while(getline(in, line))
textlines.push(new string(line)) ;
//yığıttan bazı satırları çek al
string* s ;
for(int i=0; i<10; i++){
if((s=(string*)textlines.pop( ))==0)break ;
cout<<*s<<endl ;
delete s;
} //yıkıcı öbür stringleri siler
//doğru silmeyi göster
Stack<X> xx ;
for(int j=0; j<10; j++)
xx.push(new X) ;
}///:~
X in yıkıcı işlevi sanaldır, burada bunun için gerekli değildir, ama xx daha
sonra X ten türetilmiş nesneleri tutabileceği için yıkıcı işlev sanaldır.
string ve X için değişik Stack oluşturmak çok kolaydır. Kalıptan ötürü her
iki dünyanın en iyi yanları alınır; Stack sınıfının kullanım kolaylığı ile
beraber uygun silme işlemi.
Kalıplaşmış göstergeç Stash
PStash kodlarını bir kalıpta (template) yeniden düzenlemek çok kolay
değildir. Çok sayıda üye işlev satıriçi olmaması lazımdır, neden budur.
Bununla birlikte, kalıp olarak bu işlevlerin tanımları hala başlık dosyasında
bulunur (derleyici ve ilişimci, çoklu tanım sorunlarına dikkat eder). Kod
normal PStash'e oldukça benzer, artış büyüklüğünün anlaşılması hariç.
(inflate( ) in kullandığı, öntanımlı değeri olan sınıf dışı öge olarak, böylece
artış büyüklüğü herhangi anlık noktada değiştirilebilir (bunun anlamı artış
büyüklüğü sabitlenebilir demektir; nesne yaşamı boyunca, artış
büyüklüğünün değiştirilebilir olduğu ayrıca tartışılması lazım:
//:B16:TPStash.h
#ifndef TPSTASH_H
#define TPSTASH_H
template<class T, int incr=10>
class PStash{
int quantity ;
//bellek yeri sayısı
int next ;
//bir sonraki boş yer
T** storage ;
void inflate(int increase=incr) ;
public:
PStash( ) : quantity(0), next(0), storage(0) {}
~PStash( ) ;
int add(T* element) ;
T* operator[](int index) const ; //getirme işlemi
//Bu PStash ten dayancı kaldırın
T* remove(int index) ;
//Stash te öge sayısı
int count( ) const{return next ;}
};
template<class T, int incr>
int PStash<T, incr>::add(T* element){
for(int i=0; i<next; i++){
delete storage[i] ;
//sıfır göstergeç tamam
storage[i]=0 ;
//sadece güvenlik için
}
delete []storage ;
}
template<class T, int incr>
T* PStash<T, incr>::operator[](int index) const{
require(index>=0,
"PStash::operator[] index negative") ;
if(index>=next)
return 0 ;
//sonu gösterir
require(storage[index] !=0,
"PStash::operator[] returned null pointer") ;
//istenen ögeye göstergeç
return storage[index] ;
}
template<class T, int incr>
T* PStash<T, incr>::remove(int index){
//operator[] geçerliliği denetler
T* v=operator[](index) ;
//göstergeçi kaldır
if(v!=0) storage[index]=0 ;
return v ;
}
template<class T, int incr>
void PStash<T, incr>::inflate(int increase){
const int psz=sizeof(T*) ;
T** st=new T*[quantity+increase] ;
memset(st, 0, (quantity+increase) * psz) ;
memcpy(st, storage, quantity * psz) ;
quantity+=increase ;
delete []storage ; //eski bellek
storage=st ;
//yeni belleğe işaret
}
#endif //TPSTASH_H///:~
Burada öntanımlı artış büyüklüğü kullanılmaktadır, değeri inflate( ) işlev
çağrısını emniyete almak için küçük seçildi. Bu yolla her şeyin doğru
işlemesi sağlanır.
Kalıplı PStash'in aidiyet denetimini sınamak için, aşağıdaki sınıf kendi
oluşumunu ve yıkımını belgeler. Ve ayrıca oluşturulan bütün nesnelerin
silinmesi güvenceye alınır. AutoCounter yığıtta oluşturulan sadece o
tipteki nesnelere izin verir.
//:B16:AutoCounter.h
#ifndef AUTOCOUNTER_H
#define AUTOCOUNTER_H
#include ".../require.h"
#include <iostream>
#include <set>
//Standart C++ kütüphane kabı
#include <string>
class AutoCounter{
static int count;
int id ;
class CleanupCheck{
std::set<AutoCounter*> trace ;
public:
void add(AutoCounter* ap){
trace.insert(ap) ;
}
void remove(AutoCounter* ap){
require(trace.erase(ap)==1,
"AutoCounter'i silmeyi iki kez dene");
}
~CleanupCheck( ){
std::cout<<"~CleanupCheck( )"<<std::endl ;
require(trace.size( )==0,
"AutoCounter nesnelerinin tamami silinmedi");
}
};
static CleanupCheck verifier ;
AutoCounter( ) :id(count++){
verifier.add(this) ;
//kendisini kaydet
std::cout<<"olusturulan ["<<id<<"]"
<<std::endl ;
}
//kopya yapıcı ve atama engelleme işlemi
AutoCounter(const AutoCounter&) ;
void operator=(const AutoCounter&) ;
public:
//sadece bununla nesneler oluşur
static AutoCounter* create( ){
return new AutoCounter( ) ;
}
~AutoCounter( ){
std::cout<<"destroying["<<id
<<"]"<<std::endl ;
verifier.remove(this) ;
}
//nesneleri ve göstergeçleri yazdır
friend std::ostream& operator<<(
std::ostream& os, const AutoCounter& ac){
return os<<"AutoCounter"<<ac.id ;
}
friend std::ostream& operator<<(
std::ostream& os, const AutoCounter* ac){
return os<<"AutoCounter"<<ac->id ;
}
};
#endif //AUTOCOUNTER_H///:~
Burada AutoCounter sınıfı iki şey yapar; ilki, AutoCounter'da her durum
için ardışık artan bir sayıyı id ye yerleştirir, static veri üyesi count'u
kullanarak bir sayı üretir.
İkincisi, daha karışık olanı ise; yuvalanmış CleanupCheck sınıfının
(verifier olarak adlandırılan) static durumunda oluşturulan ve yokedilen
bütün AutoCounter nesnelerinin kaydı tutulur, ve ayrıca eğer silinmesi
unutulmuşsa (yani bellek kaçağı varsa) rapor olarak sunulur. İşlemler,
Standart C++ kütüphanesinin set sınıfı kullanılarak yerine getirilir. İyi
tasarlanmış kalıpların hayatı ne kadar kolaylaştırdığını göstermesi
bakımından, mükemmel bir örnektir. (İkinci cildimizde, Standart C++
kütüphane kalıpları üzerine bütün ayrıntıları bulabilirsiniz)
set sınıfı tuttuğu tipe göre kalıplanır; burada AutoCounter göstergeçlerini
tutmak için vaziyet almıştır. Bir set, eklenecek her farklı nesnenin sadece
bir durumuna izin verir; bu add( ) işlevinde, set::insert( ) ile yerine
getirilir.
insert( ) işlevi eklemeye çalıştığınız ama aslında eklenmekte olan bazı
şeyler hakkında geri dönüş değerleri ile bilgi verir. Bununla birlikte,
mademki nesne adresleri ekleniyor, bütün nesnelerin tek adresleri olduğu
konusunda C++ dilinin güvencesine inanancağız.
remove( ) işlevinde set::erase( ), set'ten AutoCounter göstergeçlerini
silmek için kullanılır. Geri dönüş değeri kaç tane öge durumunun
kaldırıldığını gösterir; bizim durumumuzda bu değerin, sadece sıfır (0)
veya bir (1) olması beklenir. Bu değer sıfırsa (0), o nesnenin önceden
set'ten silindiği anlamına gelir, ve eğer siz onu ikinci defa silmeye
kalkarsanız, require( ) işlevi aracılığı ile, program hata raporu verir.
CleanupCheck yıkıcı işlevi, set büyüklüğünün sıfırlandığı-bunu anlamı
bütün nesnelerinin silindiği- konusunda son denetimi yaparak emniyeti
sağlamlaştırır. Yok eğer sıfır değilse, bellek kaçağı var demektir, bu da
require( ) işlevince rapor edilir.
AutoCounter yapıcı ve yıkıcı işlevleri, kendilerini verifier( ) nesnesi ile
kaydeder ve kayıtttan silerler. Programda gördüğünüz üzere, yapıcı ve
kopya yapıcı işlev ile atama işleci private erişim hakkına sahiptirler. Bu
yüzden nesne oluşturmanın tek tolu static create( ) üye işlevi ile olur- bu,
fabrika (factory) usulünün basit bir örneğidir-, bütün nesnelerin kümede
(heap) oluşturulması güvenceye alınır. Bundan dolayı verifier( ) işlevi,
atamalar ve kopya yapma esnasında karışıklığa neden olmaz.
Mademki bütün üye işlevler satıriçi hale getirildi, uygulama dosyası için
geri kalan tek şey statik veri üye tanımlarıdır.
//:B16:AutoCounter.cpp {O}
//statik sınıf üyelerinin tanımları
#include "AutoCounter.h"
AutoCounter::CleanupCheck AutoCounter::verifier ;
int AutoCounter::count=0 ;
///:~
Eldeki AutoCounter ile, şimdi PStash'in yeteneklerini sınayabiliriz.
Aşağıdaki örnek, PSTash yıkıcı işlevinin sahip olunan bütün nesneleri
sildiğini sadece göstermez, ayrıca AutoCounter sınıfının silinmeyen
nesneleri nasıl algılayabildiğini de gösterir.
//:B16:TPStashTest.cpp
//{L} AutoCounter
#include "AutoCounte.h"
#include "TPSTash.h"
#include <iostream>
#include <fstream>
using namespace std ;
int main( ){
PStash<AutoCounter> acStash ;
for(int i=0; i<10; i++)
acStash.add(AutoCounter::create( )) ;
cout<<"5 i elle sil"<<endl ;
for(int j=0; j<5; j++)
delete acStash.remove(j) ;
cout<<"remove two without deleting them"
<<endl ;
//.....silme hata iletilerini üretmek
cout<<acStash.remove(5)<<endl ;
cout<<acStash.remove(6)<<endl ;
cout<<"yıkıcı işlev geri kalanları siler: "
<<endl ;
//önceki bölümlerde yer alan sınamaları yinele
ifstream in("TPStashTest.cpp") ;
assure(in, "TPStashTest.cpp") ;
PStash<string> stringStash ;
string line ;
while(getline(in, line))
stringStash.add(new string(line));
//stringleri yazdır
for(int u=0; stringStash[u]; u++)
cout<<"stringStash["<<u<<"]= "
<<*stringStash[u]<<endl ;
}///:~
5 ve 6 numaralı AutoCounter ögeleri PStash'ten kaldırıldığı zaman,
çağıranın sorumluluğuna girerler. Ama mademki çağıran hiçbir zaman
onları silmediği için bellek kaçaklarına neden olurlar, o zaman
AutoCounter tarafından program çalışma zamanında algılanırlar.
Programı çalıştırdığınız zaman, görülen hata iletisi çok özel değildir.
Kendi sistemlerinizde bellek kaçaklarını saptamak için, AutoCounter
örneğinde gösterilen biçimi kullanırsanız, büyük ihtimalle silinmeyen
nesnelerle ilgili daha ayrıntılı bilgi isteyeceksiniz. Bunu gerçeklemek için,
ikinci cildimizde konu ayrıntıları ile irdelenecektir.
Aidiyetin açıklanması ve açıklanmaması
Şimdi aidiyet sorununa dönelim. Nesneleri değerleri ile tutan kaplar
(containers) genellikle aidiyet ile ilgili kaygılanmazlar, zira sakladıkları
nesnelere açık biçimde sahip olurlar. Ama eğer kap göstergeçler tutuyorsa
(C++ da oldukça yaygındır, bilhassa çokbiçimlilikten dolayı), daha sonra
çok muhtemeldir ki bu göstergeçler ayrıca programın başka yerlerinde
kullanılır, ve nesnenin silinmesini istemek gereksizdir, zira programdaki
öbür göstergeçler yokedilen nesneyi dayanak almış olabilir. Bunun
olmasını engellemek için, kap tasarımı ve kullanımı esnasında, aidiyet
mutlaka gözönünde bulundurulmalıdır.
Birçok program bu programdan daha basittir, ve aidiyet sorunu ile
karşılaşılmaz; bir kap sadece o kap tarafından kullanılan nesne
göstergeçlerini tutar. Bu durumda aidiyetlik çok kolay anlaşılır; kap kendi
nesnelerinin sahibidir.
Aidiyedlik sorununu yönetmeye en iyi yaklaşım; müşteri programcıya
seçim hakkı vermektir. Bunu başarmak için aidiyeti belirleyen
öntanımlanmış yapıcı işlev değişkeni kullanılır (en basit yol budur). Buna
ek olarak, al (get) ve düzenle (set) işlevleri kabın aidiyetini gözlemek ve
değiştirmek için bulunmalıdır. Kapta nesneyi kaldırmak için işlevler varsa,
aidiyetlik durumu genellikle kaldırılanı etkiler, böylece kaldırma işlevinde
silmeyi denetleyen ayrıca seçenekler olur. Kapta bulunan her öge için
aidiyet bilgisi makul biçimde yerleştirilebilir, böylece bellekteki herhangi
bir yer, silinmeye gereksinimi olup olmadığını bilir; bu dayanç sayımının
bir türüdür. Tabii, nesneyi işaret eden dayanç sayısını kabın bilip, nesnenin
bilmemesi hariç.
//:B16:OwnerStack.h
//çalışma sırasında aidiyeti denetleyebilen yığıt
#ifndef OWNERSTACK_H
#define OWNERSTACK_H
template<class T> class Stack{
struct Link{
T* data ;
Link* next ;
Link(T* dat, Link* nxt) ;
: data(dat), next(nxt) {}
}* head;
bool own ;
public:
Stack(bool own=true) : head(0), own(own){}
~Stack( ) ;
void push(T* dat){
head=new Link(dat, head) ;
}
T* peek( ) const{
return head ? head->data : 0 ;
}
T* pop( ) ;
bool owns( ) const{return own;}
void owns(bool newownership){
own=newownership ;
}
//otomatik tip çevrimi: boş değilse true dur.
operator bool( ) const{return head!=0;}
};
template<class T> T* Stack<T>::pop( ){
if(head==0)return 0;
T* result=head->data ;
Link* oldHead=head ;
head=head->next ;
delete oldHead ;
return result ;
}
template<class T> Stack<T>::~Stack( ){
if(!own)return ;
while(head)
delete pop( ) ;
}
#endif //OWNERSTACK_H//:~
Kabın önceden tanımlanmış işlevleri kendi nesnelerini silmek amacını
taşır. Ama, ya yapıcı işlev değişkeni ile oynayarak, ya da owns( ) yaz/oku
üye işlevini kullanarak burada değişiklik yapılabilir.
Muhtemelen kalıpların çoğunda rastlayacağınız gibi, bütün uygulama
başlık dosyasında yer alır. Şimdi küçük bir sınama ile aidiyet yeteneklerini
deneyelim.
//:B16:OwnerStackTest.cpp
//{L} AutoCounter
#include "AutoCounter.h"
#include "OwnerStack.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
Stack<AutoCounter> ac ;
//aidiyet tamam
Stack<AutoCounter> ac2(false) ; //aidiyeti kaldır
AutoCounter* ap ;
for(int i=0; i<10; i++){
ap=AutoCounter::create( ) ;
ac.push(ap) ;
if(i%2==0)
ac2.push(ap) ;
}
while(ac2)
cout<<ac2.pop( )<<endl ;
//yıkıcı işlev kullanımı gerekmez zira
//ac bütün nesnelere sahiptir
}///:~
ac2 nesnesi kendisine yerleştirilen nesnelerin sahibi değildir, bunun nedeni
aidiyet sorumluluğunu baba kap ac nin üzerine almasıdır. Eğer, kabın
ömründe yolayrımı olursa, kabın onun nesnelerine sahip olup olmamasını
değiştirmek isterseniz, böyle birşeyi owns( ) işlevini kullanarak
yapabilirsiniz.
Nesne-nesne temelleri üzerinde, aidiyetliğin kısmıliğini değiştirmek ayrıca
mümkündür, ama bu aidiyet sorununun çözümünü sorunun kendisinden
çok daha karmaşık yapar.
Nesneleri değerleri ile tutmak
Kalıplar olmasaydı, üretici kabın içinde nesnelerin bir kopyasını
oluşturmak, aslında karmaşık bir sorun olurdu. Kalıplar sayesinde bu
işlemler göreceli olarak kolaylaşır,- sadece göstergeçler yerine nesnelerin
tutulması yeğlenir.
//:B16:ValueStack.h
//nesneleri yığıtta değerleri ile tutmak
#ifndef VALUESTACK_H
#define VALUESTACK_H
#include ".../require.h"
template<class T, int ssize=100>
class Stack{
//öntanımlı yapıcı işlev dizide yeralan
//her öge için nesne ilk değer atamasını yapar
T stack[ssize] ;
int top ;
public:
Stack( ) : top(0){}
//kopya yapıcı işlev diziye nesne kopyalar
void push(const T& x){
require(top<ssize, "cok sayıda push( )") ;
stack[top++]=x ;
}
T peek( ) const{return stack[top] ;}
//çekip aldığınız zaman bile nesne hala var
//hemen artık daha fazla temin edilemez
T pop( ){
require(top>0, "cok sayida pop( )") ;
return stack[--top] ;
}
};
#endif //VALUESTACK_H///:~
Kaptaki nesnelerin kopya yapıcı işlevi, nesneleri değerle aktararak ve geri
döndürerek işin çoğunu yapar. push( ) işlevi içinde, Stack dizisinde
nesnenin saklanması T::operator= ile başarılır. Bunun çalışmasını
güvenceye almak için, SelfCounter adı verilen bir sınıf, nesne oluşum ve
kopya yapımlarının izlerini muhafaza eder.
//:B16:SelfCounter.h
#ifndef SELFCOUNTER_H
#define SELFCOUNTER_H
#include "ValueStack.h"
#include <iostream>
class SelfCounter{
static int counter ;
int id ;
public:
SelfCounter( ) : id(counter++){
std::cout<<"olustu: "<<id<<std::endl ;
}
SelfCounter(const SelfCounter& rv) : id(rv.id){
std::cout<<"kopyalandı: "<<id<<std::endl ;
}
SelfCounter operator=(const SelfCounter& rv){
std::cout<<"atandi: "<<rv.id<<" e "<<id<<std::endl ;
return *this ;
}
~SelfCounter( ){
std::cout<<"silindi: "<<id<<std::endl ;
}
friend std::ostream& operator<<(
std::osteram& os, const SelfCounter& sc){
return os<<"SelfCounter: "<<sc.id ;
}
};
#endif //SELFCOUNTER_H///:~
//:B16:SelfCounter.cpp {O}
#include "SelfCounter.h"
int SelfCounter::counter=0 ; ////:~
//:B16:ValueStackTest.cpp
//{L} SelfCounter
#include "ValueStack.h"
#include "SelfCounter.h"
#include <iostream>
using namespace std ;
int main( ){
Stack<SelfCounter> sc ;
for(int i=0; i<10; i++)
sc.push(SelfCounter( )) ;
//peek( ) işlevi tamam, sonuç geçici
cout<<sc.peek( )<<endl ,
for(int k=0; k<10; k++)
cout<<sc.pop( )<<endl ;
}///:~
Bir Stack kabı oluşturulduğu zaman, kaptaki nesnenin öntanımlı yapıcı
işlevi, dizide bulunan her nesne için çağrılır. Başlangıçta açık bir neden
olmaksızın 100 tane SelfCounter nesnesi oluşturulur, ama bunlar sadece
dizi ilk değer atamasıdır. Biraz masraflı görünebilir ama, bu kadar basit bir
tasarım için başka bir yol yoktur. Stack büyüklüğünün artmasına dinamik
olarak izin vererek daha genel olması sağlanırsa, daha karmaşık durumla
bile karşılaşılır. Yukarıda verilen uygulamada; yeni bir dizi (daha büyük)
oluşturma, eski diziyi yeniye kopyalama, ve eskiyi ortadan kaldırma bu işe
dahildir. (Bunu aslında, standart C++ kütüphanesinde vector sınıfı yapar)
Yineleyicilere (iterators) Giriş
Yineleyici bir nesnedir. Başka nesnelerin kabına gider her seferinde
onlardan bir tanesini seçer. Üstelik ilgili kabın uygulamasına doğrudan
erişim sağlanmaz. Kap ögelere doğrudan erişimi sağlasın veya sağlamasın,
yineleyiciler ögelere erişmenin standart yolunu bilir. Yineleyiciler,
çoğunlukla kap sınıfları ile birlikte kullanılırlar. Standart C++ kaplarının
kullanım ve tasarımında yineleyiciler temel bir unsurdur. Bu konunun
tamamlayıcı ayrıntıları ikinci cildimizde verilecektir. Yineleyiciler ayrıca
bir tasarım düzeneğidir (design patterns), bu açıdan da incelemelesi ikinci
ciltte yapılacak.
Birçok açıdan yineleyici "cingöz göstergeç"tir (smart pointer). Aslında
sizin de farkedeceğiniz gibi, yineleyiciler genellikle göstergeçleri taklit
ederler. Bununla birlikte göstergeçlere benzemeyen yanı ise; yineleyiciler
güvenliği sağlamak için tasarlanırlar. Böylece çok daha az makul, dizi
sonunu bırakıp gitme işi yapılır. (veya yaparsanız, o konuda çok daha
kolay bilgi elde edersiniz)
Bu bölümün ilk örneğine bir daha bakalım. Şimdi yineleyici ekleyerek
programı yeniden geliştirelim.
//:B16:IterIntStack.cpp
//yineleyicili basit integer yığıt
//{L} fibonacci
#include "fibonacci.h"
#include ".../require.h"
#include <iostream>
using namespace std ;
class IntStack{
enum{ssize=100};
int stack[ssize] ;
int top ;
public:
IntStack( ) : top(0){}
void push(int i){
require(top<ssize, "cok sayida push( )") ;
stack[top++]=i ;
}
int pop( ){
require(top>0, "cok sayida pop( ) ");
return stack[--top] ;
}
friend class IntStackIter ;
};
//bir yineleyici "cingöz" göstergeç gibidir.
class IntStackIter{
IntStack& s;
int index ;
public:
IntStackIter(IntStack& is) : s(is), index(0) {}
int operator++( ){//önek
require(index<s.top,
"yineleyici sinir disinda");
return s.stack[++index];
}
int operator++(int){//sonek
require(index<s.top,
"yineleyici sinir disinda");
return s.stack[index++] ;
}
};
int main( ){
IntStack is ;
for(int i=0; i<20; i++)
is.push(fibonacci(i)) ;
//yineleyici ile tersle
IntStackIter it(is) ;
for(int j=0; j<20; j++)
cout<<it++<<endl ;
}///:~
IntStackIter sadece IntStack ile çalışması için oluşturuldu. IntStackIter,
IntStack'in friend'idir, böylece IntStack'in bütün private ögelerine
erişebilir.
IntStackIter'in görevi göstergeç gibi, IntStack içine gidip değerleri elde
etmektir. Elimizdeki basit örnekte; IntStackIter sadece ileri doğru hareket
edebilir (operator++ nın, hem önekli hemde sonekli biçimlerini
kullanarak). Bununla birlikte yineleyicilerin tanımlanabilmesi ile ilgili,
çalışabileceği kap koşulları tarafından mecbur edilenler dışında, sınır
bulunmamaktadır. Bir yineleyici için, ilgili kabın içinde bir yolla dolaşmak
ve kaptaki değerlerin değişmesini sağlamak çok iyidir.
Yineleyicinin kendisine tek bir kap nesnesi iliştiren yapıcı işlev ile birlikte
oluşturulması adettendir. Ve bu yineleyici ömrü boyunca başka değişik bir
kaba iliştirilemez. (Yineleyiciler küçük ve masrafsızdırlar, bu yüzden
başka bir tane daha kolaylıkla yapılabilir)
Yineleyiciler sayesinde yığıt ögelerini çekip almadan ögeler baştan başa
incelenebilir, tıpkı göstergecin dizi ögelerinde gezinebilmesi gibi. Bununla
birlikte, yineleyici yığıtın temel yapısını bilir ve ögeleri baştan aşağı gezer,
böylece bunlar sanki göstergeç değerini artırıyormuş gibi görünerek, ama
aslında çok daha fazlasını yaparak, olur. Yani yineleyici için anahtar olan;
bir kap ögesinden diğerine gitmenin karmaşık sürecini soyutlamaktır, yani
göstergeç gibi bir şey olarak görünür. Programda her yineleyici için amaç
aynı arayüze sahip olmaktır, bu nedenle yineleyiciyi kullanan kod neyin
işaret edildiğine dikkat etmez- hemen bütün yineleyicileri yeniden
yerleştirebileceğini aynı yolla bilir-, böylece yineleyicinin gösterdiği kap
önemsiz olur. Bu yolla çok daha üretici kod yazılabilir. Standart C++
kütüphanesindeki kap (containers) ve algoritmaların tamamı,
yineleyicilerin bu prensibine dayanır.
Unsurları çok daha üretici kılmak için şu söylenmelidir; "her kap yineleyici
(iterator) olarak adlandırılan kendisi ile birlikte olan bir sınıfa sahiptir",
ama hemen belirtelim; bu, tipik adlandırma sorunlarına yolaçar. Çözüm
ise, her kaba yuvalanmış yineleyici (iterator) sınıf eklemektir. (bu
durumda dikkat edilmesi gereken husus, "iterator" küçük harfle başlar,
böylece Standart C++ kütüphane yazımına uyulmuş olur). Şimdi
IterIntStack.cpp örneğini yuvalanmış yineleyici (iterator) ile verelim;
//B16:NestedIterator.cpp
//kapta bir yineleyiciyi yuvalamak
//{L} fibonacci
#include "fibonacci.h"
#include ".../require.h"
#include <iostream>
#include <string>
using namespace std ;
class IntStack{
enum{ssize=100};
int stack[ssize] ;
int top ;
public:
IntStack( ) : top(0) {}
void push(int i){
requir(top<ssize, "cok sayida push( )") ;
stack[top++]=i ;
}
int pop( ){
require(top>0, "cok sayida pop( )");
return stack[--top];
}
class iterator{
friend class iterator ;
class iterator{
IntStack& s ;
int index ;
public:
iterator(IntStack& is) : s(is), index(0) {}
//"son nöbetçi" yineleyiciyi oluşturma
iterator(IntStack& is, bool)
: s(is), index(s.top){}
int current( ) const {return s.stack[index] ;0
int operator++( ){//önek
require(index<s.top,
"yineleyici kapsam disi");
return s.stack[++index] ;
}
int operator++( ){//sonek
require(index<s.top,
"yineleyici kapsam disi");
return s.stack[index++] ;
}
//yineleyiciyi bir ileri atlatin
iterator& operator+=(int amount){
require(index+amount<s.top,
"IntStack::iterator::operator+=( )"
"kapsam disina cikma cabasi") ;
index+=amount ;
return *this ;
}
//son bolumde iseniz bakin
bool operator==(const iterator& rv) const{
return index==rv.index ;
}
bool operator!==(const iterator& rv) const{
return index!=rv.index ;
}
friend ostream&
operator<<(ostream& os, const iterator& it){
return os<<it.current( ) ;
}
};
iterator begin( ){return iterator(*this) ;}
//sondaki nöbetçiyi olustur
iterator end( ){return iterator(*this, true) ;}
};
int main( ){
IntStack is ;
for(int i=0; i<20; i++)
is.push(fibonacci(i)) ;
cout<<"butun IntStack'i bastanbasa dolas\n";
IntStack::iterator it=is.begin( ) ,
while(it!=is.end( ))
cout<<it++<<endl ;
cout<<"IntStack'in bir bolumunu dolas"\n;
IntStack::iterator
start=is.begin( ), end=is.begin( ) ;
start+=5, end+=15 ;
cout<<"start= "<<start<<endl ;
cout<<"end= "<<end<<endl;
while(start!=end)
cout<<start++<<endl ;
}///:~
Yuvalanmış friend sınıfı yapıldığı zaman, önce sınıf adı bildirim sürecine
girilir, daha sonra friend olarak bildirilir ve en sonunda sınıf tanım işlemi
(yani kodlama) gerçeklenir.
Burada yineleyiciye bazı yeni kıvrımlar eklendi. current( ) üye işlevi o an
yineleyicinin seçtiği kap ögesini üretir. Gelişigüzel sayıda ögeyi
yineleyicinin atlaması (ileri) için operator++= kullanılır. Bundan başka
ayrıca iki tane bindirilmiş işleç bulunmaktadır; == ve != yineleyicileri
birbiri ile karşılaştırırlar. Bunlar herhangi iki IntStack==iterator u
karşılaştırabilir. Aslında bunlar, gerçek bütün standart C++
yineleyicilerinin yaptığı şekilde, yineleyici eğer dizi sonunda ise bunu
sınamak içindir. Düşünce iki yineleyicinin alt ve üst sınırları
belirlemesidir, ilk yineleyici ilk ögeyi gösterir, ikinci yineleyici ise son
ögeye kadar fakat son öge hariç. Bundan dolayı iki yineleyicinin
tanımladığı kapsama alanında dolaşmak için, aşağıdakine benzer yazım
biçimi seçilebilir;
while(start!=end)
cout<<start++<<endl;
start ve end kapsama alanındaki iki yineleyicidir. Hemen belirtelim; end
yineleyicisi son nöbetçi olarak görülür, dayancından kurtarılmaz, ve orada
bulunma sebebi sadece dizi sonuna gelindiğini bildirmektir. Bundan ötürü
"sonu bir geçiyor" anlamını temsil eder.
Zamanın çoğunu kapta bütün diziyi dolaşmak isterken harcamak istersiniz,
bu yüzden kap, dizi başını ve son nöbetçiyi gösteren, yineleyicilerin üretim
biçimlerine gereksinim duyar. Burada standart C++ kütüphanesinde gibi,
yineleyiciler kap üye işlevleri begin( ) ve end( ) tarafından üretilir. begin
( ) kabın baş kısmını(yığıta yerleştirilen ilk öge) öntanımlı olarak gösteren
ilk iterator yapıcı işlevini kullanır. Bunun yanında, ikinci yapıcı işlev, end
( ) tarafından kullanılan, son nöbetçi yineleyiciyi (iterator'u) üretmek için
gereklidir. "Sonda" olmanın anlamı; yığıt üstünün işaret edilmesidir, zira
top her zaman yığıttaki temin edilebilen- ama kullanılmaz- bir sonraki
alanı gösterir. Buradaki yineleyici (iterator) yapıcı işlevi bool tipinde ikinci
bir değişken bulundurur, görevi sadece iki yapıcı işlev arasında fark
yaratmak olup, kukla unsurdur.
main( ) işlevinde InStack yine fibonacci sayıları ile doldurulur, ve
yineleyiciler (iterators) InStack'in tamamının içinde hareket ederler. Bu
işlemi dizinin daralan kapsamı alanında sürdürürler.
Tabii bir sonraki adım, kodu, tutulan tip üzerinde kalıplaştırarak
genelleştirmektir, böylece sadece int değerleri tutmak zorunluluğu ortadan
kalkar, ve başka tipler de tutulabilir.
//:B16:IterStackTemplate.h
//yuvalanmış yineleyici ile basit yığıt kalıbı
#ifndef ITERSTACKTEMPLATE_H
#define ITERSTACKTEMPLATE_H
#include ".../require.h"
#include <iostream>
template<class T, int ssize=100>
class StackTemplate{
T stack[ssize] ;
int top ;
public:
StackTemplate( ) : top(0){}
void push(const T& i){
require(top<ssize, "cok sayida push( )");
stack[top++]=i ;
}
T pop( ){
require(top>0, "cok sayida pop( )") ;
return stack[--top] ;
}
class iterator ;
//bildirim gerekli
friend class iterator ;
//friend oluştur
class iterator{
//tanimlama
StackTemplate& s ;
int index ;
public:
iterator(StackTemplate& st) : s(st), index(0){}
//"son nöbetçi" yineleyici oluşturuluyor
iterator(StackTemplate& st, bool)
: s(st), index(s.top) {}
T operator*( ) const{return s.stack[index];}
T operator++( ){//önek biçimi
require(index<s.top,
"yineleyici kapsam disi");
return s.stack[++index] ;
}
T operator++(int){//sonek biçimi
require(index<s.top,
"yineleyici kapsam disi");
return s.stack[index++] ;
}
// yineleyiciyi ileri atlayin
iterator& operator+=(int amount){
require(index+amount<s.top,
"StackTemplate::iterator::operator+=( ) "
"sinir disina cikmaya calisilir");
index+=amount ;
return *this ;
}
//sondaysanız bakin
bool operator==(const iterator& rv) const {
return index==rv.index ;
}
bool operator!=(const iterator& rv) const {
return index!=rv.index ;
}
friend std::ostream& operator<<(
std::ostream& os, const iterator& it){
return os<<*it ;
}
};
iterator begin( ){return iterator(*this) ;}
//son nöbetçiyi oluştur
iterator end( ){return iterator(*this, true) ;}
};
#endif //ITERSTACKTEMPLATE_H///:~
Düzenli bir sınıftan kalıp (template) sınıflara dönüşüm manidar bir
biçimde saydamdır. Normal bir sınıfın ilk oluşumu ve hata ayıklamasına
bu yaklaşım, ve daha sonra onu kalıba yatırmak, genellikle kalıbı sıfırdan
oluşturmaktan daha kolay kabul edilir.
Farkedeceğiniz gibi sadece aşağıdaki kod yerine;
friend iterator ;
//friend yapar
Bu kod kullanılır ;
friend class iterator ; //friend yapar
Bu önemlidir, zira eklenen dosyadan "iterator" adı, şimdiden çalışma
alanına girer.
iterator o anki ögeyi seçmek için, current( ) üye işlevi yerine operator*
kullanır, bu etkinlik iterator'u göstergeçe daha çok benzetir, ve uygulamada
çok yaygındır.
Kalıp sınayan örneği yeniden düzenleyelim;
//:B16:IterStackTemplateTest.cpp
//{L} fibonacci
#include "fibonacci.h"
#include "IterStackTemplate.h"
#include <iostream>
#include <string>
#include <fstream>
using namespace std ;
int main( ){
StackTemplate<int> is;
for(int i=0; i<20; i++)
is.push(fibonacci(i)) ;
//boydan boya yineleyici ile dolaş
cout<<"Boydan boya yineleyici ile StackTemplate'i dolas"\n;
StackTemplate<int>::iterator it=is.begin( ) ;
while(it!=is.end( ))
cout<<it++<<endl ;
cout<<"bir kismi dolas"\n"
StackTemplate<int>::iterator
start=is.begin( ), end=is.begin( ) ;
start+=5, end+=15 ;
cout<<"start= "<<start<<endl ;
cout<<"end= "<<end<<endl ;
while(start!=end)
cout<<start++<<endl ;
ifstream in("IterStackTemplateTest.cpp");
assure(in, "IterStackTemplateTest.cpp");
string line ;
StackTemplate<string> strings ;
while(getline(in, line))
strings.push(line) ;
StackTemplate<string>::iterator
sb=strings.begin( ), se=strings.end( ) ;
while(sb!=se)
cout<<sb++<<endl ;
}///:~
Yineleyicinin ilk kullanımı, onu baştan sona yürütür. (ve "son nöbetçinin"
doğru çalıştığı gözlenmiş olur). İkinci kullanımda ise yineleyiciler kapsamı
alanı sınırlarını kolaylıkla belirler.(standart C++ kütüphanesi kap ve
yineleyicileri, hemen her yerde bu sınır belirleme usulünü kullanırlar).
Bindirilmiş operator+=, start ve end yineleyicilerini, is ögelerinin
kapsama alanının ortasındaki yere götürür, ve bu ögeler yazdırılır. Çıktıda
farkedileceği gibi, "son nöbetçi" kapsama alanına dahil değildir, böylece
sona varış saptaması kapsama alanının sonu bir geçilerek belirlenir- ama
son nöbetçi dayançlıktan kurtarılır, veya sıfır göstergeç dayançlıktan
çıkarılarak sonlandırabilir.(Standart C++ kütüphanesi kap ve
yineleyicilerinde bulunmamasına rağmen -verimlilik içinStackTemplate::iterator güvencesini yerleştirdik, ama bu yüzden çok
dikkatli olmak gerekir).
Son olarak, StackTemplate'in sınıf nesneleri ile çalıştığını belirtmek için,
birisi string ile anlaştırılır ve kaynak kodlardan alınan satırlarla
doldurulur, daha sonra bunlar yazdırılır.
Yineleyicili Stack
Kitap boyunca örnek olarak kullanılan, dinamik olarak boyutlandırılabilir
Stack sınıfı ile süreç tekrarlanabilir. Şimdi yuvalanmış yineleyicili
(iterator) Stack sınıfı örneğini karışık halde verelim;
//:B16:TStack2.h
//Yuvalanmış yineleyicili kalıplanmış Stack
#ifndef TSTACK2_H
#define TSTACK2_H
template<class T> class Stack{
struct Link{
T* data ;
Link* next ;
Link(T* dat, Link* nxt)
: data(dat), next(nxt){}
}* head ;
public:
Stack( ) : head(0) {}
~Stack( ) ;
void push(T* dat){
head=new Link(dat, head) ;
}
T* peek( ) const{
return head ? head->data : 0 ;
}
T* pop( ) ;
//yuvalanmış yineleyici sınıfı:
class iterator ;
//sınıf bildirimi
friend class iterator ; //friend yapımı
class iterator{//şimdi tanımlanacak
Stack::Link* p ;
public:
iterator(const Stack<T>& t1) : p(t1.head) {}
//kopya yapıcı işlev
iterator(const iterator& t1) : p(t1.p) {0
//son nöbetçi yineleyici
iterator( ) : p(0) {0}
//operator++ bool sonucunu gösterir
bool operator++( ){
if(p->next)
p=p->next ;
else p=0 ;
//liste sonunu gösterir
return bool(p) ;
}
bool operator++(int){return operator++( ) ;}
T* current( ) const{
if(!p)return 0 ;
return p->data ;
}
//göstergeçi dayançtan kurtarma
T* operator->( ) const{
require(p!=0,
"PStack::iterator::operator->returns 0");
return current( ) ;
}
T* operator*( ) const{return current( ) ;0
//koşul sınaması için bool çevrimi
operator bool( ) const{return bool(p) ;}
//sonlanma sınaması için karşılaştırma
bool operator==(const iterator&) const{
return p!=0 ;
}
};
iterator begin( ) const{
return iterator (*this) ;
}
iterator end( ) const{return iterator( );0
};
template<class T> Stack<T>::~Stack( ){
while(head)
delete pop( ) ;
}
template<class T> T* Stack<T>::pop( ){
if(head==0) return 0 ;
T* result=head->data ;
Link* oldHead=head ;
head=head->next ;
delete oldHead ;
return result ;
}
#endif //TSTACK2_H///:~
Farkettiğiniz gibi aidiyeti desteklemek için sınıf değiştirilmekte, ki sınıf
tam tipi (veya en azından ana sınıfı, sanal yıkıcılar kullanıldığı kabul
edilerek çalışır) bildiğinden şimdi çalışır. Kap için varsayılan; nesnelerinin
silineceğidir, ama pop( ) ile çektiğiniz göstergeçlerden bizzat siz
sorumlusunuz.
Yineleyici basit ve fiziksel olarak çok küçüktür- tek göstergeç
büyüklüğündedir. Bir yineleyici (iterator) oluşturulduğu zaman, ilişik
listenin başına ilk değer olarak atanır, ve liste boyunca sadece ileri doğru
değer artımı yapılabilir. Eğer başlangıçtan ötede başlanmak istenirse, yeni
bir yineleyici oluşturulmalıdır, ve eğer listede bir nokta hatırlanmak
istenirse o noktada işaret edici varolan yineleyicilerden yeni bir yineleyici
oluşturulur.(yineleyici kopya yapıcı işlev kullanılarak)
Yineleyicilerin bakılan nesne için işlev çağırması, current( ) işlevi,
operator*, veya göstergeç dayançtan kurtarma operator->
(yineleyicilerde yaygın görüş) kullanılarak yapılır. Son olarak current( )
işlevine özdeş gibi görünen uygulamadır. Bunun nedeni o anki bakılan
nesneye göstergeç geri döndürmesidir, ama farklıdır zira göstergeç
dayançtan kurtarma işleçi dayançtan kurtarmanın üst seviyelerini
gerçekler. (12. bölüme bakınız)
iterator sınıfı önceki örnekte görülen yolu takip eder. class iterator kabın
içine yuvalanmıştır, içinde hem kaptaki bir ögeyi işaret eden bir yineleyici
hem de "son nöbetçi" yineleyiciyi oluşturan yapıcı işlevler bulunur. Ve kap
sınıfı bu yineleyicileri üreten begin( ) ve end( ) yöntemlerine sahiptir.
(Standart C++ kütüphanesi hakkında daha fazla bilgi edindiğinizde, burada
kullanılan iterator, begin( ) ve end( ) isimlerinin kap sınıflarını ne kadar
yukarı çıkardığını iyice göreceksiniz.)
Bütün uygulama başlık dosyasında yeraldığı için, ayrı bir .cpp uzantılı
dosya bulunmamaktadır. Yineleyici ile çalışan küçük bir sınama yapalım;
//:B16:TStack2Test.cpp
#include "TStack2.h"
#include ".../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std ;
int main( ){
ifstream file("TStack2Test.cpp") ;
assure(file, "TStack2Test.cpp") ;
Stack<string> textlines ;
//dosyayı oku ve satırları yığıta yükle
string line ;
while(getline(file,line))
textlines.push(new string(line)) ;
int i=0 ;
//yineleyiciyi listedeki satırları yazdırmak için kullan
Stack<string>::iterator it=textlines.begin( ) ;
Stack<string>::iterator* it2=0 ;
while(it!=textlines.end( )){
cout<<it->c_str( )<<endl ;
it++ ;
if(++i==10) //10. satırı hatırla
it2=new Stack<string>::iterator(it) ;
}
cout<<(*it2)->c_str( )<<endl ;
delete it2 ;
}///:~
Bir Stack, string nesnelerini tutmak için görevlendirilmiştir ve dosyadan
alınan satırlarla doldurulur. Daha sonra bir yineleyici oluşturulur ve dizi
boyunca gezdirilir. 10. satır kopya yapıcı işlev tarafından hatırlatılır,
birinci yineleyiciden ikinci yineleyici oluşturulur; daha sonra bu satır
yazdırılır ve yineleyici- dinamik olarak oluşturulan- silinir. Burada nesne
ömrünü denetlemek için dinamik nesne oluşumu kullanıldı.
Yineleyicili PStash
Kap (container) sınıflarının çoğunda yineleyici bulunması, onları daha
manalı yapar. Şimdi yineleyici eklenmiş PStash sınıfı örneği verelim;
//B16:TPStash2.h
//yuvalanmış yineleyicili kalıplanmış PStash
#ifndef TPSTASH2_H
#define TPSTASH2_H
#include ".../require.h"
#include <cstdlib>
template<class T, int incr=20>
class PStash{
int quantity ;
int next ;
T** storage ;
void inflate(int increase=incr) ;
public:
PStash( ) : quantity(0), storage(0), next(0){}
~PStash( ) ;
int add(T* element) ;
T* operator[] (int index) const ;
T* remove(int index) ,
int count( ) const{return next ;}
//yuvalanmış yineleyici sınıfı
class iterator ; //gereken bildirim
friend class iterator ; //friend yapıldı
class iterator{//tanım veriliyor
PStash& ps ;
int index ;
public:
iterator(PStash& PStash)
: ps(pStash), index(0) {}
//son nöbetçiyi oluştur
iterator(PStash& PStash, bool)
: ps(pStash), index(ps.next) {}
//kopya yapıcı işlev
iterator(const iterator& rv)
: ps(rv.ps), index(rv.index) {}
iterator& operator=(const iterator& rv){
ps=rv.ps ;
index=rv.index ;
return *this ;
}
iterator& operator++( ){
require(++index<=ps.next,
"PStash::iterator::operator++"
"index sinir disina gider") ;
return *this ;
}
iterator& operator++(int){
return operator++( ) ;
}
iterator& operator--( ){
require(--index>=0,
"PStash::iterator::operator--"
"index sinir disina gider") ;
return *this ;
}
iterator& operator--(int){
return operator--( ) ;
}
//yineleyiciyi ileri veya geri ilerletiniz
iterator& operator+=(int amount){
require(index+amount<ps.next &&
index+amount>=0,
"PStash::iterator::operator+= "
"indexi sinir disina cikarmaya calisma") ;
index+=amount ;
return *this ;
}
iterator& operator-=(int amount){
require(index-amount>=0,
"PStash::iterator::operator-= "
"indexi sinir disina cikarmaya calisma") ;
index-=amount ;
return *this ;
}
//ileri götürülecek yeni bir yineleyici oluşturun
iterator& operator+(int amount) const{
iterator ret(*this) ;
ret+=amount ;
//op+= sinir denetimini yapar
return ret ;
}
T* current( ) const{
return ps.storage[index] ;
}
T* operator*( ) const{return current( ) ;}
T* operator->( ) const{
require(ps.storage[index]!=0,
"PStash::iterator::operator->returns 0") ;
return current( ) ;
}
// o an bakilan ögeyi sil
T* remove( ){
return ps.remove(index) ;
}
//sonlanma için karsilastirma sınamalari
bool operator==(const iterator& rv) const{
return index==rv.index ;
}
bool operator!=(const iterator& rv) const{
return index!=rv.index ;
}
};
iterator begin( ){return iterator(*this) ;}
iterator end( ){return iterator(*this, true) ;}
};
//kaptaki nesnelerin silinmesi
template<class T, int incr>
PStash<T, incr>::~PStash( ){
for(int i=0; i<next; i++){
delete storage[i] ; //sıfır göstergeçler tamam
storage[i]=0 ;
//güvenlik için
}
delete []storage ;
}
template<class T, int incr>
int PStash<T, incr>::add(T* element){
if(next>=quantity)
inflate( ) ;
storage[next++]=element ;
return(next-1) ; //index sayisi
}
template<class T, int incr> inline
T* PStash<T, incr>::operator[] (int index) const{
require(index>=0,
"PSTash::operator[] index negative") ;
if(index>=next)
return 0 ;
//sonu gösterir
require(storage[index]!=0,
"PStash::operator[] returned null göstergeç") ;
return storage[index] ;
}
template<class T, int incr>
T* PStash<T, incr>::remove(int index){
//operator[] geçerlilik sınamalarını yapar
T* v=operator[](index) ;
//göstergeçi sil(remove)
storage[index]=0 ;
return v ;
}
template<class T, int incr>
void PStash<T, incr>::inflate(int increase){
const int tsz=sizeof(T*) ;
T** st=new T*[quantity+increase] ;
memset(st, 0, (quantity+increase)*tsz) ;
memcpy(st, storage, quantity*tsz) ;
quantity+=increase ;
delete []storage ;
//eski saklama yeri
storage=st ;
//yeni bellek yeri işareti
}
#endif //TPSTASH2_H///:~
Bu dosyanın büyük kısmı, hem önceki PStash hem de yuvalanmış
yineleyici (iterator)nin, kalıba (template) çevrilme kolaylığını gösterir.
Bunun yanında bu sefer, işleçler o anki yineleyiciye dayançlar geri
döndürdü, böylece en kullanılan ve esnek yaklaşım sergilenmiş oldu.
Yıkıcı işlev kaptaki bütün göstergeçler için delete çağırır, ve tip kalıp
(template) tarafından saptandığı için silme işlemi doğru şekilde yapılır.
Bilinmesi gereken eğer kap ana sınıfı işaret eden göstergeçleri tutarsa,
türetilenlerden gelen nesnelerin doğru şekilde silinmesi için, o tipin virtual
yıkıcı işlevlere sahip olması lazımdır, zira kapta yer alanların yukarı
dökümü yapılması gerekecektir.
PStash::iterator ömrü boyunca yineleyicinin tek kap nesne modeline
bağlı olarak işlemlenir. Ek olarak, kopya yapıcı işlev, onun kendisinden
oluşturulduğu varolan yineleyici gibi, aynı yerde işaret eden yeni bir
yineleyici oluşturulmasına izin verir, ayrıca etkin olarak kaba çentik atılır.
operator+= ve operator-= üye işlevleri yineleyicinin nokta sayısı kadar
hareket etmesini sağlar. Bunlar kap sınırlarına bağlı olarak gerçeklenir.
Bindirilmiş artma ve azalma işleçleri yineleyiciyi bir bellek yeri değiştirir.
operator+ işlecinin ürettiği yeni yineleyici, eklenen miktar kadar ileri
gider. Önceki örnekte görüldüğü gibi, göstergeç dayançtan kurtarma
işleçleri yineleyicinin baktığı ögeler üzerinde işlem yapmak için
kullanılırlar. Ve remove( ) işlevi kabın remove( ) işlevini çağırarak o anki
nesneyi siler.
Önceki kodun aynısı (standart C++ kütüphane kapları) "son nöbetçiyi"
oluşturmak için kullanılır; ikinci yapıcı işlev, kabın end( ) üye işlevi, ve
karşılaştırma için operator==, operator!= .
Aşağıdaki örnekte iki farklı Stash nesnesi oluşturulur ve sınanır, birisi Int
adı verilen yeni bir sınıf yapıcılık ile yıkıcılığı anlatır ve, diğeri ise standart
kütüphane string sınıfı nesnelerini tutar.
//:B16:TPStash2Test.cpp
#include "TPSTash2.h"
#include ".../require.h"
#include <iostream>
#include <vector>
#include <string>
using namespace std ;
class Int{
int i ;
public:
Int(int ii=0) : i(ii){
cout<<">"<<i<< ' ' ;
}
~Int( ){cout<<"~"<<i<<' ' ;}
operator int( ) const{return i;}
friend ostream&
operator<<(ostream& os, const Int& x){
return os<<"Int: "<<x.i ;
}
friend ostream&
operator<<(ostream& os, const Int* x){
return os<<"Int: "<<x->i ;
}
};
int main( ){
{//yıkıcı işlev çağrısını zorlamak için
PStash<Int> ints ;
for(int i=0; i<30; i++)
ints.add(new Int(i)) ;
cout<<endl ;
PStash<Int>::iterator it=ints.begin( ) ;
it+=5 ;
PStash<Int>::iterator it2=it+10 ;
for(; it!=it2; it++)
delete it.remove( ) ; //öntanımlı kaldırış
cout<<endl ;
for(it=ints.begin( ); it!=ints.end( ); it++)
for(*it)
//Remove( ) bellek boşluklarına neden
olur
cout<<*it<<endl ;
}//ints yıkıcı işlevi buraya çağrılır
cout<<"\.............................\" ;
ifstream in("TPSTash2Test.cpp") ;
assure(in, "TPStash2Test.cpp") ;
//string için anlaşma
PStash<string> strings ;
string line ;
while(getline(in, line))
strings.add(new string(line)) ;
PStash<string>::iterator sit=strings.begin( ) ;
for(; sit!=strings.end( ); sit++)
cout<<**sit<<endl ;
sit=strings.begin( ) ;
int n=36 ;
sit+=n ;
for(; sit!=strings.end( ); sit++)
cout<<n++<<": "<<**sit<<endl ;
}///:~
Uygun olması amacı ile Int hem Int& hemde Int* için ostream
operator<< ile bağlantılıdır. main( ) işlevindeki ilk kod parçasını
çevreleyen zincirli ayraçlar PStash<Int> in silinmesini mecbur kılar ve
yıkıcı işlevin silme işlemini otomatik olarak gerçeklemesi sağlanır. PStash
in geri kalanları sildiğini göstermek için, bir kısım ögeler elle silinerek
kaldırılır.
PStash'in her iki durumu için, bir yineleyici oluşturularak kap boyunca
gezme amacı ile kullanılır. Farkettiğiniz gibi, bu yapıların üretilmesindeki
güzellik sayesinde, dizi kullanımındaki uygulama ayrıntıları yüzünden
eleştiriye uğranılmaz. Kap ve yineleyici nesnelerine ne yapacakları
söylenir nasıl yapacakları değil. Bu da, çözümü kavramada, inşa etmede ve
değiştirmede kolaylık sağlar.
Neden Yineleyiciler (iterators)?
Şimdiye kadar yineleyiciler mekanik olarak incelendi, ne kadar önemli
olduklarını anlamak için daha karmaşık bir örnek ele alacağız.
Gerçek nesne yönelimli bir programda çokbiçimlilik, dinamik nesne ve
kapların birlikte kullanımına yaygın olarak rastlanır. Kaplar ve dinamik
nesne oluşumu, gereken nesne tipinin, ne ve kaç tane olduğunu
bilmeksizin sorunu çözer. Ve eğer kap, ana sınıf nesnelerini işaret eden
göstergeçleri tutmaya göre düzenlenirse, türetilmiş sınıf göstergecini kaba
yerleştirdiğiniz her sefer, bir yukarı döküm (yukarıdaki tipe dönüştürme)
gerçekleşir.
Kitabın son kısmında yer alan program kodları şimdiye kadar öğrenilen
birçok unsuru içermektedir. Eğer bunları iyice kavramışsanız kitabınızın
ikinci cildine hazırsınız demektir.
Şimdi öyle bir program geliştirelim ki kullanıcısına değişik çizimler yapma
izni versin, unutmayın her çizim bir nesnedir, Shape nesnelerinin
toplamını içinde barındırabilir.
//:B16:Shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <iostream>
#include <string>
class Shape{
public:
virtual void draw( )=0 ;
virtual void erase( )=0 ;
virtual ~Shape( ){}
};
class Circle : public Shape{
public:
Circle( ) {}
~Circle( ){std::cout<<"Circle::~Circle\n" ;}
void draw( ){std::cout<<"Circle::draw\n";}
void erase( ){std::cout<<"Circle::erase\n";}
};
class Square : public Shape{
public:
Square( ){}
~Square( ){std::cout<<"Square::~Square\n";}
void draw( ){std::cout<<"Square::draw\n";}
void erase( ){std::cout<<"Square::erase\n";}
};
class Line : public Shape{
public:
Line( ){}
~Line( ){std::cout<<"Line::~Line\n";}
void draw( ){std::cout<<"Line::draw\n";}
void erase( ){std::cout<<"Line::erase\n;}
};
#endif //SHAPE_H///
Burada ana sınıfta klasik sanal işlevler kullanılmış olup, türetilmiş
sınıflarda baskınlaştırılmışlardır. Görüleceği üzere Shape sınıfında sanal
yıkıcı işlevde bulunmaktadır, sanal işlevler aracılığı ile bazı unsurlar
herhangi bir sınıfa otomatik olarak eklenebilir.Eğer kap Shape nesnelerini
işaret eden göstergeçleri veya dayançları tutarsa, ve daha sonra bu nesneler
için sanal yıkıcı işlevler çağrıldığı zaman, herşey gayet düzgün biçimde
silinerek ortadan kaldırılır.
Aşağıdaki örnekte her farklı çizim için kalıplanmış kap sınıfının farklı
çeşidinin kullanıldığı görülür; PStash ve Stack, her ikisi de bu bölümde
tanımlandı, ve bunlara ek olarak standart C++ kütüphanesinden vector
sınıfı. Kapların "kullanımı" inanılmaz kolaydır, ve genel olarak kalıtım
kullanma yaklaşımı en iyisi değildir (harç çok daha anlamlıdır), ama
burada kalıtım en basit yaklaşımdır, ve örnekte kullanımı değeri azaltmaz.
//:B16:Drawing.cpp
#include <vector>
//standart vector kullanılır
#include "TPStash2.h"
#include "TStack2.h"
#include "Shape.h"
using namespace std ;
//çizim şekillerin kabıdır
class Drawing : public PStash<Shape> {
public:
~Drawing( ) {cout<<"~Drawing"<<endl ;}
};
//plan ise şekillerin değişik bir kabıdır
class Plan : public PStack<Shape> {
public:
~Plan( ) {cout<<"~Plan"<<endl ;}
};
//şema ise şekillerin başka değişik bir kabıdır
class Schematic : public vector<Shape*> {
public:
~Schematic( ) {cout<<"~Schematic<<endl ;}
};
//işlev kalıbı
template<class Iter>
void drawAll(Iter start, Iter end){
while(start!=end){
(*start)->draw( ) ;
start++ ;
}
}
int main( ){
//her kap tipinin farklı arayüzü bulunur
Drawing d ;
d.add(new Circle) ,
d.add(new Square) ;
d.add(new Line) ;
Plan p ;
p.push(new Line) ;
p.push(new Square) ;
p.push(new Circle) ;
Schematic s ;
s.push_back(new Square) ;
s.push_back(new Circle) ;
s.push_back(new Line) ;
Shape* sarray[]={
new Circle, new Square, new Line
};
//yineleyiciler ve kalıp işlevi onların esastan
//işlemlenmesine izin verir
cout<<"Drawing d: "<<endl ;
drawAll(d.begin( ), d.end( )) ;
cout<<"Plan p: "<<endl ;
drawAll(p.begin( ), p.end( )) ;
cout<<"Schematic s: "<<endl ;
drawAll(s.begin( ), s.end( )) ;
cout<<"Array sarray: "<<endl ;
//dizi göstergeçleri ile çalışır
drawAll(sarray,
sarray+sizeof(sarray)/sizeof(*sarray)) ;
cout<<"main sonu"<<endl ;
}///:~
Farklı tipteki kaplar, Shape'i işaret eden göstergeçleri tutar ve göstergeçler
Shape'ten türetilmiş sınıfların nesnelerine yukarı döküm yapar (yukarıdaki
tipe dönüşürler). Bununla birlikte çokbiçimlilik yüzünden, sanal işlevler
çağrıldığı zaman uygun davranışların sergilendiği görülür.
Shape* dizisi olan sarray bir kap olarak düşünülebilir.
İşlev kalıpları
Biraz önce drawAll( ) işlevinde bazı yeni unsurlar gördük. Bu bölümde
epey uzun süredir sadece sınıf kalıpları (class templates) ile ilgileniyorduk,
bunlar öğrendiğiniz gibi bir yada daha fazla tip parametresine dayalı yeni
sınıflara geçiştir. Bundan başka ayrıca kolaylıkla işlev kalıpları da
oluşturulabilir. İşlev kalıpları (function templates) tip parametrelerine
dayalı yeni işlevler oluşturulmasıdır. İşlev kalıplarını oluşturmada ki
neden, sınıf kalıplarını oluşturmadaki nedenlerle aynıdır; bulak olarak bir
kod yazılmaya çalışılır ve bu, bir veya daha fazla tip özelliğinden
vazgeçilerek yerine getirilir. Burada söylenmek istenen tip
parametrelerinin sadece belli işlemlerde destekte bulunduğudur, zira hangi
tiplerin tam olarak ne olduğu bilinmez.
drawAll( ) işlev kalıbı bir algorithm (standart C++ kütüphanesindeki çok
sayıda işlev kalıbı bu adla anılır) olarak düşünülebilir. Söylenen
yineleyicilerin yapmakla ödevli oldukları ögelerin kapsama alanını nasıl
belirleyecekleridir, yineleyiciler dayançtan kuratarılmak, artırılmak ve
karşılaştırılmak koşulu ile. Bunlar bu bölümde uzun süredir anlattığımız
yineleyicilerin tam olarak bir çeşididir, ve ayrıca -tesadüf değil- standart
C++ kütüphanesinde kapların ürettiği yineleyici çeşididir, ve bu örnekte
vector kullanımı ile belgelenmiştir.
Biz drawAll( ) işlevinden üretici (generic) algorithm olmasını istiyoruz,
bu yüzden kaplar hiç herhangi bir tip olamazlar ve kabın her faklı tipi için
algorithm'in değişik sürümlerini yazmak zorunda kalınmaz. Burada esas
olan işlev kalıplarıdır, zira kabın her farklı tipi için otomatik olarak özel
kod üretirler. Ama bir konuyu belirtelim; yineleyicilerin sağladığı ek
dolaylı yönlendirme olmaksızın bu üreticilik gerçekleşmez. Bu nedenle
yineleyiciler önemlidir; kabın esas yapısını bilmeksizin kapları içeren
genel amaçlı kodlar yazılmasına izin verirler. (hemen belirtelim;
yineleyiciler ve üretici algorithm'ler çalışmak için işlev kalıplarına
gereksinim duyarlar)
Bunun ispatını main( ) işlevinde görebilirsiniz, drawAll( ) her farklı kap
tipi için değişmeden çalışır. Ve çok daha ilginci, drawAll( ) sarray dizi
başlangıç ve sonunu işaret eden göstergeçlerle de çalışır. Bu yetenek yani
dizileri kap olarak işlemlemek, standart C++ kütüphanesine eklenmesi ile,
drawAll( )'u çok daha fazla algorithm haline sokmuştur.
Kap sınıf kalıplarında nadiren kalıtım ve yukarıdöküm olduğu için, hemen
hemen hiç bir zaman kap sınıflarında sanal (virtual) işlevlere rastlanmaz.
Kap (container) sınıfının yeniden kullanımı kalıtımla (inheritance) değil,
kalıplarla (templates) gerçeklenir.
ÖZET
Nesne yönelimli programlamanın temel parçalarından biri, kap sınıflarıdır.
Onlar sayesinde program ayrıntıları gizlenir ve basitleştirilir, ayrıca
program geliştirme süreci hızlanır. Ek olarak, C de bulunan oldukça katı
veri yapı teknikleri, basit dizilerle yer değiştirerek büyük güvenlik ve
esneklik sağlanır. Kullanımı kolay olduğu için müşteri programcılar
kaplara gerek duyarlar. İşte burası kalıpların (template) ortaya çıktığı
yerdir. Kalıplar sayesinde kaynak kodun yeniden kullanımı, acemi
programcılar için bile çocuk oyuncağıdır. (kalıtım ve harç ile kodun
yeniden kullanımı sadece hedef kod (uzantısı .o) ile olur). Aslında
kalıplarla kodun yeniden kullanımı kalıtım ve harca göre çok daha basittir.
Şimdiye kadar kap ve yineleyici sınıflar oluşturmayı öğrenmiş olmanıza
rağmen, standart C++ de bulunan kap ve yineleyicileri öğrenmek çok daha
uygundur, zira onları her derleyici ile kullanabilirsiniz. Kitabımızın ikinci
cildinde yer alan standart C++ kütüphanesi algorithms ve kaplar
gereksinimleri karşılar, bu nedenle yeniden oluşturulmazlar.
Bu bölümde kap sınıflarının oluşumuna şöyle bir değinildi. Ama siz bu
konuyu daha ileri götürmelisiniz. Tam bir kap sınıf kütüphanesi, koşut
denetim akışı (multithreading), süreklilik ve artık toplayıcılığını da kapsar.
C/C++
Kütüphaneler
Selami KOÇLU
1.BÖLÜM
Standart C++ kütüphanesi
Standart C++ kütüphanesi standart C kütüphanesinin tamamını içerdiği
gibi, küçük ekleme ve değişikliklerle tip güvenliğini destekleyen kendine
özgü kütüphaneyi de kapsar. Bu yüzden Standart C++ kütüphanesi
standart C kütüphanesinden daha güçlüdür. Kitabımızda Standart C++
kütüphanesini ayrıntıları ile inceleyeceğiz. Aslında Standart C++ için en
iyi kaynak bizzat standardın kendisidir.
Şimdi gelelim kitabımızda ele alacağımız konulara; birinci bölümde
Standart C++ string sınıfını ele alacağız. string sınıfı, metin süreçlerini
geliştirirken kullanılır. Kelime işlemlerinde string'e yoğun olarak rastlanır.
C kodları ile gerçekleştirilen metin işlemleri, C++ da, string sınıfının üye
işlevleri tarafından da rahatlıkla yapılabilir. string sınıfının üye işlevleri;
append( ), assign( ), insert( ), remove( ), resize( ), replace( ), copy(),
find( ), rfind( ), find_first_of( ), find_last_of( ), find_first_not_of( ),
find_last_not_of( ), substr( ), compare( ), at( ), capacity( ), empty( ),
erase( ), length( ), max_size( ), size( ), ve swap( )
dir. Bunlardan başka atama(=), toplama (+=) ve köşeli ayraç işleçleri([]) de
bindirime uğratılarak işlemler gerçeklenebilir. Ayrıca uluslararası
karakterleri desteklemek için "geniş" bir wstring sınıfı tasarlanmıştır. Hem
string hem de wstring (<string> biçiminde bildirilirler, C deki <string.h>
ile karıştırmayın, bu C++ da <cstring> olarak kullanılır), basic_string
ortak kalıp sınıfından oluşturulmuştur. Hemen bir konuyu belirtelim;
string sınıfı iostream'lerle etle tırnak gibi tümleşiktir ve bu yüzden
strstream kullanılmaz.
Bir sonraki bölümde iostream kütüphanesi yer almaktadır.
Tanı kütüphanesi. Buradaki öğeler sayesinde C++ programları hataları
algılar ve raporlar. <exception> başlığı, standart istisna sınıflarını bildirir
ve, <cassert> ise C dilindeki assert.h ile aynı işi görür.
Genel yarara dönük kütüphane. Buradaki öğeler Standart C++
kütüphanesinin diğer kısımlarıdır, ama isterseniz programlarınızda
kullanabilirsiniz. !=, <=, >=, > işleçlerinin kalıp sürümleri bulunur
(kısaltılmış tanımları engellemek için), tuple yapan kalıp işlevini
bulunduran pair kalıp sınıfı, Standart Kalıp İşlevlerini (STL) desteklemek
için işlev nesneleri kümesi, Standart Kalıp İşlevleri ile kullanmak için,
saklama yeri yerleşim işlevleri. Bu sayede belleğe yerleşim
mekanizmalarını kolaylıkla değiştirebilirsiniz.
Yöreleştirme kütüphanesi. Bu kütüphaneleri kullanarak programlarınızı
farklı ülke koşullarına (alfabe, para,sayı, zaman v.d.) uyarlayabilirsiniz.
Kap kütüphanesi. Standart Kalıp Kütüphanesinin içerdiği gibi, <bits> ve
<bitstring> teki bits ve bitstring sınıflarını da kapsar
Yineleyici kütüphane. Standart Kalıp Kütüphane araçları ile beraber akış
ve akış tamponları yer alır.
Algoritmalar kütüphanesi. Bunlar SKK (standart kalıp kütüphanesi)
kaplarında yineleyicileri kullanarak işlem gerçekleştiren kalıp işlevleridir.
Standart C++ kütüphanesi hedef kodların (.o uzantılı) programımızda
kullanılmasına yardım etmektedir. SKK ise doğrudan kaynak kodları
kullandırmaktadır. Bundan dolayı kalıplar üzerine ayrı bir bölüm açıp
konuyu ayrıntıları ile irdeleyeceğiz. Zira kaynak kodları (değiştirerekte
olsa) doğrudan kullanabilmek, aynı tasarım düzenekleri (design patterns)
gibi programcılıkta verimliliği üst düzeye çıkarır. Bunlardan başka kalıplar
ile üst programlama da yapılabilmektedir. Üst programlama C++ da (C
dilinde yok), derleyicinin yorumlayıcı gibi kullanıldığı bir uygulamadır.
Biraz zor bir konu gibi görünse de, sağladığı olanaklar inanılacak gibi
değildir. Üst program; programı programlayan program olup kod işlemler,
uygulamada C++ yeni bir dil gibi davranır. Aslında bu konu bir kitap
kaplayacak kadar girift bir mevzudur. Buradan da anlayacağınız gibi C++
dili ile kalıplı (SKK yolu ile), nesnel yönelim ve yapısal (C dili yolu ile)
paradigmaları uygulayabildiğimiz gibi, derleyici sayesinde üst
programlama da yapılabilmektedir. Bu da C++ dilinin, kullanmayı
bildikten sonra rakipsiz olduğunu göstermektedir. Ama hiçbir zaman
unutulmaması gereken; ışık vermek için yanmalı. Tabii siz.
String'ler
string C dilinde üzerinde en fazla vakit harcanan ve yanlış anlamalara yol
açan konulardan biridir. C string kütüphane işlevleri, ünlü kelime işlem
programlarının vazgeçilmezidirler. C string işlevlerini C++ da kullanmak
için #include <cstring> yazılır. C++ kütüphanesinin string sınıfını C++
da kullanmak için ise #include <string> yazılır. C++ da kullanılan string
sınıfının çok sayıdaki yapıcı işlevi her türlü kelime işlem sorununu
çözebilir. Gerek C dili string işlevleri gerekse C++ dilinin string sınıfı
yapıcı işlevlerini ayrıntıları ile inceleyeceğiz.
Önce string nedir onu açıklayalım; string C dilinde son öğesi sıfır (sıfır
sonlandırıcı olarak adlandırılır) olan karakter dizisidir. Karakter dizileri
harf ve rakamlardan oluşur. C stringleri ile C++ stringleri arasında iki fark
vardır; birincisi, karakter dizisi olan C++ string nesneleri, yönetmek ve
işlemlemek için gereken işlevlere sahip string sınıfı tarafından
oluşturulurlar. Bir string, verilerin saklama alanı ve boyutu hakkında da
bilgiler taşır. Özellikle, C++ string nesnesi bellekteki başlangıç yerini,
içeriğini, karakter uzunluğunu, ve arttırılabilecek uzunluğu bilir. Bu da
ikinci farkı ortaya çıkarır; C++ string nesneleri C string karakter dizileri
gibi sıfır öğe ile sonlanmaz. Genellikle açıklamalarda; C char dizi, C++
string nesne kullanılır. C++ string nesneleri, C char dizilerinden
kaynaklanan hataları en aza indirir. C stringlerinde rastlanan en berbat
hatalar; dizi üzerine yeniden yazma, diziye doğru değer atanmamış veya
ilk değer ataması (başlatması) yapılmamış göstergeçle erişmeye çalışma,
bir dizi bellekten silinirken göstergeçin buraya o sırada yeniden yazım
yapması (sallantıdaki göstergeç). Böyle C hataları, sistemi çökertirler.
C++ dilinde string sınıfının bellek kullanım düzeni tam olarak bilinemez.
Bu daha ziyade derleyici üreticisinin yaklaşımına bağlıdır. Yani bir string
nesne verilerinin nerede hangi koşullarda tutulduğu hususunda, önceden
tam bilgi sahibi olunamaz.
C dilinde string işlevlerini kullanmak için #include <string.h>, karakter
işlevlerini kullanmak için #include <ctype.h>
başlıkta yazılır. C++ dilinde, C dilinin bu işlevlerini kullanmak için
#include <cstring> ve #include <cctype> başlıkları yazılır. Anlaşılacağı
üzere bunlar C dilinden gelen işlevlerdir. Siz C++ programcısı olarak
bunları da kullanabilirsiniz. Ama bizim önerimiz C++ dilinin kendi
standart kütüphanesinde bulunan string sınıf işlevlerinin kullanılmasıdır.
C++ string sınıf işlevlerini kullanmak için başlıkta #include <string>
yazmak yeterlidir. (C++ string sınıf işlevleri, C dilinde kullanılamaz
hatırlatalım).
Şimdi C++ da kullanılabilecek C string ve karakter işlevlerini verelim;
int isalnum(int ch) ; cctype
int isalpha(int ch) ; cctype
int isblank(int ch) ; cctype
int iscntrl(int ch) ; cctype
int isdigit(int ch) ; cctype
int isgraph(int ch) ; cctype
int islower(int ch) ; cctype
int isprint(int ch) ; cctype
int ispunct(int ch) ; cctype
int isspace(int ch) ; cctype
int isupper(int ch) ; cctype
int isxdigit(int ch) ; cctype
void* memchr(const void* buffer, int ch, size_t count) ;
cstring
int memcmp(const void* buf1, const void* buf2, size_t count) ; cstring
void* memcpy(void* to, const void* from, size_t count) ;
cstring
void* memmove(void* to, const void* from, size_t count) ;
cstring
void* memset(void* buf, int ch, size_t count) ;
cstring
char* strcat(char* str1, const char* str2) ;
cstring
char* strchr(const char* str, int ch) ;
cstring
int strcmp(const char* str1, const char* str2) ; cstring
int strcoll(const char* str1, const char* str2) ; cstring
char* strcpy(char* str1, const char* str2) ;
cstring
size_t strcspn(const char* str1, const char* str2) ; cstring
char* strerror(int errnum) ;
cstring
size_t strlen(const char* str) ;
cstring
char* strncat(char* str1, const char* str2, size_t count) ; cstring
int strncmp(const char* str1, const char* str2, size_t count) ; cstring
char* strncpy(char* str1, const char* str2, size_t count) ; cstring
char* strpbrk(const char* str1, const char* str2) ; cstring
char* strrchr(const char* str, int ch) ;
cstring
size_t strspn(const char* str1, const char* str2) ; cstring
char* strstr(const char* str1, const char* str2) ; cstring
char* strtok(char* str1, const char* str2) ;
cstring
size_t strxfrm(char* str1, const char* str2, size_t count) ; cstring
int tolower(int ch) ;
cctype
int toupper(int ch) ;
cctype
Burada cstring ve cctype açıklamalarını, ilgili işlevi C++ dilinde
kullanırken, yazılacak başlık biçimini hatırlatmak için verdik.
Örnek;
#include <cstring>
#include <cctype>
İşlevlerin kullanım ayrıntılarını, C standartlarının bulunduğu herhangi bir
kitapta bulabilirsiniz. Aslında yukarıdaki yazım biçimlerine bakarak birçok
özelliği siz kendiniz de keşfedebilirsiniz. Biraz gayret.
Burada bir konuyu yeri gelmişken belirtelim cstringte yer alan memcpy,
memset gibi bellek yerleşim işlevleri bazen C++ daki new işleci yerine
tercih edilebilmektedir. O nedenle bu tür bellek yerleşim işlevlerinin
ayrıntılarını öğrenmeniz sizin yararınızadır. Burada C ile ilgili daha fazla
ayrıntı vermememizin nedeni ise asıl hedefimizin C++ dili olmasıdır.
C++ standart kütüphane string'leri
Şimdi bu bölümde basic_string< > temel kalıp kütüphanesi ve onun
özelleşmiş standardı string ile wstring'i inceleyeceğiz. Bilindiği gibi bir
sınıfı kullanabilmek için, o sınıfın public arayüzünü iyi bilmek gerekir.
String sınıfının oldukça geniş yöntemler kümesi bulunmaktadır. Bunların
içinde çok sayıda yapıcı işlev, string atamaları için bindirilmiş işleçler,
string birleştiricileri, karşılaştırıcıları ve her öğeye ayrı ayrı erişebilme
yöntemleri ve daha birçokları sayılabilir. Kısaca string sınıfı bir sürü iş
yapar.
string nesnesi oluşturmak
Önce string sınıfı yapıcı işlevlerini inceleyelim. Daha sonra, sınıfın
nesnesi oluşturulurken en önemli şeyi dikkate alalım; sınıfla ilgili
seçenekler. String sınıfına ait 6 adet yapıcı işlev aşağıda ayrıntıları ile
verilmiştir.
String sınıfının yapıcı işlevleri
Yapıcı işlev
Açıklamalar
1-string(const char* s)
string nesnesini SBSS s
göstergeçi ile başlatır
2-string(size_type n, char c)
n tane öğesi olan string nesnesi
oluşturur, herbiri c karakteri ile
başlar.
3-string(const string& str, string string nesnesini str nesnesi ile başsize_type pos=0, size_type n=npos) latır, str deki pos dan başlayarak str
nin sonuna kadar gider veya n tane
karakter kullanır. (hangisi önce ise)
4-string( )
0 büyüklüğündeki varsayılan string
nesnesini oluşturur
5-string(const char* s, size_type n) string nesnesini s nin gösterdiği
SBSS ile başlatır, bu SBSS boyutu
aşılsa bile sürer
6-template<class Iter>
string nesnesini [begin, end) arası
string(Iter begin, Iter end)
değerlerle başlatır. begin ve end
göstergeç gibi davranır, yer belirler.
Değerlere begin dahil ama end değil.
Not; SBSS= sıfır bayt sonlanmalı string, yani C-string demektir.
Bunlardan başka += bindirilmiş işleci stringleri birbirinin arkasına ekler,
bindirilmiş = işleci bir stringi diğerine atar, bindirilmiş << işleci bir string
nesnesini göstertir, ve [] bindirilmiş işleci stringin herhangi bir üyesine
erişimi sağlar. Şimdi yukarıda anlatılan yapıcı işlevler ve işleçlerin
kullanılmasını örnekle gösterelim;
//:B01:string1.cpp
//string sınıfına giriş
#include <iostream>
#include <string>
//yapıcı işlevlerin kullanımı
using namespace std ;
int main( ){
string bir(“Piyango!”) ; //1 numaralı yapıcı işlev
cout<<bir<<endl ;
//bindirime uğramış <<
string iki(10, '$') ;
//2. yapıcı işlev kullanımı
cout<<iki<<endl ;
string uc(bir) ;
//3. yapıcı işlev kullanımı
cout<<uc<<endl ;
bir+=”Vayy!” ;
//+= bindirimi
cout<<bir<<endl ;
iki=”Kusura bakma!” ;
uc[0]='S' ;
string dort ;
//4. yapıcı işlev kullanımı
dort=iki+uc ;
//+ ve = bindirilmiş
cout<<dort<<endl ;
char hepsi[ ]=”iyi biten hersey iyidir” ;
string bes(hepsi, 16) ;
//5. yapıcı işlev kullanımı
cout<<bes<<”!\n” ;
string alti(hepsi+17, hepsi+20) ; //6. yapıcı işlev kullanımı
cout<<alti<<”,” ;
string yedi(&bes[17], &bes[20]) ; //yine 6. yapıcı işlev
cout<<yedi<<”...\n” ;
}///:~
Programın çıktısı aşağıdaki gibidir;
Piyango
$$$$$$$$$$
Piyango!
Piyango! Vayy!
Kusura bakma! Siyango!
iyi biten hersey
iyi, iyi
string ilk değer atama (=başlatma) sınırlamaları
stringler tek karakter, ASCII veya integer bir değerle başlatılamazlar.
Örnek;
string str1(“a”) ;
string str2(0x40) ;
//olamaz, hata zira tek karakter
//olamaz, hata zira integer
Bu başlatma koşulu atama ve kopya yapıcı işlevle yapılan ilk değer
atamaları içinde geçerlidir.
string sınıf girdileri
string sınıfı ile ilgili başka önemli bir konuda, girdilerle ilgili
seçeneklerdir. C biçimi string çağrımları için üç seçenek bulunur;
char bilgi[100] ;
cin>>bilgi ;
//bir kelime oku
cin.getline(bilgi, 100) ; //bir satır oku, gözardı et\n
cin.get(bilgi, 100) ;
//bir satır oku, kuyrukta bırak\n
string nesnesinin girdi seçenekleri neler?. String sınıfı önce >> işlecini
bindirime uğratır. İlk nesne (cin) string nesnesi olmadığı için, >> işleci
string sınıfı yöntemi değildir. Onun yerine, istream nesnesini (cin) ilk
değişkeni olarak, string nesnesini de ikinci değişkeni olarak alan genel bir
işlevdir. Bundan başka, C deki >> işlecinin stringlerle kullanımında tutarlı
olmak adına, C++ stringleri de; tek kelime okur, boşluğa rastladığında
girişi sonlandırır, dosya sonunu algılar, veya string nesnesine saklanabilen
en fazla sayıdaki karaktere ulaşabilir. İşlev öncelikle hedef stringin
içeriğini siler ve daha sonra her seferinde bir karakter olmak üzere okur ve
yazar. String nesnesine izin verilen karakter sayısından daha az sayıda
giriş yapılırsa, operator>>(istream&, string&) işlevi string nesnesinin
boyutunu otomatik olarak girişe uydurur. Özet olarak şunu söyleyebiliriz;
>> işlecini C biçiminde olduğu gibi C++ da da aynen kullanabiliriz. Ayrıca
dizi boyutunun aşılacağı hususunda korkmamız da gerekmez.
char dosya_ad[10] ;
cin>>dosya_ad ;
string lad ;
cin>>lad ;
//9 karakterden büyük olduğunda sorun çıkar
//çok çok uzun kelimeleri okuyabilir
getline( ) işleçle gösterilemediği için getline( ) işlevinin eşdeğerini
koymak (operator>>( ) işlevinden farklı) biraz daha az şeffaftır, onun
yerine üyelik gösterimi kullanılır.
cin.getline(dosya_ad, 10) ;
String nesnesini benzer biçimde yazabilmek için de istream sınıfına yeni
bir üye işlev eklenmesi gerekir. Tabii bu akıllıca olmazdı. Onun yerine
string kütüphanesinde üye olmayan getline( ) işlevi tanımlanır. Bu işlev
ilk değişken olarak istream nesnesi, ikinci değişken olarakta string
nesnesi alır. Böylece girişten string nesnesine bir satır okumak için ;
string tam_ad ;
getline(cin, tam_ad) ; //cin.getline(dosya_ad, 10) yerine
Burada getline( ) işlevi önce varış stringini siler daha sonra giriş
sırasından her seferinde bir karakter okur ve bunu stringe ekler. Bu işlem,
işlev satır sonuna gelene kadar, dosya sonu algılanana kadar, veya string
nesnenin kapasite sınırına kadar sürer. Eğer yeni satır (\n) karakteri
algılanırsa, bu karakter okunur fakat string nesnesine eklenmez. Burada
hemen bir şeyi belirtelim; istream sürümüne benzemeyen biçimde string
sürümde, okunacak karakterlerin maksimum sayısını veren boyut
değişkeni bulunmaz. Bunun nedeni; string nesnesinin boyutu otomatik
olarak ayarlamasıdır. Aşağıdaki örnek iki türlü giriş seçeneğini
göstermektedir;
//:B02: String2.cpp
//string girişleri
#include <iostream>
#include <string>
using namespace std ;
int main( ){
string kelime ;
cout<<”Bir satir gir: ” ;
cin>>kelime ;
while(cin.get( )!='\n')
continue
cout<<kelime<<”hepsi istenilenmi. \n” ;
string satir ;
cout<<”tam bir satir gir:” ;
getline(cin, satir) ;
cout<<”Satir: “<<satir<<endl ;
}///:~
Bu program parçasının çıktısı ;
Bir satir gir: Paraların tamamı onundu
Paraların hepsi istenilenmi
tam bir satir gir: bu örnek programı herkes begenirdi degilmi
Satir: bu örnek programı herkes begenirdi degilmi
Stringlerle öbür işlemler
Buraya kadar string nesnelerini oluşturmanın değişik yollarını öğrendik.
String nesnesinin içeriğini göstermek, verileri string nesnesine okumak,
string nesnesine ekleme yapmak, string nesnesine atama yapmak, ve
string nesnelerini birbirine eklemek bunların arasındadır. Başka neler
yapılabilir?.
Stringleri karşılaştırabilirsiniz. Altı tane olan ilişkinlik işleçlerinin tamamı,
string nesnelerince bindirime uğratılır. Bir string nesnesi diğerinden
makine metin karşılaştırmasında daha önce yer alırsa, ilki diğerinden daha
küçük kabul edilir. Makine metin karşılaştırma sırası ASCII kod ise,
bunun anlamı; sayılar büyük karakterlerden daha küçük, büyük
karakterlerde küçük karakterlerden küçük demektir. Her ilişkinlik işleci üç
türlü bindirime uğrayabilir; ya bir string nesnesini başka bir string nesnesi
ile karşılaştırırsınız, ya bir string nesnesini C benzeri string ile
karşılaştırırsınız, ya da C benzeri stringi bir string nesnesi ile
karşılaştırırsınız.
string snake1(“cobra”) ;
string snake2(“coral”) ;
char snake3[20]=”anaconda” ;
if(snake1<snake2)
;
//operator<(const string&, const string&)
.....
if(snake1==snake3)
;
//operator==(const string&, const char*)
.....
if(snake3!=snake2)
;
//operator!=(const char*, const string&)
String büyüklüğü de saptanabilir. Stringte bulunan karakterlerin sayısı
size( ) ve length( ) üye işlevleri bunu gerçekler.
if(snake1.length( )==snake2.size( ))
cout<<”her iki string aynı boyda”<<endl ;
Gerek size( ) gerekse length( ) işlevleri aynı görevi yapmasına rağmen
neden ikisi de var. Bunun nedeni; length( ) işlevinin string sınıfının ilk
sürümlerinde bulunması, size( ) işlevinin standart kalıp kütüphanesi
uyumluluğu sağlamak için oluşturulmasıdır.
Bir string içinde alt stringler veya karakterleri değişik yollarla
arayabilirsiniz. find( ) yönteminin dört değişik kullanım yani bindirim yolu
aşağıda gösterildi;
find( ) işlevinin bindirilmiş hali
Yöntem öntipi
Açıklaması
size_type find(const string& str,
size_type pos=0)const
Birinci str altstringini pos noktasından aramaya başlayarak
bulur.Bulduğu zaman
altstring ilk karakter sırasını
geri döndürür, aksi taktirde
string::npos geri döner.
size_type find(const char* s,
size_type pos=0)const
Birinci s altstringini pos noktasından aramaya başlayarak bulur. Bulduğu zaman altstring
ilk karakter sırasını geri döndürür, aksi taktirde
string::npos geri döner.
size_type find(const char* s,
size_type pos=0, size_type n)const
Birinci s altstringinin ilk n karakterini pos noktasından aramaya başlayarak bulur.
Bulduğunda altstring ilk karakter sırasını geri döndürür.
Aksi taktirde string::npos geri
döner.
size_type find(char ch,
size_type pos=0)const
Birinci ch karakterini pos noktasından aramaya başlayarak bulur. Bulduğunda karakter sırası
geri döndürülür. Aksi durumda
string::npos geri döner.
Kütüphanede bunlardan başka, bindirilmiş find( ) yönteminin aynı
künyede (ayraçlar arasındakiler) benzer yöntemler bulunur; rfind( ),
find_first_of( ), find_last_of( ), find_first_not_of( ), find_last_not_of( ).
rfind( ) yöntemi altstring veya karakterin son kez oluşumunu bulur.
find_first_of( ) değişkende yer alan karakterlerden herhangi birinin
stringte ilk kez bulunduğu sırayı verir. Örnek;
int nerede=snake1.find_first_of(“mark”) ;
“cobra” da yer alan “r” harfinin sırasını bulur.(“cobra” nın üçüncü harfi
“r”). Bunun nedeni “mark” karakterlerinden “cobra” da ilk rastlanan
karakterin “r” olmasıdır. find_last_of( ) yöntemi de benzer fakat ters
biçimde çalışır. Yani sonuncu benzer karakterin olduğu sırayı verir.
int nerede=snake1.find_last_of(“mark”) ;
“mark” ın “cobra” da bulunan son karakteri “a” dır. “a” karakteri
“cobra” nın dördüncü karakteridir ve yöntem bu sıra değerini geri
döndürür. find_first_not_of( ) yöntemi ise değişkende olmayan stringin
ilk karakterini verir.
int nerede=snake1.find_first_not_of(“mark”) ;
“cobra” da “mark” ta bulunmayan ilk karakter “c” dir. “c” nin “cobra”
daki yeri birinci sıradır, yani bir (1) döndürülür. find_last_not_of( )
yöntemi ise değişkende olmayan stringin son karakter yerini verir.
int nerede=snake1.find_last_not_of(“mark”) ;
“cobra” da “mark” ta bulunmayan son karakter “b” dir. “b” nin “cobra”
daki yeri üçüncü sıradır, yani üç (3) geri döndürür.
Daha çok sayıda yöntem bulunmaktadır, string yöntemlerinin adları
kitabın başlangıcında ayrıntılara girmeden verilmişti. Bazılarının
ayrıntılarını verelim;
//string işlemleri:
const charT* c_str( ) const;
const charT* data( ) const;
allocator_type get_allocator( ) const ;
//string karşılaştırmaları:
int compare(const basic_string& str) const;
int compare(size_type pos1, size_type n1, const basic_string& str)
const;
int compare(size_type pos1, size_type n1, const basic_string& str,
size_type pos2, size_type n2) const;
int compare(const charT* s) const;
int compare(size_type pos1, size_type n1, const charT* s) const;
int compare(size_type pos1, size_type n1, const charT* s, size_type n2)
const;
basic_string& append(const basic_string& str);
basic_string& append(const basic_string& str, size_type pos, size_type
n);
basic_string& append(const charT* s, size_type n);
basic_string& append(const charT* s);
basic_string& append(size_type n, charT c);
template<class InputIter>
basic_string& append(InputIter first, InputIter last);
void push_back(charT c);
basic_string& assign(const basic_string& str);
basic_string& assign(const basic_string& str, size_type pos, size_type
n);
basic_string& assign(const charT* s, size_type n);
basic_string& assign(const charT* s);
basic_string& assign(size_type n, charT c);
template<class InputIter>
basic_string& assign(InputIter first, InputIter last);
basic_string& insert(size_type pos1, const basic_string& str);
basic_string& insert(size_type pos1, const basic_string& str, size_type
pos2,size_type n);
basic_string& insert(size_type pos, const charT* s, size_type n);
basic_string& insert(size_type pos, const charT* s);
basic_string& insert(size_type pos, size_type n, charT c);
iterator insert(iterator p, charT c);
void insert(iterator p, size_type n, charT c);
template<class InputIter>
void insert(iterator p, InputIter first, InputIter last);
basic_string& erase(size_type pos = 0, size_type n = npos);
iterator erase(iterator position);
iterator erase(iterator first, iterator last);
basic_string& replace(size_type pos1, size_type n1, const
basic_string& str);
basic_string& replace(size_type pos1, size_type n1, const
basic_string& str, size_type pos2, size_type n2) ;
basic_string& replace(size_type pos, size_type n1, const charT* s,
size_type n2) ;
basic_string& replace(size_type pos, size_type n1, const charT* s) ;
basic_string& replace(size_type pos, size_type n1, size_type n2, charT
c) ;
basic_string& replace(iterator i1, iterator i2, const basic_string& str) ;
basic_string& replace(iterator i1, iterator i2, const charT* s, size_type
n) ;
basic_string& replace(iterator i1, iterator i2, const charT* s) ;
basic_string& replace(iterator i1, iterator i2, size_type n, charT c) ;
template <class InputIterator>
basic_string& replace(iterator i1, iterator i2, InputIterator j1,
InputIterator j2) ;
size_type copy(charT* s, size_type n, size_type pos=0) const ;void swap
(basic_string& str) ;
String karakter karşılaştırmaları ile ilgili örneklerin bir kısmı ilk ciltte
verilmişti. Dilerseniz bakabilirsiniz.
Bindirilmiş ilişkinlik işleçleri, stringlerin sayılar gibi işlenmesini sağlar.
While(tahminler>0 && atis!=hedef)
Bu, C stringlerindeki strcmp( ) işlevinin kullanılmasından daha kolaydır.
Hemen bir şey daha belirtelim; npos string sınıfının static üyesidir. npos
değeri string sınıf nesnesinde bulunan karakterlerin maksimum sayısıdır.
Dizin sıfırdan başladığı için, olası en büyük dizinin 1 (bir) büyüğüdür.
Bundan dolayı bir karakter veya stringi ararken hata göstermede
kullanılabilir.
string nesne kullanımında C işlev bindirimi
string nesnelerini karşılaştırırken bindirilmiş == işleci kullanılır. Fakat
büyük harf küçük harf ayırma özelliği nedeni ile, bu işleç bazı durumlarda
sorunlara neden olur. Yani büyük harf küçük harf ayrımı istenmediği zaman
başka çözüm bulunması gerekir. Durumu bir örnekle açıklayalım;
#include <string> //string nesnesi için
...
string strA ;
cin>>strA ;
//kullanıcı selami adını girsin
string strB=”Selami” ; //belleğe konan sabit
if(strA==strB){
cout<<”stringler ayni.\n” ;}
else{
cout<<”stringler ayni degil.\n” ;}
selami'deki s ile Selami'deki S arasındaki farktan dolayı çıktı;
stringler ayni degil
olur. Peki biz büyük harf küçük harf farkını gözardı etmek istersek ne
yapmalıyız. Çok sayıda C kütüphanesinde, stricmp( ) ve _stricmp( )
büyük küçük harfleri ayırmayan işlevler bulunur. Burada bir hatırlatma
yapalım; bu işlevler C standardında bulunmamaktadır. O yüzden bu
işlevlerin size ait bindirilmiş sürümlerini yazmanız, ve onları programınıza
eklemeniz gerekebilir. Şimdi yukarıdaki örneğin benzerini büyük harf
küçük harf farkını gözardı ederek yazalım;
#include <cstring>
//stricmp( ) işlevi için
#include <string>
//string nesneleri için
...
string strA ;
cin>>strA ;
//kullanıcının selami girdiğini kabul edelim
string strB=”Selami” ; //bellekteki sabit
inline bool stricmp(const std::string& strA,
const std::string& strB){
//bindirilmiş işlev
return stricmp(strA.c_str( ), strB.c_str( ))==0 ; //C işlevi
}
bool bStringsAreEqual=stricmp(strA, strB) ;
Yukarıdaki basit yazım biçimi ile harf büyüklüklerini gözardı ederek,
string özdeşliklerini sınayabilirsiniz.
Dizin yeri belli öğeyi, [] ve at( ) işlevi ile bulmak
C biçimi yazımda, dizide herhangi bir yerdeki karakteri bulabilmek için, []
köşeli ayraçlar kullanılmaktaydı. C++ string sınıfı sayesinde at( ) işlevi ile
aynı işlem kolayca gerçeklenebilir. Örnek;
string s(“abcdef”) ;
cout<<s[2]<<” “ ;
cout<<s.at(2)<<endl ;
çıktı;
cc
Evet, burada her iki kullanım aynı gibi gözükmesine rağmen, aslında [] ile
at( ) işlevi arasında büyük bir fark bulunmaktadır. at( ) işlevi dizi
sınırlarının dışında bir öğeye erişilmek istendiği zaman istisna
fırlatılmasını sağlar, [] (köşeli ayraçları) ise sizi bu durumda bir başınıza
bırakır. Örnek;
string s(“abcdef”) ;
s[6] ;
//çalışma sırasında hataya yolaçar, zira dizi dışı
try{
//istisna fırlatma denemesi yapabilirsiniz.
s.at(6) ;
}catch(...){
cerr<<”dizi sınırları dışına çıkıldı”<<endl ;}
Programcılıkta istisnalar program kurgusunun dışında kalan değerleri
içerirler. Bu değerler aslında istenmeyenler olup, kullanılmaya kalkıldığı
zaman programın dengesini alt üst edecek sonuçlara yolaçabilirler. O
nedenle istisna yönetimi birinci kitapta da belirttiğimiz gibi önemli bir
konudur. Burada da at( ) gibi istisna fırlatmayı sağlayacak işlevi, [] yerine
kullanmak oldukça yararlıdır. İstisnai durum ortaya çıktığı zaman, istisna
yöneticisi, programın düzenini sürdürmek için, sonraki eylemleri ona göre
düzenler. Yani siz programınızı ona göre geliştirirsiniz.
Şimdi string yöntemlerinin kullanıldığı eğlenceli bir program yazalım.
Programımız adam asma oyunun grafik uygulamasıdır. Program beş harfli
kelimelerden oluşan string nesneler dizisine sahiptir, ve gelişigüzel olarak
bunların içinden birini seçer. Sizde bu kelimeyi tahmin etmeye çalışırsınız.
Kelimeyi harf tahminleri ile bulacaksınız.
//:B01:string3.cpp
//string kullanılan eğlenceli bir adam asma programı
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cctype>
using namespace std ;
const int NUM=10 ;
const string kelime_list[NUM]={“izmir”, “bursa”, “sinop”, “siirt”,
“sivas”, “corum”, “kilis”, “tokat”, “mugla”, “konya”};
int main( ){
srand(time(0)) ;
char oyna ;
cout<<”kelime oyunu oynayalim?<y/n>” ;
cin>>oyna ;
oyna=tolower(oyna) ;
while(oyna=='y'){
string hedef=kelime_list[rand( ) % NUM] ;
int length=hedef.length( ) ;
string attempt(length, '-') ;
string badchars ;
int tahminler=5 ;
cout<<”gizli kelimeyi tahmin et. Uzunluk”<<length
<<”tahmin edilen harfler\n”
<<”her seferinde bir harf ile elindeki”<<tahminler
<<”tahminde hata .\n” ;
cout<<”kelimeniz:
“<<attempt<<endl ;
while(tahminler>0 && attempt!=hedef){
char harf ;
cout<<”bir harf tahmin et: “ ;
cin>>harf ;
if(badchars.find(harf) != string::npos
||attempt.find(harf) != string::npos){
cout<<”tahmin ettin, bir daha denermisin?.\n” ;
continue ;
}
int loc=hedef.find(harf) ;
if(loc==string::npos){
cout<<”yanlis tahmin!\n” ;
--tahminler ;
badchars+=harf ; //stringe ekle
}
else{
cout<<”dogru tahmin !\n” ;
attempt[loc]=harf ;
loc=hedef.find(harf, loc+1) ; //harf yine belirirse
while(loc!=string::npos){
attempt[loc]=harf ;
loc=hedef.find(harf, loc+1) ;
}
}
cout<<”kelimeniz: “<<attempt<<endl ;
if(attempt!=hedef){
if(badchars.length( )>0)
cout<<”berbat secimler: “<<badchars<<endl ;
cout<<tahminler<<”berbat tahminler kaldi\n” ;
}
}
if(tahminler>0)
cout<<”tamam dogru.! \n” ;
else
cout<<”kusura bakma kelime: “<<hedef<<”.\n” ;
cout<<”bir daha denermisin?.<y/n>” ;
cin>>oyna ;
oyna=tolower(oyna) ;
}
cout<<”elveda\n” ;
return 0 ;
}//:~
Programı Linux'ta derlediğinizde konsolda adam asma oynayabilirsiniz.
Başka neler var
String kütüphanesinde bulunan işlevler, başka deyimle yöntemler
kümesinin sağladığı kolaylıklar, yukarıdakilerle sınırlı değildir. Stringin
bir kısmını veya tamamını silen, bir string ile diğer bir stringin bir
parçasını veya tamamını yerdeğiştiren, stringe başka unsurlar ekleyen
veya silen, bir string ile başka bir stringin bir parçasını veya tamamını
karşılaştıran, bir stringten bir alt stringi bulup çıkaran işlevler
bulunmaktadır. Bir işlevde, bir stringin bir parçasını diğer bir stringe
kopyalamaktadır. Ayrıca string içeriklerini değiş tokuş eden bir işlevde
bulunmaktadır. Bu işlevler string nesneleri ile çalıştıkları kadar, bindirime
uğratılarak C benzeri stringlerle de, aynı biçimde çalışırlar. Burada birçok
kısımda, stringleri char tiplermiş gibi ele alarak inceledik. Aslında daha
önce de belirttiğimiz gibi string sınıfı gerçekte kalıp (template) sınıfına
yaslanarak geliştirilmiştir.
template<class charT, class traits=char_traits<charT>,
class Allocator=allocator<charT>>
basic_string{....} ;
Sınıfta aşağıdaki iki typedef bulunur;
typedef basic_string<char> string ;
typedef basic_string<wchar_t> wstring ;
Son satır wchar_t tabanlı stringlerin kullanımını izin verir. Hatta siz belli
koşulları yerine getirip karakter benzeri unsurlar için bir sınıf geliştirebilir
ve basic_string kalıp sınıfını onunla kullanabilirsiniz. traits sınıfı seçilen
karakterlerin öne çıkan özelliklerini tanımlayan bir sınıftır, örnek olarak
değerlerin karşılaştırma biçimini açıklaması gibi. char ve wchar_t tipleri
için, char_traits kalıbının önceden tanımlanmış özellikleri bulunur. Ve
bunlar traits kalıbının varsayılanlarıdır. Allocator sınıfı, bellek
yerleşimini yöneten sınıftır. char ve wchar_t tipleri için allocator
kalıbının önceden tanımlanmış özellikleri vardır. Bunlar da allocator
varsayılanlarıdır. Genellikle new ve delete işleçleri bellek yerleşiminde
kullanılır. Ama isterseniz belleğin bir yerini kendi bellek yönetim
biçimleriniz için ayırıp kullanabilirsiniz. Bu da sizin yeteneklerinize bağlı.
Aslında herşey sizin paradigmaları kavrayıp onları uygulama becerilerinize
kalmıştır. Burada kurallar anlatılır birkaç örnekle uygulama gösterilir, geri
kalan sahneleri ve oyunları siz oynamalısınız. Programcılık kuralları ilgili
dil tarafından konmuş zeka sanatıdır. Notaları herkes öğrenebilir ama,
tutulan besteyi herkes yapamaz.
ÖRNEKLER
1- Doğum tarihlerini okuyup onaylayan bir program örneği
//B:dogum tarihlerini okuyup onaylama
#include <iostream>
#include <string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
int
int
int
int
valid_input(int lower, int upper, const string& description);
year();
month();
date(int month_value, int year_value);
int main() {
cout << "Enter your date of birth." << endl;
int date_year = year();
int date_month = month();
int date_day = date(date_month, date_year);
string months[] = {"January", "February", "March",
"April",
"May",
"June",
"July",
"August",
"September", "October", "November",
"December"
};
string ending = "th";
if(date_day == 1 || date_day == 21 || date_day == 31)
ending = "st";
else if(date_day ==2 || date_day == 22)
ending = "nd";
else if(date_day == 3 || date_day == 23)
ending = "rd";
cout <<
<<
<<
<<
}
endl
"We have established that your were born on "
months[date_month-1] << " " << date_day << ending
", " << date_year << "." << endl;
return 0;
// Upper ve lower arasında bir int değer okur
int valid_input(int lower, int upper, const string& description) {
int data = 0;
cout << "Please enter " << description
<< " from " << lower << " to " << upper << ": ";
cin >> data;
while(data<lower || data>upper) {
cout << "Invalid entry; please re-enter " << description << ": ";
cin >> data;
}
return data;
}
// Yılı okur
int year() {
const int low_year = 1850; // 155 yaşından büyük insan olmaz
const int high_year = 2000;
// 5 yaşından küçük olmaz...
return valid_input(low_year, high_year, "a year");
}
// Ayı okur
int month() {
const int low_month = 1;
const int high_month = 12;
return valid_input(low_month, high_month, "a month number");
}
// Verilen ay ve yıldaki tarihi okur
int date(int month_number, int year) {
const int date

Benzer belgeler