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