1 Sınıf Tasarımında Dosya Organizasyonu Genellikle bir
Transkript
1 Sınıf Tasarımında Dosya Organizasyonu Genellikle bir
20-03-2002 Sınıf Tasarımında Dosya Organizasyonu Genellikle bir sınıfın fiziksel organizasyonu iki dosya halinde yapılır. Sınıfın ismi X olmak üzere X.h ve X.cpp dosyaları. X.h dosyasının içerisine nesne yaratmayan bildirim işlemleri yerleştirilir. Yani X.h dosyasının içeriği şunlar olabilir: - Sınıf bildirimi Sembolik sabit tanımlamaları typedef ve enum bildirimleri Konuyla ilgili çeşitli global fonksiyonların prototipleri inline fonksiyon tanımlamaları Global const değişken tanımlamaları X.cpp dosyası içerisine sınıfın üye fonksiyonlarının tanımlamaları, sınıf ile ilgili global fonksiyonların tanımlamaları yerleştirilir. *.h ve *.cpp dosyalarının birbirlerinden ayrılması kütüphane oluşturma işlemi için zorunludur. *.h dosyası hem *.cpp dosyasından hem de *.cpp dosyası kütüphaneye yerleştirildikten sonra bu sınıfın kullanılması için dışarıdan include edilir. Dosyaların başına bir açıklama bloğu yerleştirilmelidir. Bu açıklama bloğunda dosyanın ismi, kodlayan kişinin ismi, son güncelleme tarihi, dosyanın içindekilerinin ne olduğu ve copyright bilgileri bulunabilir. Örnek bir açıklama bloğu şöyle olabilir: /*---------------------------------------------------------File Name : X.h/X.cpp Author : Kaan Aslan Last Update : 20/03/02 This is a sample header/implementation file. Copyleft C and System Programmers Assosiation (1993) All rights free ------------------------------------------------------------*/ *.h dosyalarının büyük projelerde isteyerek ya da istemeyerek birden fazla include edilmesinde problem oluşturmaması için include koruması (include guard) uygulanması gerekir. Include koruması sayesinde önişlemci dosyayı bir kez gördüğünde içeriğini derleme modülüne verir ancak ikinci gördüğünde vermez. Tipik bir include koruması şöyle oluşturulur: #ifndef _X_H_ #define _X_H_ <Dosya içeriği> #endif Buradaki sembolik sabit ismi dosya isminden hareketle oluşturulmuş herhangi bir isimdir. 1 Bir başlık dosyasının birden fazla include edilmesi genellikle zorunluluk nedeniyle oluşur. Örneğin programcı bir sınıf için A.h başka bir sınıf için ise B.h dosyalarının include etmiş olsun. Bu dosyaların kendi içlerinde general.h isimli temel bir dosya include edilmiş olsun. Böyle bir durumda general.h dosyası iki kez include edilmiş gibi gözükür. general.h dosyası içerisinde include koruması uygulandığından problem oluşmayacaktır. Ancak önişlemci işlemlerini hızlandırmak için ortak başlık dosyalarının ek bir korumayla include edilmesi özellikle çok büyük projelerde önerilmektedir. Ek include koruması şöyle yapılabilir: #ifndef _GENERAL_H_ #include “general.h” #endif Burada A.h içerisinde önişlemci ek koruma ya da include korumasına takılmaz, ancak B.h içerisinde ek korumaya takılır, dolayısıyla daha general.h dosyasını açmadan dosya önişlemci dışı bırakılır. Dosyanın açıldıktan sonra önişlemci dışı bırakılmasıyla açılmadan önişlemci dışı bırakılması arasında büyük projelerde bir hız farkı oluşabilmektedir. Bazen özellikle çok küçük sınıflar için ayrı ayrı *.h ve *.cpp dosyaları oluşturmak yerine bunlar guruplanıp bir kaçı için bir *.h ve *.cpp dosyası oluşturulabilir. Pek çok geliştirme ortamı (örneğin VisualC) bir sınıf ismi verildiğinde bu düzenleme işlemini otomatik olarak yapmaktadır. Örneğin VC6.0’da Insert / NewClass seçildiğinde programcıdan sınıf ismi istenir ve otomatik şu işlemler yapılır: - Sınıf bildirimini başlangıç ve bitiş fonksiyonu olacak biçimde *.h dosyası içerisinde yapar *.h dosyasına bir include koruması yerleştirir *.cpp dosyasını oluşturur, *.h dosyasını buradan include eder *.cpp dosyasında başlangıç ve bitiş fonksiyonlarını içi boş olarak yazar *.cpp dosyasını proje dosyasına ekler Geliştirme ortamı gereksiz kodlama yükünü belli ölçüde programcının üzerinden almaktadır. Projenin Disk Üzerindeki Organizasyonu Proje geliştirirken disk üzerinde proje için bir dizin oluşturulmalı ve düzenli bir çalışma sağlanmalıdır. Gurup halinde proje geliştirirken düzenin sağlanması için çeşitli düzen sağlayıcı programlar kullanılabilmektedir. Projenin dizin yapısı duruma uygun her hangi bir biçimde seçilebilir. Örneğin, projenin kaynak kodları SRC alt dizininde, dokümantasyon bilgileri DOC alt dizininde, object modüller ve çalışabilen programlar BIN alt dizininde, projenin kullandığı kütüphaneler LIB alt dizininde, deneme kodları SAMPLE alt dizininde bulunabilir. Projenin başlık dosyaları bir araya getirilip tek bir başlık dosyası biçimine dönüştürülebilir. Yani, programcı tek bir başlık dosyası include eder fakat o başlık dosyasının içerisinde pek çok başlık dosyası include edilmiştir. 2 Değişkenlerin İsimlendirmesi Değişken isimlendirilmesi konusunda programcı tutarlı bir yöntem izlemelidir. Örneğin Windows ortamında macar notasyonu denilen isimlendirme tekniği yoğun olarak kullanılmaktadır. Macar notasyonu C++’a özgü bir isimlendirme tekniği değildir. C ve yapısal programlama dilleri için düşünülmüştür. İster macar notasyonu kullanılsın ister başka bir notasyon kullanılsın C++ için şu konularda tutarlılık sağlanmalıdır: - - - Sınıf isimleri her sözcüğün ilk harfi büyük olacak şekilde ya da tutarlı başka bir yöntemle belirlenmelidir. C++’da yapılarda bir sınıf olduğu için yapılarla sınıflar aynı biçimde isimlendirilebilir. Yapıların tamamen C’deki gibi kullanıldığı durumlarda yapı isimleri her harfi büyük olacak biçimde belirlenebilir. Bazı kütüphanelerde sınıf isimlerinin başına özel bir karakter de konulabilmektedir. Örneğin MFC’de sınıf isimlerinin başına ‘C’ getirilmektedir (CWnd, CBrush, CObject gibi ...). Sınıfın veri elemanları üye fonksiyonlar içerisinde kolay teşhis edilsin diye ayrı bir biçimde isimlendirilmelidir. Genellikle veri elemanları, başına ya da sonuna ‘_’ konularak ya da ‘d_’, ‘m_’ gibi önekler ile başlatılır. Global değişkenler de özel bir biçimde isimlendirilmelidir. Pek çok programcı global değişkenleri ‘g_’ öneki ile başlatarak isimlendirmektedir. Üye fonksiyonlar içerisinde global fonksiyonlar çağırılırken vurgulama için unary :: operatörü kullanılmalıdır. Örneğin: ::SetData(100); Sınıfların İçsel Yerleşim Organizasyonu Sınıfın bölümleri yukarıdan aşağıya doğru public, protected, private sırasıyla yazılmalıdır. Çünkü en fazla kişinin ilgileneceği bölümün sınıfın hemen başında olması daha açıklayıcı bir durumdur. Bir sınıf tür bildirimlerine, üye fonksiyon bildirimlerine ve veri eleman bildirimlerine sahip olabilir ve her bildirim grubunun üç bölümü olabilir. Düzenli bir çalışma için önce veri eleman bildirimleri sonra üye fonksiyon bildirimleri sonra da veri eleman bildirimleri her bir gurup public, protected, private sırasıyla yazılmalıdır. Örneğin: class Sample { public: typedef int SYSID; public: Sample(); ... ... private: void SetItem(SYSID id); ... ... public: int m_a; protected: 3 int m_b; private: int m_c, m_d; }; Sınıfın kullanıcı için dokümantasyonu yapılırken public ve protected bölümleri tam olarak açıklanmalıdır. public bölüm herkes için protected bölüm sınıftan türetme yapacak kişiler için ilgi çekicidir. Ancak private bölüme kimse tarafından erişilemez bu yüzden dokümantasyonunun yapılmasına gerek yoktur. Zaten tasarımda private bölüm daha sonra istenildiği gibi değiştirilebilecek bölümü temsil eder. Üye fonksiyonların *.cpp dosyasında bildirimdeki sırada tanımlanması iyi bir tekniktir. Sınıfın Üye Fonksiyonlarının Guruplandırılması Sınıfın üye fonksiyonları da çeşitli biçimlerde guruplandırılarak alt alta yazılabilir. Bir sınıf genellikle aşağıdaki guruplara ilişkin üye fonksiyon içerir: 1) Başlangıç ve bitiş fonksiyonları: Bu fonksiyonlar sınıfın çeşitli parametre yapısındaki başlangıç fonksiyonlarıdır. Sınıf bitiş fonksiyonu içerebilir ya da içermeyebilir. 2) Sınıfın veri elemanlarının değerlerini alan fonksiyonlar (get fonksiyonları): Bu fonksiyonlar sınıfın korunmuş private ya da protected bölümündeki veri elemanlarının değerlerini alan fonksiyonlardır. Bu fonksiyonlar genellikle çok küçük olur bu nedenle genellikle inline olarak yazılırlar. Örneğin, Date isimli sınıfın gün, ay ve yıl değerlerini tutan üç private veri elemanı olabilir ve bu değerleri alan GetDay(), GetMonth() ve GetYear() get fonksiyonları olabilir. 3) Sınıfın veri elemanlarına değer yerleştiren fonksiyonlar (set fonksiyonları): Bu tür fonksiyonlar sınıfın private ve protected veri elemanlarına değer atarlar. Bir veri elemanının değerini hem alan hem de yerleştiren üye fonksiyon tanımlamak mümkündür. Yapılacak şey geri dönüş değerini referans almak ve return ifadesiyle o veri elemanına geri dönmektir. class Sample { public: int &GetSetA(); private: int m_a; }; int &Sample::GetSetA() { return m_a; } Sample x; int y; x.GetSetA() = 100; y = x.GetSetA() + 100; Ancak böyle bir tasarımdan özel durumlar yoksa kaçınmak gerekir. 4 4) Sınıfın durumu hakkında istatistiksel bilgi veren fonksiyonlar: Bu tür fonksiyonlar sınıfın ilgili olduğu konu hakkında istatistiksel bilgi verirler. Örneğin, Circle sınıfındaki GetArea() fonksiyonu gibi ya da bağlı listedeki eleman sayısını veren GetCount() fonksiyonu gibi. 5) Giriş çıkış fonksiyonları: Ekran, klavye ve dosya işlemlerini yapan fonksiyonlardır. 6) Operatör fonksiyonları: Bunlar okunabilirliği kolaylaştırmak amacıyla sınıfa yerleştirilmiş olan operatörle çağrışımsal bir ilgisi olan işlemleri yapan fonksiyonlardır. 7) Önemli işlevleri olan ana fonksiyonlar: Bu fonksiyonlar sınıf ile ilgili önemli işlemleri yapan genel fonksiyonlardır. 8) Sanal fonksiyonlar: Çok biçimli (polimorphic) bir sınıf yapısı söz konusuysa sınıfın bir gurup sanal fonksiyonu olmalıdır. Sınıfların Türetilebilirlik Durumu Türetilebilirlik durumuna göre sınıfları üç guruba ayırabiliriz: 1- Somut sınıflar: Konu ile ilgili işlemlerin hepsini yapma iddiasında olan, türetmenin gerekli olmadığı sınıflardır. 2- Soyut sınıflar: Kendisinden türetme yapılmadıkça bir kullanım anlamı olmayan sınıflardır. C++’da soyut sınıf kavramı saf sanal fonksiyonlarla syntax’a dahil edilmiştir. Ancak saf sanal fonksiyona sahip olmasa da bu özellikteki sınıflara da soyut sınıf denir. 3- Ara sınıflar: Türetmenin ara kademelerinde olan sınıflardır. Bir türetme şeması söz konusuysa herzaman değil ama genellikle soyut sınıflar en tepede, somut sınıflar en aşağıda, ara sınıflar ise ara kademelerde bulunur. Sınıfların İşlevlerine Göre Sınıflandırılması 1- Herhangi bir konuya özgü işlem yapan genel sınıflar: Bu tür sınıflar dış dünyadaki nesnelere karşılık gelen genel sınıflardır. 2- Yararlı sınıflar (utility class): Bunlar her türlü özel konulara ilişkin olmayan, her türlü projede kullanabileceğimiz genel sınıflardır. Örneğin, string işlemlerini yapan sınıflar, dosya işlemlerini yapan sınıflar, tarih işlemlerini yapan sınıflar gibi. 3- Nesne tutan sınıflar (container class / collection class): Dizi, bağlı liste, kuyruk, ikili ağaç, hash tabloları gibi veri yapılarını kurup çalıştıran, amacı bir algoritmaya göre birden çok nesne tutmak olan sınıflardır. 1996 yılında STL denilen template tabanlı kütüphane C++ programlama diline dahil edilmiştir ve C++’ın standart kütüphanesi yapılmıştır. STL içerisinde pek çok yararlı sınıf ve nesne tutan sınıf standart olarak vardır. 5 4- Arabirim sınıflar (interface class): Sisteme, donanıma ya da belli bir duruma özgü işlemler için kullanılan sınıflardır. Bu tür özel durum üzerinde işlem yapmak için ayrı sınıflar tasarlamak iyi bir yaklaşımdır. Böylece sisteme ya da donanıma özgü durumlar arabirim sınıflar tarafından ele alınabilir. Bu durumlar değiştiğinde diğer sınıflar çalışmadan etkilenmez, değişiklik sadece arabirim sınıflar üzerinde yapılır. Nesne Yönelimli Programlamanın Temel İlkeleri Nesne yönelimli programlama tekniği sınıf kullanarak programlama yapmak demektir. Nesne yönelimli programlama tekniği üzerine pek çok kavramdan bahsedildiyse de bu programlama tekniğinin temel olarak üç ilkesi vardır. Bu üç ilke dışındaki kavramlar bu ilkelerden türetilmiş kavramlardır. 1- Sınıfsal temsil (encapsulation): Bu kavram dış dünyadaki nesnelerin ya da kavramların ayrıntılarını gözardı ederek bir sınıf ile temsil edilmesi anlamına gelir. Bir nesneyi ya da kavramı sınıf ile temsil etmek yeterli değildir, onun karmaşık özelliklerini gizlemek gerekir. Ayrıntıların gözardı edilmesine aynı zamanda soyutlama (abstraction) da denilmektedir. C++’da ayrıntıları gözden uzak tutmak için sınıfın private bölümünü kullanırız. Tabii bazı ayrıntılar vardır sıradan kullanıcıların gözünden uzak tutulur ama bir geliştirici için gerekli olabilir. Bu tür özellikler için sınıfın protected bölümü kullanılır. Sınıfsal temsil ile karmaşık nesnelerin ya da kavramların özeti dışarıya yansıtılmaktadır. Eskiden yazılım projeleri bugüne göre çok büyük değildi, böyle bir soyutlama olmadan da projeler tasarlanıp geliştirilebiliyordu. Ancak son yıllarda projelerdeki kod büyümesi aşırı boyutlara ulaşmıştır. Büyük projelerin modellemesi çok karmaşıklaşmıştır. Nesne yönelimli programlama bu karmaşıklığın üstesinden gelmek için tasarlanmıştır. 2- Türetme (inheritance): Türetme daha önceden başkaları tarafından yazılmış olan bir sınıfın işlevlerinin genişletilmesi anlamındadır. Türetme sayesinde daha önce yapılan çalışmalara ekleme yapılabilmektedir. C++’da bir sınıftan yeni bir sınıf türetilir, eklemeler türemiş sınıf üzerinde yapılır. 3- Çok biçimlilik (polymorphism): Sınıfsal temsil ve türetme temel ilkelerdir, ancak pek çok tasarımcıya göre bir dilin nesne yönelimli olması için çok biçimlilik özelliğine de sahip olması gerekir. Çok biçimlilik özelliğine sahip olmayan dillere nesne tabanlı (object based) diller denir (VB.NET versiyonuna kadar nesne tabanlı bir dil görünümündedir. .NET ile birlikte çok biçimlilik özelliği de eklenmiştir ve nesne yönelimli olabilmiştir). Çok biçimliliğin üç farklı tanımı yapılabilir. Her tanım çok biçimliliğin bir yönünü açıklamaktadır. a- Birinci tanım: Çok biçimlilik taban sınıfın bir fonksiyonunun türemiş sınıfların her biri tarafından o sınıflara özgü biçimde işlem yapacak şekilde yazılmasıdır. Örneğin, Shape genel bir sınıf olabilir, bu sınıfın GetArea() isimli sanal bir fonksiyonu olabilir, bu fonksiyon bir geometrik şeklin alanını veren genel bir fonksiyondur. Rectangle sınıfı bu fonksiyonu dikdörtgenin alanını verecek biçimde, Circle sınıfının ise dairenin alanını verecek biçimde tanımlar. b- İkinci tanım: Çok biçimlilik önceden yazılarak derlenmiş olan kodların sonradan yazılan kodları çağırması özelliğidir. Örneğin, bir fonksiyon bir sınıf Shape türünden gösterici parametresine sahip olsun ve bu göstericiyle GetArea() isimli sanal fonksiyonunu 6 çağırarak işlem yapıyor olsun. Bu işlem yapılırken henüz Triangle sınıfı daha yazılmamış olabilir. Ancak kod yazılıp derlendikten sonra biz bu sınıfı oluşturup, bu sınıf türünden nesnenin adresini fonksiyona geçersek, fonksiyon Triangle sınıfının GetArea() fonksiyonunu çağıracaktır. c- Üçüncü tanım: Çok biçimlilik türden bağımsız sınıf işlemlerinin yapılmasına olanak sağlayan bir yöntemdir. Örneğin, bir programcı bir oyun programı yazıyor olsun, mesela tuğla kırma oyunu. Bu oyunda bir şekil hareketli bir cisme çarparak yansımaktadır. Yansıma, şeklin özelliğine bağlı olarak değişebilir. Programcı oyunu yazarken yansıyan şekli genel bir şekil olarak düşünür. Yani türü ne olursa olsun her türlü şeklin kendine özgü bir hareket biçimi, hızı, büyüklüğü ve yansıma biçimi vardır. Kodun şekille ilgili kısmı türden bağımsız yazılır, böylece kendisi ya da başka bir programcı Shape sınıfından bir sınıf türeterek ilgili sanal fonksiyonları yazarak kendi şeklini eskisi yerine etkin hale getirebilir. Ya da örneğin, programcı bir takım nesnelerin bir veri yapısında olduğu fikriyle programını yazabilir. Programını yazarken hangi veri yapısının kullanıldığını bilmek zorunda değildir. Collection isimli genel bir veri yapısını temsil eden sınıf tasarlanır, bu sınıfın her türden veri yapısı üzerinde geçerli olabilecek işlemlere ilişkin sanal fonksiyonları vardır. Böylece programcının kodu özel bir veri yapısına göre yazılmamış hale gelir, her veri yapısı için çalışabilir duruma getirilmiş olur. Buradaki türden bağımsızlık template işlemleriyle karıştırılmamalıdır. Template işlemlerinde derleme aşaması için bir türden bağımsızlık söz konusudur. Halbuki çok biçimlilikte derlenmiş olan kodun türden bağımsızlığı söz konusudur. Template’ler derlendikten sonra türü belirli hale gelen kodlardır. Nesne Yönelimli Analiz ve Modelleme Büyük projeler çeşitli aşamalardan geçilerek ürün haline getirilirler. Tipik aşamalar sistemin analizi, kodlama için modellenmesi (yani, kodlamaya ilişkin belirlemelerin yapılması), kodlama işleminin kendisi, test işlemi (test işlemi kodlama işlemi ile beraber yürütülen bir işlem olabilir, tabii ürünün tamamının alfa ve beta testleri de söz konusu olabilir), dokümantasyon ve bakım işlemleri (yani, ürünün bir kitapçığı hazırlanabilir, ürünün oluşturulmasına ilişkin adımlar dokümante edilebilir, ürün oluşturulduktan sonra çıkacak çeşitli problemlere müdahale edilebilir ve hatta nihayi ürün üzerinde değiştirme ve geliştirme işlemleri yapılabilir). Her ne kadar proje geliştirme işleminin teorik tarafı bu adımları sırası ile içerse de küçük gruplar ya da tek kişilik çalışmalarda programcı kendi sezgisiyle bunları eş zamanlı olarak sağlamaya çalışabilir. Teorik açıklamalar ancak genel kurallardır. Bu genel kurallar izlendiği halde başarısız olunabilir, izlenmediği halde başarılı olunabilir. Nesne yönelimli teknik kullanılan projelerde analiz aşamasından sonra proje için gerekli sınıfların tespit edilmesi ve aralarındaki ilişki açıklanmalıdır. Eğer böyle yapılırsa bundan sonra projenin kodlama aşamasında problemleri azalır. Proje içerisindeki sınıfların tespit edilmesi, bunların arasındaki ilişkilerin belirlenmesi sürecine nesne yönelimli modelleme denilmektedir. Nesne yönelimli modellemede ilk yapılacak iş proje konusuna ilişkin dış dünyadaki gerçek nesneler ya da kavramları birer sınıfla temsil etmektir. Örneğin, C derneği otomasyona geçecek olsun bütün işlemleri yapacak bir proje geliştirilecek olsun. Konuya ilişkin gerçek hayattaki 7 nesneler ve kavramlar belirlenir, bunlar birer sınıfla temsil edilir (bu işleme transformation denilmektedir). Örneğin dernekte neler vardır? - derneğin yönetim kurulu öğrenciler bilgisayarlar ve demirbaşlar maaşlı çalışanlar hocalar üyeler sınıflar Bu nesne ve kavramların hepsi birer sınıfla temsil edilir. Bu aşamadan sonra bu sınıflar arasındaki ilişkiler tespit edilmeye çalışılır. Örneğin, hangi sınıf hangi sınıftan türetilebilir? Hangi sınıf hangi sınıfı kullanacaktır? Hangi sınıfın derlenmesi için diğer sınıfın bilgilerine gereksinim vardır? Bunlar bir sınıf şeması ile belirtilebilir. Sınıfların Sınıfları Kullanma Biçimi Sınıfların sınıfları kullanma biçimi dört biçimde olabilir: 1- Türetme ilişkisi ile kullanma (inheritance): Mesela A taban sınıftır, B A’dan türetilir, B A’yı bu biçimde kullanmaktadır. 2- Veri elemanı olarak kullanma (composition): Bir sınıfın başka bir sınıf türünden veri elemanına sahip olması durumunda eleman olan sınıf nesnesinin ömrü, elemana sahip sınıf nesnesinin ömrüyle ilgilidir. Yani, eleman olan sınıf nesnesi, elemana sahip sınıf nesnesi yaratıldığında yaratılır ve o nesne yok edildiğinde yok edilir. UML notasyonunda bu durum elemana sahip sınıftan eleman olarak kullanılan sınıfa doğru içi dolu yuvarlak (•) ya da karo (♦) ile gösterilir. Örneğin: A B Sınıf nesneleri büyükse eleman olan sınıf nesnelerinin heap üzerinde tahsis edilmesi daha uygun olabilir. Bu durumda elemana ilişkin sınıf türünden bir gösterici veri elemanı alınır, sınıfın başlangıç fonksiyonu içerisinde bu göstericiye tahsisat yapılır, bitiş fonksiyonu içinde de geri bırakılır. Örneğin: class B { private: A *m_pA; //... }; 8 Bu biçimde bir kullanma ile diğerinin arasında kavramsal bir farklılık yoktur. Her iki durumda da eleman olan nesnenin ömürleri elemana sahip sınıfın ömrüyle aynıdır. 3- Başka bir sınıf nesnesinin adresini alarak veri elemanı biçiminde kullanma (aggregation): Bu durumda kullanılacak nesne kullanan nesneden daha önce yaratılmıştır, belki daha sonra da var olmaya devam edecektir. Sınıfın yine nesne adresini tutan bir gösterici veri elemanı vardır. Kullanılacak nesnenin adresi kullanan sınıfın başlangıç fonksiyonu içerisinde alınarak veri elemanına atanır. Yani bu durumda kullananılacak nesne kullanan sınıf tarafından yaratılmamıştır. Bu durum genellikle sınıf ilişki diyagramlarında içi boş yuvarlak (ο) ya da karo (◊) ile gösterilir. class B { public: B (A *pA) { m_pA = pA; } private: A *m_pA; //... }; A a; { B b(&a); ... } ... Bu tür kullanma durumu genellikle bir nesnenin başka bir nesneyle ilişkili işlemler yaptığı durumlarda, ancak kullanılan nesnenin bağımsız olarak kullanılmasına devam ettiği durumlarda tercih edilir. Örneğin, bir bankada bir müşterinin hesabı üzerinde işlem yapmak için kullanılan bir sınıf olsun. Burada müşteri nesnesi daha önce yaratılmalıdır, belki üzerinde başka işlemler de uygulanmıştır, ancak hesap işlemleri söz konusu olduğunda o nesne başka bir sınıf tarafından kullanılacaktır. Gösterici yoluyla kullanma söz konusu olduğundan nesnedeki değişiklik kullanan sınıf tarafından hemen fark edilir. 4- Üye fonksiyon içerisinde kullanma (association): Bu durumda sınıfın bir üye fonksiyonu başka bir sınıf türünden gösterici parametresine sahiptir, yani sınıf başka bir sınıfı kısmen kullanıyordur. Bu durum genellikle sınıf ilişki diyagramlarında kesikli oklarla gösterilir. A B 9 class B { public: void Func(A *pA); //... }; Bunların dışında bir sınıf başka bir sınıfı sınıfın yerel bloğu içerisinde kullanıyor olabilir. Ancak bu durum önemsiz bir durumdur, çünkü bu kullanma ilişkisi kimseyi ilgilendirmeyecek düzeydedir. Çeşitli Yararlı Sınıfların Tasarımı Bu bölümde string, dosya, tarih gibi genel işlemler yapan yararlı sınıfların tasarımı üzerinde durulacaktır. String Sınıfları Yazılarla işlemler yaparken klasik olarak char türden diziler kullanılır. Ancak dizilerin uzunluğu derleme zamanında sabit ifadesiyle belirtilmek zorundadır. Bu durum yazılar üzerinde ekleme ve çıkarma işlemleri yapıldığında bellek verimini düşürmekte ve programı karmaşık hale getirmektedir. Bellek kayıplarını engellemek için dizi dinamik tahsis edilebilir, bu durumda dizi büyütüleceği zaman yeterli uzunlukta yeni bir blok tahsis edilebilir. Ancak dinamik tahsisatlar programcıya ek yükler getirmektedir. İşte bu nedenlerden dolayı yazı işlemlerinin bir sınıf tarafından temsil edilmesi (yani encapsule edilmesi) çok sık rastlanılan bir çözümdür. Bir string sınıfının veri elemanları ne olmalıdır? Yazı için alan dinamik olarak tahsis edileceğine göre dinamik alanı tutan char türden bir gösterici olmalıdır. Yazının uzunluğunun tutulmasına gerek olmasa da pek çok işlemde hız kazancı sağladığından uzunluk da tutulmalıdır. Profesyönel uygulamalarda yazı için blok tam yazı uzunluğu kadar değil, daha büyük alınır. Böylece küçük ekleme işlemlerinde gereksiz tahsisat işlemleri engellenir. Tabii, yazı uzunluğunun yanı sıra tahsis edilen bloğun uzunluğu da tutulmalıdır. Bloklar önceden belirlenmiş bir sayının katları biçiminde tahsis edilebilir. Bu durumda string sınıfının tipik veri elemanları şöyle olacaktır: class CString { //... protected: char *m_pStr; unsigned m_size; unsigned m_length; static unsigned m_allocSize; }; unsigned CString::m_allocSize = CSTRING_ALLOC_SIZE; 10 Sınıfın m_allocSize isimli static veri elemanı hangi blok katlarında tahsisat yapılacağını belirtir. Bu static veri elemanı başlangıçta 10 gibi bir değerdedir. Yani, bu durumda blok 10’un katları biçiminde tahsis edilir. Bu durumda bir CString sınıf nesnesinin yaratılmasıyla şöyle bir durum oluşacaktır: ankara\0 m_pStr m_size m_length Sınıf çalışması olarak tasarlanan string sınıfı MFC CString sınıfına çok benzetilmiştir. Sınıfın başlangıç fonksiyonları şunlardır: CString(); CString(const CString &a); CString(char ch, unsigned repeat = 1); CString(const char *pStr); CString(const char *pStr, unsigned length); ~CString(); Sınıf çalışmasındaki CString sınıfının pek çok türdeki üye fonksiyonu vardır. Bu fonksiyonlar şu işleri yaparlar: - unsigned GetLength() const; Sınıfın tuttuğu yazının uzunluğuna geri döner. - BOOL IsEmpty() const; Nesnenin hiç karaktere sahip olmayan bir nesneyi gösterip göstermediğini belirler. - void Empty(); Nesnenin tuttuğu yazıyı siler. - void SetAt(unsigned index, char ch); char GetAt(unsigned index) const; Bu fonksiyonlar yazının belli bir karakterini alıp yerleştirmekte kullanılır. - int Compare(PCSTR s) const; Nesnenin içerisindeki yazı ile parametre olarak girilen yazıyı karşılaştırır. - int CompareNoCase(PCSTR s) const; Nesnenin tuttuğu yazı ile parametre olarak girilen yazı büyük harf küçük harf duyarlılığı olmadan karşılaştırılır. - CString Left(int count) const; CString Right(int count) const; Bu fonksiyonlar nesne içerisindeki yazının soldan ve sağdan n karakterini alarak yeni bir yazı oluştururlar. Örneğin, CString path(“C:/autoexec.bat”); CString drive; 11 drive = path.Left(2); - - Görüldüğü gibi bu fonksiyonlar geri dönüş değeri olarak geçici bir nesne yaratmaktadır. Tabi, CString sınıfının bir atama operatör fonksiyonu olmalıdır. drive = path.Left(2); işleminde şunlar yapılır: a- Fonksiyon içerisinde soldaki iki karakter bir CString nesnesi olarak elde edilir ve bu nesne ile return edilir. b- Geçici nesne kopya başlangıç fonksiyonu ile yaratılır. c- Geçici nesneden drive nesnesine atama için atama operatör fonksiyonu çağırılır. d- Geçici nesne için bitiş fonksiyonu çağırılır. CString Mid(int first) const; CString Mid(int first, int count) const; Bu fonksiyonlar yazının belli bir karakter index’inden başlayarak n tane karakterini alıp yeni bir CString nesnesi olarak verir. Fonksiyonun tek parametreli biçimi geri kalan yazının tamamını almaktadır. void MakeUpper(); void MakeLower(); Sınıf içerisinde tutulan yazıyı büyük harfe ve küçük harfe dönüştürür. void Format(PCSTR pStr, ...); Bu fonksiyon değişken sayıda parametre alan bir fonksiyondur. sprintf() gibi çalışır, sınıfın tuttuğu eski yazıyı silerek yeni yazıyı oluşturur. void MakeReverse(); Sınıfın tuttuğu yazıyı tersdüz eder. void TrimLeft(); void TrimRight(); Yazının solundaki ve sağındaki boşlukları atar. int Find(char ch) const; int Find(PCSTR pStr) const; Bu fonksiyonlar yazı içerisinde bir karakteri ve bir yazıyı ararlar, geri dönüş değerleri başarılıysa bulunan yerin yazıdaki index numarası, başarısızsa –1 değeridir. Sınıfın ReverseFind() fonksiyonu aramayı tersten yapar. CString Sınıfının Operatör Fonksiyonları - Sınıfın [] operatör fonksiyonu sanki diziymiş gibi yazının bir indexine erişir. char &operator [](unsigned index); [] operatör fonksiyonu hem sol taraf hem de sağ taraf değeri olarak kullanılabilir. Örneğin: CString s = “Ankara”; s[0] = ‘a’; // s.operator[](0) = ‘a’; 12 - Sınıfın const char * türüne dönüştürme yapan bir tür dönüştürme operatörü de vardır. operator const char *() const; Bu tür dönüştürme operatör fonksiyonu doğrudan yazının tutulduğu adres ile geri döner, böylelikle biz CString türünden bir nesneyi doğrudan const char * türüne atayabiliriz. CString sınıfında bu işlem genellikle bir fonksiyonun çağırılması sonucunda oluşmaktadır. Örneğin: CString s = “Ankara”; puts(s); // puts(s.operator const char *()); Anımsatma: C++ tür dönüştürme operatör fonksiyonları şu durumlarda çağırılır: 1- Nesne tür dönüştürme operatörü ile ilgili türe dönüştürülmek istendiğinde. Örneğin: Date x; ... (int) x; 2- Sınıf nesnesini başka türden bir nesneye atanması durumunda. Örneğin: int a; Date b; ... a = b; 3- İşlem öncesinde otomatik tür dönüştürmesiyle. Örneğin: int a, b; Date c; a = b + c; // a = b + c.operator int(); Eğer işlem soldaki operandın sağdakinin türüne, aynı zamanda sağdaki operandın soldakinin türüne dönüştürülerek yapılabiliyorsa iki anlamlılık hatası oluşur. C++ derleyicisi bir operatörle karşılaştığında önce operandların türlerini araştırır. Operandlar C’nin normal türlerine ilişkinse küçük tür büyük türe dönüştürülerek işlem gerçekleştirilir. Operandlardan en az biri bir nesneyse derleyici sırasıyla şu kontrolleri yapar: iiiiii- İşlemi doğrudan yapacak global ya da üye operatör fonksiyonu araştırır. Her ikisinin birden bulunması error oluşturur. Birinci operandı ikinci operandın türüne ya da ikinci operandı birinci operandın türüne dönüştürerek işlemi yapmaya çalışır. Her iki biçimde de işlem yapılabiliyorsa bu durum error oluşturur. Bu dönüştürme işleminde derleyici sınıf nesnesini normal türlere dönüştürürken sınıfın tür dönüştürme operatör fonksiyonunu kullanır. Normal türü sınıf türüne dönüştürmek için ise başlangıç fonksiyonu yoluyla geçici nesne yaratma yöntemini kullanır. Örneğin: Complex a(3, 2); double b = 5, c; 13 c = a + b; Burada Complex sınıfının uygun bir operator + fonksiyonu varsa işlem o fonksiyonun çağırılmasıyla problemsiz yapılır. Eğer yoksa derleyici bu sefer Complex türünden nesneyi double türüne ya da double türünü Complex sınıfı türüne dönüştürerek işlemi yapmak isteyecektir. Yani, 1) c = a.operator double() + b; 2) c = a + Complex(b); Her iki biçim de mümkünse iki anlamlılık hatası oluşur. Eğer yalnızca bir durum sağlanıyorsa işlem normal olarak yapılır. Her iki operandın da diğerinin türüne dönüştürülebildiği durumlarda iki anlamlılık hatalarından kurtulmak için ifade açıkça yazılabilir. Yani, c = a + Complex(b); c = (double) a + b; CString sınıfının const char * türüne dönüştürme yapan operatör fonksiyonu ile sanki CString nesnesi bir diziymiş gibi kullanılabilmektedir. Yazının tutulduğu adresi veren tür dönüştürme operatör fonksiyonunun char * değil de const char * türünden olduğuna dikkat edilmelidir. Bu durumda örneğin, CString s(“Ankara”); char *p; p = s; işlemi error ile sonuçlanır. Eğer bu işlem mümkün olsaydı biz CString nesnesinin kullandığı dinamik alan üzerinde değişiklik yapabilirdik ve sınıfın veri elemanı bütünlüğünü bozabilirdik. puts(s), strlen(s), strcpy(buf, s) gibi işlemler mümkündür, ancak strupr(s), strcpy(s, buf) gibi işlemler error ile sonuçlanır. MFC’de yazının tutulduğu adresi dışarıdan değiştirilebilecek biçimde veren GetBuffer() isimli bir üye fonksiyon da vardır. Ancak programcı bu fonksiyonu dikkatli kullanmalıdır. Yazının güncellenmesi bittikten sonra sınıfın ReleaseBuffer() isimli fonksiyonunu çağırmalıdır, çünkü ReleaseBuffer() dışarıdan yapılmış değişiklikleri görerek sınıfın veri elemanı bütünlüğünü korur. char *GetBuffer(unsigned minLength); void ReleaseBuffer(unsigned newLength); Programcı yazının tutulduğu adresi elde ederken tahsisat alanının genişliğini de bilmelidir, bu yüzden GetBuffer() fonksiyonuna tahsisat alanını belirleyen bir parametre eklenmiştir. GetBuffer() genişletilmiş alanın adresiyle geri döner. Benzer biçimde ReleaseBuffer() yazının uzunluğunu belirleyerek işlemini bitirir. –1 özel değeri herhangi bir işlemin yapılmayacağını gösterir. Örnek: CString s = “Ankara”; 14 char *pUpdate; pUpdate = s.GetBuffer(30); s.ReleaseBuffer(-1); - CString sınıfının + operatör fonksiyonları iki CString nesnesini, bir CString nesnesinin sonuna bir karakteri ya da bir CString nesnesinin sonuna bir yazıyı ekler. Aynı işlemleri yapan += operatör fonksiyonları da vardır. Örneğin: CString a = “ankara”, b = “izmir”; c = a + b; puts(c); a += b; c = a + “istanbul”; a += ‘x’; - CString sınıfının başka bir CString nesnesiyle, bir yazı ile her türlü karşılaştırmayı yapan bir grup üye ve global operatör fonksiyonu vardır. CString sınıfını başka CString nesnesine atamakta kullanılan ve bir karakter atamakta kullanılan atama operatör fonksiyonları vardır. Nihayet sınıfın cout ve cin nesneleriyle işlem yapabilecek << ve >> operatör fonksiyonları vardır. Anahtar Notlar: a sayısını n’in katlarına çekmek için şu ifade kullanılır: (a + n – 1) / n * n Anahtar Notlar: Bir sınıf için kopya başlangıç fonksiyonu gerekiyorsa atama operatör fonksiyonu da gerekir. Kopya başlangıç fonksiyonu ve atama operatör fonksiyonunun gerektiği tipik durumlar başlangıç fonksiyonlarına veri elemanları için dinamik tahsisat yapıldığı durumlardır. Atama operatör fonksiyonlarının hemen başında nesnenin kendi kendine atanıp atanmadığı tespit edilmelidir. Anımsatma: C’de ve C++’da başına signed ya da unsigned anahtar sözcüğü getirilmeden char denildiğinde default durum derleyiciyi yazanların isteğine bırakılmıştır (implementation dependent). C’de bu durum bir taşınabilirlik problemine yol açmasın diye char *, signed char *, unsigned char * türlerinin hepsi aynı adres türüymüş gibi kabul edilmiştir. Böylelikle char türünün default durumu ne olursa olsun C’de aşağıdaki kod bir probleme yol açmaz. char s[] = “Ankara”; unsigned char *p; p = s; Halbuki C++’da bu üç tür de tamamen farklı türler gibi ele alınmıştır. Bu nedenle yukarıdaki örnekte derleyicinin default char türü unsigned olsa bile error oluşur. Bu yüzden C++’da fonksiyonun parametresi char * türündense bu fonksiyon unsigned char * türü için çalışmayacaktır. Maalesef fonksiyon bu tür için yeniden yazılmalıdır. Anımsatma: Global operatör fonksiyonları işlevsel olarak üye operatör fonksiyonlarını kapsar. Ancak tür dönüştürme, atama, ok (->), yıldız (*) operatör fonksiyonları üye olmak zorundadırlar. Binary 15 operatörlerde birinci operand doğal türlere ilişkin ikinci operand ise bir sınıf nesnesi olduğunda bu durum ancak global operatör fonksiyonlarıyla karşılanmaktadır. Üye operatör fonksiyonu olarak yazılmak zorunda olanların zaten böyle bir zorunluluğu yoktur. Bu yüzden bazı tasarımcılar soldaki operand sınıf nesnesi, sağdaki operand doğal türlerden olduğunda bunu üye operatör fonksiyonu olarak, tam tersi durum söz konusu olduğunda bunu global operatör fonksiyonu olarak yazmak yerine hepsini global operatör fonksiyonu olarak yazarlar. Global operatör fonksiyonlarının friend olması çoğu kez gerekmektedir. Anımsatma: Bir sınıf nesnesi aynı türden geçici bir nesneyle ilk değer verilerek yaratılıyorsa normal olarak işlemlerin şu sırada yapılması beklenir: 1- Geçici nesne yaratılır. 2- Yaratılan nesne için kopya başlangıç fonksiyonu çağırılır. Ancak standardizasyonda böylesi durumlarda derleyicinin optimizasyon amaçlı kopya başlangıç fonksiyonunu hiç çağırmayabileceği, yaratılan nesneyi doğrudan geçici nesnede belirtilen başlangıç fonksiyonuyla yaratabileceği belirtilmiştir. Aynı durum fonksiyonun geri dönüş değerinin bir sınıf türünden olduğu ve fonksiyondan geçici bir nesne yaratılarak return ifadesi ile dönüldüğü durumlarda da geçerlidir. Bu durumda da geçici bölge için return ifadesinde belirtilen başlangıç fonksiyonu çağırılacaktır. Bu nedenle CString sınıfının Mid() fonksiyonu aşağıdaki gibi düzeltilirse daha verimli olur: CString CString::Mid(int first) const { return CString(m_pStr + first); } CString Sınıfının Kullanımına İlişkin Örnek Bu örnekte bir komut yorumlayıcı algoritmasının çatısı oluşturulacaktır. Komut yorumlayıcılarda bir prompt çıkar, kullanıcı bir komut yazar, komut yorumlayıcı bu komut kendi kümesinde varsa onu çalıştırır yoksa durumu bir mesajla bildirir. DOS komut satırı ve UNIX işletim sisteminin shell programları buna benzer programlardır. Bu uygulamadaki amaç bir string sınıfını kullanma çalışması yapmaktır. Tasarımımızda komut yorumlayıcı Shell isimli bir sınıf ile temsil edilecektir. Prompt yazısı sınıfın CString türünden bir veri elemanında tutulabilir, sınıfın başlangıç fonksiyonu bu prompt yazısını parametre olarak alabilir. Programın main kısmı şöyle olabilir: void main() { Shell shell(“CSD”); shell.Run(); } Görüldüğü gibi program Run() üye fonksiyonu içerisinde gerçekleşmektedir. Komut yazıldığında komut ile parametreler ayrıştırılarak sınıfın iki veri elemanında tutulabilir. Komut yorumlayıcının döngüsü içerisinde yazı alınır, komut ve parametreler ayrıştırılır, komut önceden belirlenmiş komut kümesinde aranır. Anahtar Notlar: İyi bir nesne yönelimli teknikte az sayıda global değişken kullanılmalıdır. Global değişkenler bir sınıf ile ilişkilendirilip sınıfın static veri elemanı yapılmalıdır. 16 Anahtar Notlar: İyi bir nesne yönelimli teknikte sembolik sabitler için mümkün olduğu kadar az #define kullanılır. Bunun yerine sembolik sabitler bir sınıf içinde enum olarak bildirilir, böylece global faaliyet alanı kirletilmemiş olur. Komut, belirlenen komut kümesinde aranıp bulunduktan sonra komutu çalıştırmak için sınıfın bir üye fonksiyonu çağırılır. Komutları yorumlayan bu fonksiyonların çok biçimli olması faydalıdır, bu nedenle sanal yapılması uygundur. /* fonksiyon göstericisi kullanarak */ /* shell.h */ #ifndef _SHELL_H_ #define _SHELL_H_ #define LINELEN 128 class Shell { public: Shell(const char *pPrompt):m_prompt(pPrompt){} void Run(); private: CString m_prompt; CString m_command; CString m_param; static char *m_cmd[]; }; typedef struct _CMDPROC { char *pCommand; void (*pProc)(Shell *pShell); } CMDPROC; void void void void void void void DirProc(Shell *pShell); RenameProc(Shell *pShell); CopyProc(Shell *pShell); MoveProc(Shell *pShell); RemoveProc(Shell *pShell); XcopyProc(Shell *pShell); QuitProc(Shell *pShell); #endif /* shell.cpp */ #include #include #include #include #include #include <stdio.h> <stdlib.h> <iostream.h> "general.h" "cstring.h" "shell.h" CMDPROC cmdProc[] = {{"dir", DirProc}, {"rename", RenameProc}, {"copy", CopyProc}, 17 {"remove", RemoveProc}, {"xcopy", XcopyProc}, {"move", MoveProc}, {"quit", QuitProc}, {NULL, NULL}}; void DirProc(Shell *pShell) { cout << "DirProc, param :" << endl; } void RenameProc(Shell *pShell) { cout << "RenameProc, param :" << endl; } void CopyProc(Shell *pShell) { cout << "CopyProc, param :" << endl; } void MoveProc(Shell *pShell) { cout << "MoveProc, param :" << endl; } void RemoveProc(Shell *pShell) { cout << "RemoveProc, param :" << endl; } void XcopyProc(Shell *pShell) { cout << "XcopyProc, param :" << endl; } void QuitProc(Shell *pShell) { exit(1); } void Shell::Run() { char buf[LINELEN]; CString cmd; for (;;) { cout << m_prompt << '>'; gets(buf); cmd = buf; cmd.TrimLeft(); int cmdIndex = cmd.FindOneOf(" \t\0"); m_command = cmd.Left(cmdIndex); m_param = cmd.Mid(cmdIndex); m_param.TrimLeft(); for (int i = 0; cmdProc[i].pCommand != NULL; ++i) if(cmdProc[i].pCommand == m_command) { cmdProc[i].pProc(this); 18 } } break; } if (cmdProc[i].pCommand == NULL) { cout << "Bad command or file name\n"; continue; } int main() { Shell shell("CSD"); shell.Run(); return 0; } Line Editör Ödevi İçin Notlar Satır satır işlem yapılan editörlere line editör denir. DOS’un edlin editörü, UNIX’in ed editörü tipik line editörlerdir. Line editörlerde daha önce uygulamasını yaptığımız bir komut satırı vardır, kullanıcı bir komut ve bir satır numarası girer, editör o satır üzerinde ilgili işlemleri yapar. Böyle bir line editör uygulaması için bir Editor sınıfı ve bir Shell sınıfı alınabilir. Shell sınıfı Editor sınıfının bir veri elemanı gibi kullanılabilir. Bu sınıfların yanı sıra işlemleri kolaylaştıran String ve File sınıflarından da faydalanılabilir. Dosya İşlemlerini Yapan Sınıflar Dosya işlemleri de tipik olarak sınıflarla temsil edilebilir. Pek çok sınıf kütüphanesinde dosya işlemlerini yapan bir sınıf vardır. Java, C# gibi nesne yönelimli dillerde dosya işlemleri için tek bir sınıf değil polimorfik özelliği olan bir sınıf sistemi kullanılmaktadır. Ayrıca C++’ın standart kütüphanesinde dosya işlemleri iostream sınıf sistemi ile de yapılabilmektedir. Maalesef bu sınıf sistemi dosya işlemleri için yetersiz kalmaktadır. Bu nedenle dosya işlemleri için iostream sınıf sistemini kullanmak yerine çoğu kez bu işlem için ayrı bir sınıf tasarlama yoluna gidilmektedir. MFC kütüphanesinde dosya işlemleri için CFile isimli bir sınıf tasarlanmıştır. CFile sınıfı binary dosyalar üzerinde işlemler yapar, CFile sınıfından CStdioFile isimli bir sınıf türetilmiştir, bu sınıf da text dosyaları üzerinde işlem yapmaktadır. Örnek bir dosya sınıfı için MFC kütüphanesindeki CFile sınıfına benzer bir sınıf tasarlanacaktır. CFile Sınıfının Tasarımı ve Kullanılması CFile sınıfı cfile.h ve cfile.cpp isimli iki dosya halinde tasarlanmıştır. Anahtar Notlar: Bir sınıf başka bir sınıfı kullanıyor olsun. Örneğin B sınıfının A sınıfını kullandığını düşünelim. B sınıfı ve A sınıfı ikişer dosya halinde yazılmış olsun. A.h dosyası B 19 sınıfının hangi dosyasında include edilmelidir? Bu sorunun yanıtı, biz dışarıdan B sınıfını kullanırken yalnızca B.h dosyasının include edilmesinin probleme yol açıp açmayacağıyla verilir. Yani, eğer A sınıfı B.h içerisinde kullanılmışsa, yani B sınıfının bildirimi içerisinde kullanılmışsa, sınıfın B.h içerisinde include edilmesi gerekir (genellikle bu durum composition ya da aggregation durumudur). Eğer A sınıfı yalnızca B sınıfının üye fonksiyonları içerisinde kullanılmışsa bu durumda A sınıfı yalnızca B.cpp içerisinde kullanılmıştır, dolayısıyla B.cpp’nin içerisinden include edilmesi uygundur. Çok temel olan dosyaların include edilmesi sırasında ek bir include koruması uygulanabilir ya da çok temel dosyalar tamamen uygulama programcısı tarafından zorunlu olarak include edilmesi gereken bir durum biçiminde ele alınabilir. Anahtar Notlar: Bir sınıfın içerisinde bir enum bildirimi yapılmışsa enum sabitleri o sınıfın içerisinde doğrudan kullanılabilir, ancak dışarıdan doğrudan kullanılamaz. Ancak enum sınıfın public bölümündeyse çözünürlük operatörüyle dışarıdan kullanılabilir. Büyük projelerde global alandaki isim çakışmasını engellemek için sembolik sabitlerin #define ile oluşturulması tavsiye edilmez, bunun yerine sembolik sabitler bir sınıfla ilişkilendirilmeli ve o sınıfın enum sabiti olarak bildirilmelidir. CFile sınıfının üye fonksiyonları şunlardır: - Sınıfın üç başlangıç fonksiyonu vardır: CFile(); CFile(FILE *fp); CFile(const char *pFileName, UINT openFlags); Default başlangıç fonksiyonu ile nesne yaratılırsa dosya daha sonra sınıfın CFile::Open() üye fonksiyonuyla açılmalıdır. İkinci başlangıç fonksiyonu daha önce fopen() fonksiyonu ile açılmış olan dosyayı sınıf ile ilişkilendirilip kullanmak için düşünülmüştür. Nihayet üçüncü fonksiyon, ismi ile belirtilen dosyayı belirtilen modda açar. - Sınıfın bitiş fonksiyonu sınıf tarafından açılıp kullanılmakta olan bir dosya varsa onu kapatır, yoksa bir şey yapmaz. Anahtar Notlar: Pek çok sınıf için bitiş fonksiyonu koşulsuz bir geri alma işlemini yapmaz. Önce bir kontrol yapar, bu kontrolle başlangıç işlemlerinin yapılıp yapılmadığını anlar, yapılmışsa onu geri alır, yapılmamışsa hiç bir şey yapmaz. Örneğin CFile gibi bir sınıfın bitiş fonksiyonu hemen dosyayı kapatmaya yeltenmemelidir. Önce dosyanın açılıp açılmamış olduğuna bakmalı açılmışsa kapatmalıdır. Bu işlem genellikle sınıfın veri elemanına özel bir değerin yerleştirilmesi ve bunun kontrol edilmesi ile yapılmaktadır. Örneğin: CFile::CFile() { m_f = NULL; } CFile::~CFile() 20 { if (m_f) ::flose(m_t); } - Eğer dosya başlangıç fonksiyonu yoluyla açılmamışsa sınıfın Open() fonksiyonu ile açılabilir. virtual BOOL Open(const char *pFileName, UINT openFlags); Sınıfın başlangıç fonksiyonunda ve bu fonksiyonda belirtilen açış modu sınıfın enum sabitlerinin bit or işlemlerine sokulmasıyla elde edilir. Örneğin: CFile f; if (f.Open(“x.dat”, CFile::modeCreate | CFileReadWrite) { ... ... } Dosya açış modlarına ilişkin enum sabitleri bütün bitleri 0, yalnızca bir biti 1 olan sayılardır. - Sınıfın Close() fonksiyonu dosyayı kapatmaktadır. virtual void Close(); Close() fonksiyonunun tasarımında dosya açıksa kapatılmıştır, zaten sınıfın bitiş fonksiyonu da bu fonksiyonu çağırmaktadır. - Sınıfın dosya göstericisinin gösterdiği yerden belli miktarda byte okuyup yazan Read() ve Write() fonksiyonları vardır. virtual UINT Read(void *pBuf, UINT count); virtual UINT Write(const void *pBuf, UINT count); - GetLength() fonksiyonu dosyanın uzunluğunu verir. virtual DWORD GetLength() const; - Seek() ve GetPosition() fonksiyonları sırasıyla dosya göstericisini konumlandırır ve onun offset değerini elde eder. virtual BOOL Seek(long offset, UINT origin); virtual DWORD GetPosition() const; fonksiyonunun ikinci parametresi CFile::begin, CFile::end, olabilir. Ayrıca sınıfın SeekToBegin() ve SeekToEnd() fonksiyonları da vardır. Bu fonksiyonlar dosya göstericisini başa ve sona çeker. Seek() CFile::current 21 - Açılan dosyanın ismi sınıfın bir veri elemanında tutulmaktadır. GetFileName() fonksiyonu ile dosya ismi uzantısıyla beraber, GetFileTitle() fonksiyonu ile dosya ismi yalnızca isim olarak elde edilebilir. CFile sınıfında iki veri elemanı kullanılmıştır. Dosya içeride fopen() fonksiyonu ile açılmıştır, fopen() fonksiyonunun geri verdiği handle FILE *m_f veri elemanında saklanmıştır. Açılan dosyanın ismi CString m_fileName elemanında saklanmıştır. Anahtar Notlar: Sınıfın bazı üye fonksiyonları kısmen aynı şeyleri yapıyor olabilir. Bu durumda bu aynı işlemler bir üye fonksiyona yaptırılır, bu üye fonksiyon dışarıdan çağırılmayacağına göre sınıfın private bölümüne yerleştirilir. Anahtar Notlar: Başlangıç fonksiyonlarının geri dönüş değeri yoktur. Başlangıç fonksiyonlarında başarısız olunabilecek bir durum varsa bu durumu dışarıya bildirmenin üç yolu vardır: 1- Başarısızlık durumda hiç bir şey yapılmaz, bir mesaj verilir görmezlikten gelinir. 2- Başarısızlık başlangıç fonksiyonu içerisinde tespit edilir ve bir exception oluşturulur. Programcı da nesneyi try bloğunda yaratır. Örneğin, MFC’de dosya CFile sınıfının başlangıç fonksiyonunda açılamadıysa CFileException sınıfı türünden dinamik bir nesne yaratılıp o adres ile throw edilir. Örneğin, try { CFile f(...); //... } catch (CFileException *pFileException) { //... } 3- Başlangıç fonksiyonu içerisinde başarısız olunduğunda sınıfın bir veri elemanı set edilir, daha sonra o veri elemanına bakılarak başarı durumu tespit edilir. Bu bakılma işlemi için tipik olarak ! operatör fonksiyonu kullanılmaktadır. Örneğin, CFile f(...); if (!f) { cout << “Cannot open file...\n”; exit(EXIT_FAILURE); } class CFile { //... private: BOOL m_bSuccess; }; CFile::CFile(....) { if (Dosya açılamadıysa) 22 } //... m_bSuccess = FALSE; BOOL CFile::operator !() const { return !m_bSuccess; } CFile Sınıfının Çokbiçimliliği CFile sınıfı çeşitli sanal fonksiyonlarla çokbiçimli (polymorphic) bir sınıf olarak yazılmıştır. Sınıfın önemli üye fonksiyonlarının hepsi sanal yapılmıştır. CFile sınıfından bir sınıf türetilip bu fonksiyonlar yazılırsa türetilen sınıfın fonksiyonları çalıştırılacaktır. Sınıf kütüphanelerinde genellikle başka konularda yazılmış pek çok fonksiyon temel sınıfları kullanarak tasarlanmıştır. Örneğin, kütüphane içerisinde iki dosyayı kopyalayan Copy() isimli bir fonksiyon olsun. Bu fonksiyon global olabileceği gibi başka bir sınıfın üye fonksiyonu da olabilir. Copy() fonksiyonu dosyanın isimlerini değil de CFile nesnelerini parametre olarak alıp, onların Read(), Write() fonksiyonlarını kullanarak kopyalama işlemi yapsın. BOOL Copy(CFile *pSource, CFile *pDest); Şimdi biz CFile sınıfından CSocketFile gibi bir sınıf türetip sanal fonksiyonları bu sınıf için yeniden yazalım. Bu sınıf TCP/IP portlarını bir dosya gibi kullanıyor olsun. Biz şimdi iki port arasında dosya transferi yapmak için yeni bir Copy() fonksiyonu yazmak yerine eski Copy() fonksiyonunu kullanabiliriz. CSocketFile fs, fd; //... Copy(&fs, &fd); Bazen çokbiçimlilik yalnızca araya girme, yani kancalama işlemi için kullanılır, yani biz CFile sınıfından bir sınıf türetip o sınıf için Read(), Write() fonksiyonlarını yazarken yine taban sınıfın orjinal fonksiyonlarını çağırarak işlemlerimizi yaparız ama bu arada bazı ek işlemleri de araya sokarız. virtual CExtFile::Read(...) { //... return CFile::Read(...); } İleri Template İşlemleri Template bir fonksiyonu ya da sınıfı derleyicinin belirli bir türe göre yazması anlamına gelir. Tek bir fonksiyon ya da bir sınıfın tamamı template olarak yazılabilir. Bir template fonksiyon 23 çağırıldığında derleyici çağırılma ifadesindeki parametrelere bakarak template fonksiyonu o parametrelere göre yazar. Açısal parantezler içerisinde class anahtar sözcüğü yerine typename anahtar sözcüğü de kullanılabilmektedir. Template konusu C++’a sonradan eklenmiş ve geliştirilmiş bir konudur. Maalesef derleyiciler arasında template özelliklerinde ciddi biçimde uyumsuzluklar bulunabilmektedir. C++’ın son 1998 standardı bazı derleyiciler tarafından tam olarak desteklenememektedir. Derleyici bir template fonksiyon ya da sınıf ile karşılaştığında bazı syntax kontrollerini o anda yapar. Ancak henüz template parametresinin türü belli olmadığı için bazı kontrolleri template açılımını yaparken yapmaktadır. Örneğin: tepmlate <class T> void Func(T &a) { a.Func2(); //... } Burada derleyici T türünü henüz bilmediği için a.Func2() işleminde her hangi bir error bildirimi yapmaz. Ancak Func() fonksiyonu şöyle çağırılmış olsun: Func(x); Şimdi derleyici template açılımını yaparken x’in türüne bakacaktır, x bir sınıf türünden değilse ya da sınıf türündense ama Func2() isimli bir üye fonksiyonu yoksa işlem error ile sonuçlanacaktır. Özetle derleyiciler templateler için syntax kontrolünü iki aşamada yaparlar: 1- Template bildirimini gördüklerinde 2- Tamplate açılımlarının yapıldığında Template parametresi template fonksiyonlarda fonksiyonun çağırılma biçimine göre normal bir tür ya da bir gösterici türü olabilir. Örneğin: template <class T> void Func(T a) { T p; //... } Burada eğer fonksiyon, int a[10]; Func(a); biçiminde çağırılırsa T türü int * anlamına gelir. Dolayısıyla template fonksiyonu içerisindeki ‘p’ int * türünden bir göstericidir. Halbuki fonksiyon, Func(20); 24 biçiminde çağırılsaydı T int anlamına gelecekti, dolayısıyla p de int türünden bir değişken olacaktı. Bazı derleyiciler template fonksiyonları yalnızca template açılımlarını gördüklerinde syntax bakımından değerlendirirler (derleyicinin template fonksiyonun çağırıldığını gördüğünde ya da bir tamplate sınıf nesnesinin tanımlandığını gördüğünde yaptığı işlemlere template açılımı denilmektedir). Bir template fonksiyonuyla aynı isimli normal bir fonksiyon olabilir. Bu durumda fonksiyon çağırıldığında derleyici önce parametrenin normal fonksiyon ile uyuşup uyuşmadığına bakar, normal fonksiyon uyuşuyorsa normal fonksiyonunun çağırıldığını varsayar. Uyuşmuyorsa template açılımı uygular. Bir template fonksiyon parametresine bakılmaksızın belirli türden açılmaya zorlanabilir. Örneğin: Func<double>(10); Burada fonksiyonun parametresi int türünden olduğu halde biz derleyicinin double açılımı yapmasını isteyebiliriz. Bu sayede normal fonksiyon yerine template açılımının da uygulanması sağlanabilir. Örneğin: y = abs<int>(x); Burada abs() fonksiyonu için normal bir fonksiyon bulunmasına karşın template açılımı kullanılmıştır. Bu özellik 1996 ve sonrasında kabul edilmiştir. Derleyici template fonksiyon bildirimini gördüğünde bellekte her hangi bir yer ayırmaz. Template açılımı yapıldığında fonksiyon yazılır. Template fonksiyonların ve sınıfların bildirimleri başlık dosyalarında tutulmalıdır, çünkü her derleme aşamasında derleyici tarafından kullanılmaktadır. Açılımı yapılmayan template bildirimlerinin koda olumsuz bir etkisi olmaz. Template sınıflarda bir sınıfın tüm üye fonksiyonları açılım sırasında derleyici tarafından yazılır. Template bir sınıf türünden nesne tanımlanırken template açılımının açısal parantezlerle belirtilmesi gerekir. Örneğin: list<int> a; Bir tamplate sınıfta template parametresi default bir tür ismi alabilir. Örneğin: template <class T1 = int, class T2 = float> class Sample { //... }; Bu default template parametreleri açılımda belirtilmezse etkili olur. Sample<> a; Sample<long> a; Sample<short, long> a; 25 Template bir sınıf hiçbir yerde yalnızca sınıf ismi ile kullanılamaz. Açısal paramtezlerle açılım belirtilerek kullanılır. Fonksiyonların parametreleri ya da geri dönüş değerleri bir template sınıf türünden olabilir, tabii template türünün belirtilmesi gerekir. Örneğin: Sample<int>Func(); void Func(Sample<int> *p); Template fonksiyonlar genellikle sınıf içinde yazılır ve bitirilir. Template sınıfının üye fonksiyonu dışarıda yazılacaksa bir template fonksyon gibi yazılmalıdır ve sınıf isminden sonra açılım türü de belirtilmelidir. Örneğin: template <class T> void Sample<T>::Func() { //... } Normal bir sınıfın her hangi bir fonksiyonu template fonksiyon olabilir (İngilizce member template denilmektedir). Örneğin: class Sample { public: template <class T> Sample(T a); //... }; (VisualC++ 6 member template konusunu desteklememektedir) Template bir sınıf taban sınıf olarak kullanılabilir ya da taban sınıf template sınıf olmadığı halde türemiş sınıf template sınıf olabilir. Birinci durumda tabii yine template türünün belirtilmesi gerekir. Örneğin: template <class T> class A { //... }; class B : public A<int> { //... }; /* or */ class A { //... }; template <class T> class B : public A { //... }; 26 Hem taban hem de türemiş sınıf template sınıf olabilir. Bu durumda türemiş sınıfta taban sınıf belirtilirken yine template türü geçirilmelidir. Tabii türemiş sınıftaki template parametresi taban sınıf template türü olarak verilebilir. Örneğin: template <class T> class A { //... }; template <class T> class B : public A<T> { //... }; Template sınıfının template parametresi (yani T) bütün sınıf içinde ve üye fonksiyonlarının içerisinde bir tür ismi olarak kullanılabilir. Template sınıf türünden nesne tanımlarken template türü başka bir template sınıf olabilir. Örneğin: Queue<list<int> > queue; Burada derleyici önce list sınıfını int türü ile açar, Queue sınıfının template parametresinin türü int türüne açılmış list olur. Yani bu örnekte her elemanı bağlı liste olan bir kuyruk sınıfı oluşturulmuştur. Bağlı listede int türden bilgiler tutulmaktadır (burada ifadenin shift operatörü (>>) ile karışmaması için araya boşluk bırakılması gerekir). Template sınıf ile aynı isimli normal bir sınıf olabilir ama normal sınıfın template açılım türü belirtilmelidir. Örneğin: template <class T> class Sample { //... }; template <> // Bu yazılmak zorunda değil class Sample<int> { //... }; Burada aşağıdaki sınıf bir template sınıf değildir. Sınıfın başındaki template bildirimi yazılmayabilir. Şimdi sınıf, Sample<int> a; biçiminde açılırsa aşağıdaki template olmayan sınıf kullanılacaktır. 27 Standart Template Kütüphanesi (STL) STL ilk kez HP şirketi programcıları tarafından geliştirilmiş ve kullanılmıştır. 1996’da C++’ın standardizasyon taslaklarında STL C++’ın standart kütüphanesi olarak kabul edilmiştir. STL tamamen template sınıflar ve fonksiyonlardan oluşan geniş bir kütüphanedir. STL kullanabilmek için ilgili fonksiyonun ya da sınıfın bulunduğu başlık dosyası include edilmelidir. STL fonksiyonları ve sınıfları guruplandırılarak çeşitli başlık dosyalarının içerisine yerleştirilmiştir. Eskiden STL sınıflarınınn ve fonksisyonlarını bulunduğu başlık dosyasının uzantısı *.h biçimindeydi. 1996 ve sonrasında bu uygulamaya son verilmiştir. Şimdi STL kütüphanesi uzantısı olmayan dosyaların içerisindedir. Örneğin eskiden bağlı liste sınıfı list.h içerisindeydi, şimdi yalnızca list dosyası içerisindedir. Bugünkü derleyicilerin çoğu hem *.h uzantılı dosyaları hem de uzantısız dosyaları bulundurmaktadır, dolayısıyla eskiden yazılmış kodlar problemsiz derlenmektedir. Tabii derleyicilerin standardizasyon öncesi dönemi desteklemesi zorunlu değildir. Tüm STL kodları bu dosyalar içerisinde olduğu için nasıl yazıldıkları kolaylıkla incelenebilir. STL kütüphansei üç tür elemandan oluşur: 1- Algoritmalar: STL içerisindeki global template fonksiyonlara algoritma denilmektedir. Bu fonksiyonlar özellikle bazı operatörler kullanılarak yazılmıştır, bu yüzden diğer template sınıflar ile birlikte kullanılabilir. 2- Nesne tutan sınıflar (container classes): İçerisinde birden fazla nesnenin tutulduğu, çeşitli veri yapılarının uygulandığı sınıflara nesne tutan sınıflar denir (Container class nesne yönelimli terminolojide genel bir terimdir. Container class terimi ile collection terimi eş anlamlı olarak kullanılmaktadır). Örneğin dizileri temsil eden sınıflar, kuyruk sınıfları, bağlı liste sınıfları tipik birer nesne tutan sınıftır. STL içerisinde bazı nesne tutan sınıflar başka nesne tutan sınıflardan faydalanılarak yazılmıştır. Örneğin, stack sınıfı deqeue sınıfı kullanılarak yazılmıştır. Böylecene bu tür sınıflara adaptör sınıflar (STL adaptors) denilmektedir. 3- Yararlı sınıflar (utility classes): Nesne tutma amacında olmayan genel sınıflardır. Nesne yönelimli programlama tekniğindeki en büyük gelişmelerden biri veri yapılarının standart bir biçimde sınıflarla temsil edilmesidir. Örneğin STL sayesinde programcının gereksinim duyacağı neredeyse algoritmik herşey standart olarak yazılmıştır. STL içerisinde olmayan veri yapıları ve algoritmalar STL kullanılarak programcılar tarafından yazılabilir. Her programcının aynı biçimdeki veri yapıları ve algoritmalar üzerinde çalışması kodların anlaşılmasını kolaylaştırmaktadır. STL içerisinde tüm temel algoritmalarının veri yapılarının bulunması işleri kolaylaştırmakla birlikte bütün problemleri kendi başına çözmemektedir. Pogramcının algoritmalar ve veri yapıları arasındaki farkları bilmesi, duruma göre bunlardan birini seçmesi gerekir. Hangi veri yapısının ve algoritmanın kullanılacağı yine belli düzeyde bir bilgi gerektirmektedir. STL sınıflarının tasarımında çokbiçimlilik (polymorphism) performansı düşürür gerekçesiyle kullanılmamıştır. Yani sınıflar bir türetme ilişkisi içerisinde değil, bağımsız bir biçimde bulunur. Ancak programcı isterse STL sınıflarından türetme yapabilir. Yine STL sınıflarında iostream 28 sistemi dışında türetme kullanılmamıştır. Exception handling mekanizması çok az düzeyde kullanılmıştır. İsim Aralığı (Namespace) Faaliyet Alanı Bir isim aralığı (namespace) global faaliyet alanında tanımlanmış bir bloktur. Bir isim aralığı içerisindeki değişkenler ve fonksiyonlar yine global düzeydedir, ancak erişim yapılırken namespace ismi çözünürlük operatörüyle (::) belirtilmek zorundadır. Bir isim eğer namespace ismi belirtilmemişse kendi namespace’i ve kapsayan namespace faaliyet alanlarında otomatik olarak aranır. Hiçbir namespace içerisinde olmayan global bölgeye “global namespace” denilmektedir. Global namespace diğer isim aralıklarını kapsar. Bir isim aralığının içerisinde prototipi bildirilmiş bir fonksiyon ya da bildirimi yapılmış bir sınıf başka bir isim aralığında tanımlanamaz. Global namespace içerisinde tanımlanabilir. Örneğin: namespace X { void Func(); } namespace Y { void X::Func() { } } void X::Func() { } /* error */ /* doğru */ Bir namespace bildiriminden sonra aynı namespace tekrar bildirilirse bu namespace tanımlamaları tekbir tanımlamaymış gibi birleştirilir. Örneğin: namespace X { int a; //... } //... namespace X { int b; //... } Tüm STL elemanları std isimli bir isim aralığında tanımlanmıştır. Eğer using bildirimi kullanılmamışsa STL isimlerini kullanırken bu namespace ismini eklemek gerekir. Örneğin: std::list<int> x; 29 using Bildirimi using bildirimi bir namespace içerisindeki isme erişirken namespace ismini kullanmadan erişimi gerçekleştirmek amacıyla kullanılır. using bildirimi iki biçimde kullanılır: 1- using namespace <namespace_ismi>; Örneğin: using namespace std; 2- using <namespace_ismi>::<namespace içerisindeki bir isim> Örneğin: using std::cout; Genellikle birinci çeşit using bildirimiyle sıklıkla karşılaşılır. Birinci çeşit using bildirimi global bir alana yerleştirilebilir, herhangi bir namespace içerisine yerleştirilebilir ya da herhangi bir blok içerisine yerleştirilebilir, sınıf bildirimi içerisine yerleştirilemez. Birinci çeşit using bildirimi nereye yerleştirilirse o yerleştirildiği faaliyet alanında aranacak her isim using bildiriminde belirtilen faaliyet alanında da aranır. Birinci çeşit namespace bildiriminde eğer bir isim namespace bildiriminin yerleştirildiği yerde varsa, using bildirimiyle belirtilen namespace içerisinde de varsa bu durum iki anlamlılık hatasına yol açar. Benzer biçimde bir isim using bildirimiyle belirtilmiş birden fazla namespace içerisinde varsa bu durum da error oluşturmaktadır. using bildirimlerini *.h dosyaları içerisine yerleştirmek tavsiye edilen bir yöntem değildir, çünkü bu durumda o *.h dosyasını include eden her modülde using bildirimi global düzeyde etkili olacaktır. Birinci çeşit using bildiriminde namespace anahtar sözcüğünden sonra kesinlikle namespace ismi gelmelidir, sınıf ismi gelirse error oluşur. İkinci çeşit using bildirimi seyrek kullanılır. Bu bildirimde using anahtar sözcüğünü sırasıyla bir namespace ismi sonra çözünürlük operatörü ve namespace içerisindeki bir isim izler. Örneğin: using A::B::Func; Birinci çeşit using bildiriminde tüm bir namespace faaliyet alanına dahil edilmektedir. Halbuki ikinci çeşit using bildiriminde yalnızca using bildiriminde belirtilen isim using bildiriminde belirtilen faaliyet alanında aranmaktadır. Örneğin, biz std namespace’i içerisindeki cout için global düzeyde using std::cout; bildirimini yapmış olalım. Şimdi biz cout nesnesini std namespace ismini belirtmeden doğrudan kullandığımızda bir problem oluşmaz, ancak bu durum sadece cout ismi için söz konusudur. Birinci çeşit using bildiriminin sonunda bir namespace ismi, ikinci çeşit using bildiriminin sonunda bir namespace içerisindeki ismin bulunduğuna dikkat edilmelidir. Örneğin: 30 using namespace A::B::C; using A::B::C::x; İkinci çeşit using bildirimi her yere yerleştirilebilir, sınıf bildirimi içerisine de yerleştirilebilir. Sınıf bildirimi içerisine yerleştirilirse namespace ismi yerine sınıfın taban sınıflarından birinin ismi kullanılmak zorundadır. Örneğin: class A { protected: int m_a; //... }; class B : public A { public: using A::m_a; //... }; Sınıf içerisinde ikinci çeşit using bildiriminin kullanılması özellikle taban sınıftaki bir ismin (protected bölümündeki bir ismin) türemiş sınıfta başka bir bölüme (örneğin private ya da public bölüme) aktarılması için kullanılmaktadır. Yukardaki örnekte bir B türünden bir nesneyle A’nın m_a elemanına erişemezdik, ancak using bildirimiyle A’daki m_a B sınıfında public bölüme aktarılmıştır, dolayısıyla artık erişebiliriz. STL string Sınıfı C++’ın standart kütüphanesinde yazı işlerini kolaylaştırmak için kullanılan bir string sınıfı vardır. STL tamamen template tabanlı bir kütüphanedir, yani bütün global fonksiyonlar template fonksiyonlar, bütün sınıflar da template sınıflardır. İşte aslında string sınıfı basic_string template sınıfından yapılmış bir typedef ismidir. string ismi aşağıdaki gibi typedef edilmiştir: typedef basic_string<char> string; Yani yazı işlemleri için asıl template sınıf basic_string template sınıfıdır, string ismi bu sınıfın char türü için açılmış halidir. basic_string template sınıfı “string” dosyası içerisindedir (dosyanın *.h biçiminde uzantısı yoktur). basic_string sınıfı üç template parametresi içeren genel bir sınıftır. template <class E, class T = char_traits<E>, class A = allocator<E> > class basic_string { //... }; Birinci template parametresinin verilmesi zorunludur, bu tür yazının her bir karakterinin hangi türden olduğunu anlatır. Örneğin ASCII yazıların her bir karakteri 1 byte’dır ve tipik olarak char 31 türüyle temsil edilir, ancak UNICODE yazılarda her bir karakter 2 byte yer kaplar ve wchar_t ile temsil edilir. Bu durumda bir ASCII yazıyı tutmak için nesne basic_string<char> x; biçiminde tanımlanır, UNICODE yazıyı tutmak için nesne basic_string<wchar_t> x; biçiminde tanımlanır. Genellikle ASCII yazılar yoğun olarak kullanıldığından işlemi kolaylaştırmak için string typedef ismi bildirilmiştir. Yani, basic_string<char> str; ile string str; aynı anlamdadır. basic_string sınıfının ikinci template parametresi default olarak char_traits sınıfı türündendir. char_traits bir STL sınıfıdır ve iki karakteri karşılaştıran static üye fonksiyonları vardır. basic_string sınıfının karşılaştırma fonksiyonları bu sınıftaki static fonksiyonlar çağırılarak yazılmıştır. Örneğin, basic_string sınıfının < operatör fonksiyonu, ikinci template parametresiyle belirtilen sınıfın lt() ve eq() static fonksiyonlarını çağırarak yazılmış olsun, template <class E, class T = char_traits<E>, class A = allocator<E> > bool basic_string<E, T, A>::operator <(const E *pStr) { for (int i = 0; i < SIZE; i++) { if (T::lt(m_pBuf[i], pStr[i])) return true; if (!T::eq(m_pBuf[i], pStr[i])) return false; } return false; } char_trait sınıfının iki karakteri karşılaştıran üye fonksiyonları ASCII karakter tablosunu temel alarak işlemlerini yapmaktadır. Biz örneğin karşılaştırma işlemlerinin Türkçe yapılmasını istersek char_trait sınıfının elemanlarını başka bir sınıf adı altında ama Türkçe’ye uygun bir biçimde yazmalıyız, böylece basic_string sınıfına hiç dokunmadan onun işlevini değiştirmiş oluruz. Örneğin bu sınıfın ismi trk_traits olsun. typedef std::basic_string<char, trk_traits> trkstring; trkstring a(“ılgaz”); trkstring b(“ismail”); 32 if (a < b) { //... } basic_string sınıfının üçüncü template parametresi default olarak allocator türündendir. Neredeyse tüm STL template sınıfları böyle bir allocator template parametresi almaktadır. Aslında STL içerisinde dinamik tahsisatlar doğrudan new operatörüyle yapılmamıştır, template argümanı olarak belirtilen sınıfın static üye fonksiyonu çağırılarak yapılmıştır. allocator bir STL sınıfıdır ve default olarak bu sınıfın tahsisat yapan fonksiyonu new operatörünü kullanmaktadır. Programcı başka isimde yeni bir tahsisat sınıfı yazabilir ve böylece tüm STL sınıfları o sınıfın tahsisat fonksiyonunu çağıracak duruma gelir. Tahsisat sınıfının kullanım ve anlamı ileride ele alınacaktır. basic_string Sınıfının Üye Fonksiyonları Başlangıç Fonksiyonları: Sınıfın daha önce yazmış olduğumuz CString sınıfına benzer parametre yapıları içeren şu başlangıç fonksiyonları vardır: 1- basic_string(); Default başlangıç fonksiyonudur. 2- basic_string(const basic_string &str); Kopya başlangıç fonksiyonudur. 3- basic_string(const basic_string &str, size_type pos, size_type n); size_type, basic_string sınıfı içerisinde bildirilmiş bir typedef ismidir. Bu typedef default olarak size_t türündendir. Bu başlangıç fonksiyonu başka bir basic_string nesnesinin belirli bir karakterinden başlayarak n tane karakteri alıp nesneyi oluşturur. Örneğin: string a(“ankara”); string b(a, 2, 4); // b = kara 4- basic_string(const E *str, size_type n); Adresiyle verilmiş bir yazının ilk n karakterinden nesne oluşturur. Örneğin: string a(“ankara”, 3); // a = ank 5- basic_string(const E *str); Parametresiyle belirtilen adresten ‘\0’ görene kadarki kısımdan yazıyı oluşturur. En çok kullanılan constructor’dur. Örneğin: string a(“ankara”); 33 6- basic_string(size_type n, E ch); Aynı karakterden n tane olan bir nesne oluşturur. Örneğin: string a(4, ‘a’); string b(“aaaa”); assert(a == b); 7- basic_string(const iterator first, const iterator last); İki iterator arasından nesne oluşturur (iterator konusu STL’in en önemli kavramlarından biridir, ileride ele alınacaktır). Atama Operatör Fonksiyonları: Bilindiği gibi atama operatör fonksiyonları string sınıfı için yazının tutulduğu eski alanı boşaltıp yeni yazı için yeni bir alan tahsis etme eğilimindedir. 1- basic_string &operator =(const basic_string &str); İki basic_string nesnesinin atanmasında kullanılır. Örneğin: string a(“ankara”); string b = “istanbul”; b = a; assert(a == b); 2- basic_string &operator =(const E *str); Adresiyle verilmiş olan bir yazıyı atamakta kullanılır. Örneğin: string a(“ankara”); a = “istanbul”; 3- basic_string &operator =(E ch); Nesnenin tek bir karakterden oluşan yazıyı tutmasını sağlar. Örneğin: string a; a = ‘x’; Atama operatör fonksiyonlarının hepsinin geri dönüş değeri sol taraftaki nesnenin kendisidir, yani aşağıdaki işlem geçerlidir: string a(“ankara”), b, c; c = b = a; Anahtar Notlar: ostream türünden cout nesnesi 1996 ve sonrasında string türünü de yazdıracak operatör fonksiyonuna sahip olmuştur. 1996 ve sonrasında başlık dosyalarının uzantısı kaldırıldığından ancak eski pek çok derleyici *.h uzantılı eski sistemi de desteklediğinden bir karmaşa doğabilir. Şöyle ki, biz iostream.h dosyasını include edersek eski iostream 34 kütüphanesini kullanıyor duruma düşeriz, bu durumda cout ile string türünü yazdıramayız. cout ile string türünü yazdırabilmek için uzantısı olmayan iostream dosyasının include edilmesi gerekir. string Sınıfının Diğer Operatör Fonksiyonları string sınıfının, CString sınıfında olduğu gibi bütün karşılaştırma operatörlerine ilişkin operatör fonksiyonları vardır. Bu fonksiyonlar yazıları içerik bakımından karşılaştırmaktadır. Bu operatör fonksiyonlarının operandlarından biri string diğeri string ya da const char * türünden olabilir. Örneğin: string s = “ankara”; string k = “izmir”; if (s == k) { //... } if (s < “samsun”) { //... } Bu operatör fonksiyonlarının birinci operandının string türünden olması zorunlu değildir, çünkü birinci operandı string olmayan fonksiyonlar global operatör fonksiyonuyla yazılmışlardır. Karşılaştırma operatör fonksiyonları yerine tıpkı strcmp() fonksiyonunda olduğu gibi üç durumu da belirten compare() üye fonksiyonu kullanılabilir. int compare(const char *str) const; int compare(const string &) const; Fonksiyonlar birinci yazı ikinci yazıdan büyükse pozitif herhangi bir değere, küçükse negatif bir değere, eşitse sıfır değerine geri dönerler. Sınıfın daha ayrıntılı işlem yapmaya yarayan farklı parametreli compare() fonksiyonları da vardır. string sınıfının += ve + operatör fonksiyonları yazının sonuna başka bir yazı eklemek için ya da iki yazıyı toplamak için kullanılmaktadır. += operatör fonksiyonu yerine sınıfın append() üye fonksiyonu da kullanılabilir. += ve + operatör fonksiyonlarının bir operandı string türündendir, diğer operand string, const char * ya da char türünden olabilir. += fonksiyonunun geri dönüş değeri string türünden referans, + operatör fonksiyonunun ise string türündendir. 35 Sınıfın elemana erişim için kullanılan bir [] operatör fonksiyonu vardır. char &string::operator [](size_type idx); char string::operator [](size_type idx) const; Bu fonksiyonlarda [] içerisindeki index değeri yazının uzunluğundan büyük olursa gösterici hatasına yol açar. Elemana erişme işlemi add() üye fonksiyonuyla da yapılabilir. add() üye fonksiyonu kullanılırsa index değeri yazının uzunluğunu aştığında out_of_range isimli exception oluşur. STL string sınıfında yazıların sonunda ‘\0’ bulunmamaktadır (sınıf içerisinde yazının uzunluğu tutulduğu için ‘\0’ ın ayrıca yer kaplaması istenmemiş olabilir). Sınıfın tuttuğu yazının karakter uzunluğu length() üye fonksiyonuyla alınabilir. size_type length() const; Bu durumda yazının sonuna kadar karakter karakter ilerlemek için aşağıdaki yöntem kullanılmalıdır: for (int i = 0; i < s.length(); ++i) cout << s[i] << endl; (Döngü içerisinde sürekli length() fonksiyonunun çağırılması performans problemine yol açmaz. Çünkü template fonksiyonlar inline olarak yazılmıştır.) string sınıfının size() üye fonksiyonu length() üye fonksiyonuyla eşdeğerdir. string Sınıfının Yaralı Fonksiyonları string sınıfının bir grup arama işlemini yapan find() ve rfind() isimli üye fonksiyonları vardır. Bu fonksiyonlar yazı içerisinde arama yaparlar. Fonksiyonların parametresi string, const char * veya char türünden olabilmektedir. Fonksiyonlar yazının bulunduğu yerin index numarasıyla geri döner. rfind() aramayı sondan başa doğru yapar. Fonksiyonlar başarısızlık durumunda string sınıfı içerisindeki npos değerine geri dönerler. string s(“ankara”); int index; index = s.find(“kara”); if (index == string::npos) { cerr << "error olustu" << endl; exit(1); } cout << index << endl; Daha ayrıntılı parametrelere sahip olan find() ve rfind() fonksiyonları da vardır. 36 substr() fonksiyonu yazının belirli bir kısmından yeni bir yazı oluşturur. string substr(size_type idx) const; string substr(size_type idx, size_type len) const; idx parametrsi başlangıç offsetini, len parametresi ise o offsetten sonraki karakter sayısını anlatır. len parametresi yazılmazsa geri kalan tüm yazı alınır. string s(“ankara”); int index; index = s.find(“kara”); if (index == string::npos) { cerr << "error olustu" << endl; exit(1); } string t; t = s.substr(index, 2); replace() fonksiyonu belirli bir offsetten başlayarak belirli bir uzunluktaki karakterleri başka bir yazıyla değiştirir. Sınıfın clear() üye fonksiyonu ya da erase() üye fonksiyonu parametresiz kullanılırsa sınıftaki tüm yazıyı siler. Belirli bir aralığı silen versiyonları da vardır. Sınıfın insert() fonksiyonları da vardır. Ayrıca string sınıfı iteratör işlemlerini de desteklemektedir. string sınıfının okuma yapan bir >> operatör fonksiyonu vardır, ancak okuma ilk boşluk karakteri görüldüğünde sonlandırılır. string sınıfının tasarımını yaptığımız CString sınıfında olduğu gibi const char * türüne tür dönüştürme yapan operatör fonksiyonu yoktur, çünkü yazının saklandığı alan ‘\0’ kullanılmamıştır. Sınıfın c_str() üye fonksiyonu bu eksikliği kapatmak için tasarlanmıştır. const char *c_str() const; Bu fonksiyon sınıf içerisinde tutulan yazıyı başka bir alana taşır, yazının sonuna ‘\0’ koyar ve o alanına adresiyle geri döner. c_str() fonksiyonu string nesnesi içerisinde yazının tutulduğu bölgenin adresiyle geri dönmez, yazıyı başka bir bölgeye taşır oranın adresiyle geri döner. (c_str() üye fonksiyonu sınıf içerisindeki yazıyı taşırken taşıma alanı olarak nereyi kullanmaktadır? Bu durum standardizasyonda belirtilmemiştir ancak uygulamada derleyiciler şu yöntemlerden birini kullanmaktadır: Yeni bir dinamik alan yaratma. Bu yöntemde programcı c_str() fonksiyonunu çağırdığında tıpkı yazının tutulduğu gibi yeni bir dinamik alan tahsis edilir. Bu alan bitiş fonksiyonu tarafından free hale getirilmektedir ya da bu alan sınıfın içerisinde normal bir dizi olarak da tahsis edilebilir.) 37 string sınıfının data() üye fonksiyonu tamamen c_str() fonksiyonu gibi kullanılır ancak bu fonksiyon doğrudan yazının bulunduğu bölgenin adresiyle geri döner. Programcı sonunda ‘\0’ olmadığını hesaba katmalıdır. STL string Sınıfının İçsel Tasarımı string sınıfı genellikle CString uygulamasında olduğu gibi daha geniş bir tampon bölge tahsis etme yöntemiyle tasarlanmıştır. Yani yazının tutulduğu blok yazının uzunluğundan büyük olabilmektedir. Sınıfın c_str() fonksiyonu başka bir blok tahsisatına yol açmaktadır. Bu fonksiyon yazıyı yeni bloğa taşıyarak sonuna ‘\0’ ekleyip taşıdığı bloğun adresiyle geri dönmektedir. Sınıfın capacity() fonksiyonu yeniden tahsisat yapılamayacak maksimum yazı uzunluğunu verir. Bu da aslında yazının saklanmasında kullanılan bloğun uzunluğudur. Sınıfın resize() fonksiyonları sınıfın tuttuğu yazıyı büyültüp küçültmekte kullanılır. void resize(size_type num); void resize(size_type num, char c); Fonksiyonun birinci versiyonunda büyütme yapıldığında belirtilen kısım ‘\0’ karakterler ile, ikinci versiyonunda ikinci parametreyle belirtilen karakterler ile doldurulur. Bu işlemden sonra size() üye fonksiyonu çağırıldığında uzunluk değişir. reserve() üye fonksiyonu yazının tutulduğu blok büyüklüğünü değiştirmekte kullanılır. void reserve(size_type n = 0); Kapasite önceki değerinden küçültülürse (örneğin default argüman 0 değerini alırsa) blok en fazla yazının uzunluğu kadar küçültülür. Bu fonksiyon bir nesne üzerinde yoğun işlemler yapılırken tahsisat sayısının azaltılması amacıyla capacity değerini yükseltmek için kullanılmaktadır. Klavyeden okuma sırasında doğrudan cin nesnesi yerine STL global getline() fonksiyonu kullanılabilir. Kullanımı şöyledir: string s; getline(cin, s); string sınıfının max_size() fonksiyonu nesnenin teorik olarak tutabileceği maksimum karakter sayısını vermektedir. Bu değer sistemin limit değeridir ve sistemden sisteme değişebilir, pratik bir anlamı yoktur. 38 STL Sınıflarında Kullanılan Tür İsimleri STL sınıflarının içerisinde kendi sınıfıyla ilgili pek çok typedef edilmiş tür ismi tanımlanmıştır. Bu tür isimleri fonksiyonların parametrik yapılarında kullanılmıştır. Programcı bu fonksiyonlara parametre geçerken ya da geri dönüş değerlerini kullanırken tür uyuşumunu sağlamak için bu typedef isimlerinden faydalanabilir. Bu tür isimlerinin gerçek C++ karşılığının ne olacağı standardizasyonda belirtilmemiştir, derleyicileri yazanlara bırakılmıştır. Örneğin string sınıfında tanımlanan size_type tür ismi pek çok derleyicide unsigned int biçimindedir, ancak unsigned int olmak zorunda değildir. Örneğin standardizasyonda sınıfın find() üye fonksiyonunun geri dönüş değeri size_type türü olarak belirtilmiştir. Parametrik uyumu korumak için int ya da unsigned int yerine bu tür isminin kullanılması daha uygundur. string s(“anakara”); string::size_type pos; pos = s.find(‘k’); Hemen her STL sınıfında pointer, const_pointer, reference ve const_reference türleri template açılım türünden gösterici ve referans biçiminde typedef edilmiştir. Template parametresi T olmak üzere bu typedefler şöyledir: typedef typedef typedef typedef T *pointer; const T *const_pointer; T &reference; const T &const_reference; Algoritma Analizi Bir problemi tam olarak çözen adımlar topluluğuna algoritma denir. Algoritmaların karşılaştırılmasında iki ölçüt kullanılır: hız ve kaynak kullanımı. Ancak hız ölçütü algoritmaların karşılaştırılmasında çok daha önemli bir ölçüt olarak kabul edilmektedir. Algoritmaları hız bakımından karşılaştırmak ve matematiksel bir ifade ile durumu belirlemek çoğu zaman çok zor hatta olanaksızdır. Çünkü örneğin bir dizi üzerinde işlemler yapılırken algoritma dizinin dağılımına göre bir akış izleyebilir ve konu olasılık ve istatistiksel yöntemlere kayar. Algoritmaları hız bakımından kıyaslamak için pratik yöntemler önerilmiştir. Bu pratik yöntemlerden en çok kullanılanı algoritmanın karmaşıklığı (complexity of algorithm)’dır. Algoritmaların kesin kıyaslanması için en iyi yöntem şüphesiz simülasyon yöntemidir. Algoritmanın karmaşıklığını belirlemek için algoritma içerisinde bir işlem seçilir (bu işlem çoğu kez bir if değimi olur) ve algoritmayı çözüme götürmek için en kötü olasılıkla ve ortalama olasılıkla bu işlemden kaç tane gerektiğine bakılır. Dizi işlemlerinde karmaşıklık genellikle dizinin uzunluğunun bir fonksiyonudur. Örneğin, n elemanlı bir dizide bir eleman aranacak olsun, bunun için işlem olarak if değimi seçilebilir, karmaşıklık en kötü olasılıkla n, ortalama (n+1)/2 dir. Algoritmanın karmaşıklığının kesin sayısını bulmak bile bazı durumlarda çok zor olabilmektedir. Bu nedenle karmaşıklıkları sınıflara ayırarak algoritmaları karşılaştırma yöntemine gidilmiştir. Algoritmaları sınıflara ayırarak karşılaştıran yöntemlerden bir tanesi Big O yöntemidir. Big O yöntemine göre en iyiden kötüye doğru karmaşıklık sınıfları şunlardır: 39 O(1) O(log n) O(n) O(n log n) O( n 2 ) O( n 3 ) O( 2 n ) sabit logaritmik doğrusal doğrusal logaritmik karesel küpsel üstel Algoritma hiç bir döngü içermiyorsa sabit zamanlıdır, yani çok hızlıdır O(1) ile gösterilir. Algoritma dizinin uzunluğu n olmak üzere, n’in 2 tabanına göre logaritması logaritmik karmaşıklığa sahiptir. Örneğin ikili arama (binary search) işleminin karmaşıklığı log n2 ’dir. Logaritmik karmaşıklıklara kendi kendini çağıran fonksiyonlarda da rastlanır. Program tekil bir döngü içeriyorsa (iç içe olmayan ama ayrık birden fazla olabilen) karmaşıklık doğrusaldır. Karmaşıklık hem logaritmik hem de doğrusal olabilir (quick sort algoritmasında olduğu gibi). Nihayet algoritma iç içe iki döngü içeriyorsa karesel, üç döngü içeriyorsa küpseldir. İki algoritma bu biçimde kategorilere ayrılarak temel bir hız belirlemesi yapılabilir. Daha kesin bir değerlendirme için en kötü ve ortalama olasılıktaki kesin değerler hesaplanabilir. Kesin değerler O(n) = f(n) notasyonu ile gösterilir. Örneğin rastgele bir dizi içerisindeki arama karmaşıklığı O(n) = (n+1)/2’dir. Veri Yapılarındaki Erişim Karmaşıklığı Kullanılan veri yapısının en önemli parametresinden birisi herhangi bir elemana erişileceği zaman bunun zamansal maliyetidir. Bazı veri yapılarında erişim sabit zamanlı (rastgele), bazılarında doğrusal, bazılarında logaritmik olabilir. Bazı veri yapılarında çeşitli özel elemanlara erişmek ile herhangi bir elemana erişmenin karmaşıklığı farklı olabilmektedir. Örneğin, bağlı liste uygulamalarında genellikle listenin ilk ve son elemanı bir göstericide tutulur, bu durumda listenin ilk ve son elemanına erişmek sabit zamanlı bir işlemdir. Ancak herhangi bir elemana erişmenin karmaşıklığı O(n) = (n + 1)/2, yani doğrusaldır. Örneğin ikili ağaç yapısında ağaç tam dengelenmişse herhangi bir elemana erişim logaritmik karmaşıklıktadır. Dizilerde herhangi bir elemana erişim sabit zamanlıdır. STL’de Nesne Tutan Sınıflar STL’de temel veri yapıları ile nesne tutan pek çok sınıf vardır. Bir veri yapısının nerelerde kullanılacağını bilmek gerekir. STL içerisinde bu sınıflar hazırdır, ancak programlama sırasında bu kararın verilmesi tamamen programcıya bağlıdır. Bir programda hangi nesne tutan sınıfı kullanacağımız uygulamamıza ve o nesne tutan sınıfın algoritmik yapısına bağlıdır. STL Bağlı Liste Sınıfı Bildirimi “list” dosyasında olan list isimli template sınıf bağlı liste işlemlerinde kullanılmaktadır. Bağlı liste, elemanları ardışıl bulunmak zorunda olmayan dizilere denir. Bağlı 40 listenin her elemanı sonraki elemanın yerini gösterir, ilk ve son elemanın yeri sınıfın veri elemanlarında tutulur. Tek bağlı listelerde bir elemandan yalnızca ileriye doğru gidilebilir, halbuki çift bağlı listelerde her eleman sonrakinin ve öncekinin yerini tuttuğu için ileri ya da geri gitme işlemi yapılabilmektedir. list sınıfı çift bağlı liste şeklinde oluşturulmuştur. Bağlı listelerde ilk ve son elemana erişmek sabit zamanlı işlemlerdir, herhangi bir elemana erişilmesi doğrusal karmaşıklığa sahiptir. list sınıfı template parametresiyle belirtilen türden nesneleri tutar. Örneğin: list<int> x; Burada x bağlı listesinin elemanları int türden nesneleri tutar. Ya da örneğin: list<Person> y; Burada y bağlı listesinin her elemanı Person türünden bir yapıyı tutmaktadır. STL bağlı listeleri elemanların kendisini tutan bir yapıdadır, yani bir yapıyı bağlı listeye eklediğimizde onun bir kopyası listede tutulmuş olur. Tabii eleman yerleştiren fonksiyonlar yerleştirilecek bilgiyi adres yoluyla alırlar, bu durum yalnıza fonksiyona parametre aktarımını hızlandırmak için düşünülmüştür. Fonksiyon o adresteki bilginin kendisini bağlı listeye yazmaktadır. list Sınıfının Başlangıç Fonksiyonları Tüm STL sınıflarında olduğu gibi list sınıfı da allocator sınıfı türünden default bir template parametresi almaktadır. template <class T, class A = allocator<T> > class list { //... }; Sınıfın başlangıç fonksiyonları şunlardır: 1- list(); Default başlangıç fonksiyonu ile başlangıçta boş bir bağlı liste yaratılır. 2- list(const list &r); Kopya başlangıç fonksiyonudur. 3- explicit list(size_t n, const T &value = T()); Bu başlangıç fonksiyonu T template parametresi, yani bağlı listede saklanacak nesnelerin türü olmak üzere n tane bağlı liste elemanı oluşturur. n eleman da T normal türlere ilişkinse 0, sınıf türündense default başlangıç fonksiyonuyla doldurulur. Anahtar Notlar 1: Bir fonksiyonun referans parametresi default değer alabilir. Örneğin, void Func(const X &r = X()) { //... 41 } Eğer fonksiyon parametresiz çağırılırsa bir geçici nesne oluşturulur, o geçici nesnenin adresi referansa atanır. Geçici nesne fonksiyon sonunda boşaltılır. Anahtar Notlar 2: C++’da C’dekinin yanı sıra ikinci bir tür dönüştürme operatörü daha vardır: tür (ifade). Örneğin, x = double (100); Aslında başlangıç fonksiyonu yoluyla geçici nesne yaratma bu çeşit bir tür dönüştürmesi işlemidir. Bu tür dönüştürme işleminin iki özel durumu vardır: a- Tür bir sınıf ismiyse parantezin içerisinde virgüllerle ayrılmış birden fazla ifade bulunabilir. b- Tür doğal türlere ilişkinse ve parantezin içi boş bırakılmışsa 0 konulmuş kabul edilir. Bu durum template fonksiyonlar için düşünülmüştür. Bu başlangıç fonksiyonu şöyle kullanılabilir: list<int> x(10); list<Person> y(10); list<int> z(10, 500); list<Person> k(30, Person(“Noname”)); list sınıfının bitiş fonksiyonu alınan bütün elemanları geri bırakır. list Sınıfının Önemli Üye Fonksiyonları Sınıfın size() üye fonksiyonu bağlı listedeki eleman sayısına geri döner. size_type size() const; Örnek: list<int> x(10); assert(x.size() == 10); Sınıfın empty() üye fonksiyonu bağlı listenin boş olup olmadığı bilgisini verir. bool empty() const; Sınıfın atama operatör fonksiyonu sol taraftaki operanda ilişkin bağlı listeyi önce boşaltır, sonra sağ taraftaki operanda ilişkin bağlı liste elemanlarının aynısı olacak biçimde yeni liste oluşturur. Örneğin, list<int> x(10, 20); list<int> y(5, 100); 42 y = x; Burada int türünden iki ayrı bağlı liste vardır. Önce soldaki liste boşaltılır, sonra sağdaki listenin elemanlarından yeni bir bağlı liste yapılır. Her iki bağlı listenin elemanlarında da aynı değerler vardır ama elemanlar gerçekte farklıdır. Sınıfın karşılaştırma operatör fonksiyonları vardır ve bu fonksiyonlar karşılıklı elemanları operatör fonksiyonlarıyla karşılaştırır. Örneğin, if (x > y) { ... } Sınıfın clear() üye fonksiyonu tüm bağlı liste elemanlarını siler. x.clear(); assert(x.empty()); resize() üye fonksiyonu bağlı listeyi daraltmak ya da genişletmek amacıyla kullanılır. void resize(size_type n, T x = T()); Eğer birinci parametrede girilen sayı bağlı listedeki eleman sayısından azsa bağı liste daraltılır, fazlaysa yeni elemanlar eklenir. Eklenen elemanlar ikinci parametresiyle belirtilen değerleri alır. list Sınıfının Eleman Ekleyen ve Silen Fonksiyonları Bir bağlı liste için başa ya da sona eleman ekleme, araya eleman insert etme, herhangi bir elemanı silme en çok kullanılan işlemlerdir. Bu işlemleri yapan fonksiyon isimleri diğer nesne tutan sınıflar için de ortak isimlerdir. Genel olarak bütün nesne tutan sınıflar için (bazı istisnaları vardır) front() ve back() isimli fonksiyonlar ilk ve son elemanı almak için (bunları silmezler), push_front() ve push_back() isimli fonksiyonlar başa ve sona eleman eklemek için, pop_front() ve pop_back() isimli fonksiyonlar baştaki ve sondaki elemanları silmek için (silinen elemanları vermezler), insert() isimli fonksiyonlar araya eleman eklemek için ve remove() isimli fonksiyonlar eleman silmek için kullanılırlar. void push_front(const T &x); void push_back(const T &x); void pop_front(); void pop_back(); T &back(); T &front(); Anahtar Notlar: Bir referans geçici bir nesne ile ilk değer verilerek yaratılıyorsa (geçici nesneyi derleyici de oluşturabilir) referansın const olması gerekir. Referans C++’ın doğal türlerindense 43 verilen ilk değer referans ile aynı türden bir nesne değilse ya da sabitse derleyici geçici nesne oluşturacağından yine referansın const olması gerekir. Görüldüğü gibi push_front() ve push_back() fonksiyonları bilgiyi adres yoluyla almaktadır. Insert ve delete işlemleri iteratör işlemi gerektirdiği için daha sonra ele alınacaktır. front() ve back() fonksiyonları bağlı liste düğümünde tutulan elemana ilişkin referansa geri döner. Bu nedenle ilk ve son elemanlar bu fonksiyon yoluyla değiştirilebilir. Iterator Kavramı Iterator STL kütüphanesinin ana kavramlarından biridir. Iterator veri yapılarını dolaşmakta kullanılan gösterici gibi kullanılabilen bir türdür. Iterator ya gerçek bir adres türüdür ya da *, -> operatör fonksiyonları yazılmış bir sınıfıtır. Kullanıcı bakış açısıyla iteratör bir gösterici gibi işlem gören bir türdür. STL’de herbir nesne tutan sınıfın içerisinde iteratör diye bir tür ismi vardır. Bu tür ismi ya doğrudan template parametresi türünden gösterici typedef ismidir ya da o sınıf içerisinde tanımlanmış bir sınıfın ismidir. Eğer iteratör ismi bir gösterici ise X nesne tutan sınıf olmak üzere şöyle bir bildirim uygulanmıştır: template <class T, ...> class X { public: typedef T *iterator; //... }; Şimdi aşağıdaki gibi bir bildirimde aslında int * türden bir gösterici tanımlanmıştır. X<int>::iterator iter; Iterator nesne tutan sınıf içerisinde bir sınıf ismi olabilir. Örneğin: template <class T, ...> class X { public: class iterator { //... }; }; Şimdi biz aşağıdaki tanımlamayla aslında bir sınıf nesnesi tanımlamış oluyoruz. 44 X<int>::iterator iter; Iterator ister normal bir gösterici olsun isterse bir sınıf ismi olsun bu durum programcıyı ilgilendirmez. Iteratorler * ve -> operatörleriyle kullanılabilir. Eğer iterator bir göstericiyse * operatörünün zaten doğrudan bir anlamı vardır. Eğer iterator bir sınıf ismi ise o sınıf için * operatör fonksiyonu yazılmış olduğundan ifade yine anlamlıdır. Iterator isminin gerçek türü ne olursa olsun iterator türünden bir nesne * operatörüyle kullanıldığında template parametresi türünden bir nesne belirtir. Iterator türünden nesneye * operatörü uygulandığında elde edilen ifade bir sol taraf değeridir, yani atamanın solunda da sağında da kullanılabilir. Tabii eğer iterator ismi bir sınıf ise bunun sağlanabilmesi için sınıfın * operatör fonksiyonunun geri dönüş değeri T türden referans olmalıdır. Her nesne tutan sınıfın begin() ve end() isimli iki üye fonksiyonu vardır ve bu fonksiyon geri dönüş değeri olarak o sınıf türünden bir iterator verir. iterator begin(); iterator end(); Bir nesne tutan sınıf türünden sınıf nesnesiyle begin() ya da end() üye fonksiyonunu çağırırsak bu fonksiyonların geri dönüş değerleri kendi sınıfları içerisindeki iterator türünden olduğu için kendi sınıfları türünden bir iterator nesnesine atanmalıdır. list<int> x; list<int>::iterator iter; iter = x.begin(); Buraya kadar anlatılanların hepsi STL içerisindeki tüm nesne tutan sınıflar için geçerlidir. Örneğin vector de bir nesne tutan sınıftır, onun da içinde bir iterator tür ismi vardır, o sınıfın da begin() ve end() isimli üye fonksiyonları vardır. vector<int> x; vector<int>::iterator iter; iter = x.begin(); Iteratorler Neden Kullanılır? Iterator veri yapısını dolaşmak için gereken gösterici anlamında bir türdür. Nesne tutan sınıfın begin() üye fonksiyonuyla bir iterator alındığında ve bu iterator * operatörüyle kullanıldığında veri yapısının içerisindeki ilk elemana erişilir. Iterator kendi yeteneğine göre ++, -- ve [] operatörleriyle kullanılabilir. ++ sonraki elemana geçme, -- önceki elemana geçme ve [] herhangi bir elemana erişme anlamına gelir. Her sınıfın iteratör türü bu operatörlerin hepsini desteklemek zorunda değildir. Örneğin, list sınıfının iteratör türü ++ ve -- operatörlerini destekler. Iterator isminin gerçek türü ne olursa olsun genel olarak en azından == ve != operatörlerini destekler. >, <, >= ve <= operatörleri her sınıfın iterator türü tarafından desteklenmemektedir. 45 Sınıfın begin() üye fonksiyonuyla elde edilen iterator her zaman ilk elemana ilişkin, end() üye fonksiyonuyla elde edilen iterator her zaman temsili olarak sondan bir sonraki, yani nesne tutan sınıfta olmayan elemana ilişkindir. Bu durumda bir nesne tutan sınıfı baştan sona dolaşmak için kullanılan klasik kalıp aşağıdaki gibidir: list<int> x; ... list<int>::iterator iter; for (iter = x.begin(); iter != x.end(); ++iter) cout << *iter << endl; Görüldüğü gibi iter önce ilk elemana ilişkin iteratör değerindedir, döngü içerisinde sürekli arttırılır, en sonunda x.end() ile alınan sondan bir sonraki iteratör değerine eşit olur ve döngüden çıkılır. Iterator’lerin Sınıflandırılması Iteratorun türü o itratorun hangi operatörlerle kullanılacağını ve bu iteratörün * ile kullanıldığında sol taraf değeri olarak kullanılıp kullanılmayacağını belirler. Iteratorler bu bakımdan beş bölüme ayrılabilir. Iteratorlerin bu biçimde sınıflara ayrılması algoritma diye isimlendirilen hangi fonksiyonlarla kullanılabileceğini belirlemekte faydalı olur. Her nesne tutan sınıfın iteratorleri bu guruplardan birine girer, böylece biz bu iteratorlerle hangi işlemlerin yapılabileceğini anlarız. Iteratorler aşağıdaki guruplara ayrılmaktadır: 1- Input Iteratorleri: Input iteratorleri yalnızca okuma yapabilen iteratorlerdir, yani bu guruptan bir iterator * ile kullanıldığında atama operatörünün sol tarafına getirilemez. Input iteratorleri *, ->, =, ++, == ve != operatörleriyle kullanılabilir. Örneğin iter bir input iterator gurubundan olsun, *iter = n gibi bir işlemi yapamayız, ancak n = *iter gibi bir işlemi yapabiliriz. ++iter gibi bir işlemi yapabiliriz ama --iter gibi bir işlemi yapamayız. iter1 ve iter2 iki input iteratorü olsun, bu iki iteratörü birbirine atayabiliriz, == ve != operatörleriyle karşılaştırabiliriz. 2- Output Iteratorleri: Bu gurup iteratorler veri yapısına yalnızca yazma yapabilen iteratorlerdir, yani iter bir output iterator olsun, *iter = n işlemi geçerlidir, ancak n = *iter işlemi geçerli değildir. Output iteratorleri yalnızca *, = ve ++ operatörlerini destekler. 3- Forward Iteratorler: Bu gurup iteratorler veri yapısına hem okuma hem de yazma yapabilirler, yani input iteratorleri ile output iteratorlerinin birleşimidirler. iter bir forward iterator olmak üzere hem *iter = n işlemi hem de n = *iter işlemi geçerlidir. Forward iteratorler *, ->, =, ++, == ve != operatörlerini desteklerler. 4- Bidirectional Iteratorler: Bu iterator gurubu forward iteratorler gurubunun -- operatörü eklenmiş durumudur. Yani bidirectional iteratorler veri yapısından hem okuma hem yazma yapmakta kullanılabilirler, çift yönlü ilerleyebilirler. Bidirectional iteratorler *, ->, =, ++, --, == ve != operatörlerini desteklerler. 5- Random Access Iteratorler: En yetenekli iterator gurubudur. Bidirectional iteratorlerin yeteneklerine sahiptir, ek olarak [] operatörünü ve diğer karşılaştırma operatörlerini de 46 desteklerler. Random access iteratorler *, ->, =, +, -, ++, --, [], >, <, >=, <=, -=, +=, == ve != operatörlerini desteklerler. Iterator’lerin const Olma Durumuna ve Doğrultularına Göre Sınıflandırılmaları Iteratorler const olma durumlarına ve doğrultularına göre dört guruba ayrılırlar. Her guruptaki iteratorler nesne tutan sınıfların üye fonksiyonlarıyla elde edilebilmektedir. 1- Normal Iteratorler: Normal iteratorler ++ operatörüyle ileri, -- operatörüyle geri giden operatörlerdir. Normal iteratorler const değillerdir, yani iter bir normal iterator olmak üzere *iter = n ve n = *iter biçimlerinde kullanılabilirler. Normal iteratorler nesne tutan sınıfların iterator tür ismiyle temsil edilir ve begin(), end() üye fonksiyonlarıyla elde edilir. 2- const Iteratorler: Normal iteratorler const olmayan bir göstericiyi temsil ediyorsa const iteratorler const bir göstericiyi temsil ederler. iter bir const iterator olmak üzere *iter = n ifadesi geçersizdir ama n = *iter ifadesi geçerlidir. Nesne tutan sınıfların const_iterator biçiminde bir tür ismi vardır, const iterator nesne tutan sınıfların begin() ve end() üye fonksiyonlarıyla elde edilebilir. Örneğin: list<int>::const_iterator iter; list<int> x; ... iter = x.begin(); *iter = 10; //error cout << *iter << ‘\n’; //ok 3- Reverse Iteratorler: Bu tür iteratorler const değildirler, ancak doğrultuları terstir, yani ++ operatörüyle geriye -- operatörüyle ileri doğru giderler. Reverse iteratorler algoritma denilen global fonksiyonların ters yönlü çalışmalarını mümkün hale getirmek için düşünülmüştür. Nesne tutan sınıfların reverse_iterator biçiminde bir tür ismi vardır. Reverse iterator almak için nesne tutan sınıfların rbegin() ve rend() isimli üye fonksiyonları kullanılır. rbegin() üye fonksiyonu son elemana ilişkin iterator değerini, rend() fonksiyonu baştan bir önceki elemana ilişkin iterator değerini verir. Örneğin aşağıdaki işlemle bir bağlı liste sondan başa yazdırılabilir: list<int> x; list<int>::reverse_iterator riter; for (int i = 0; i < 100; ++i) x.push_back(i); for (riter = x.rbegin(); riter != x.rend(); riter++) cout << *iter << ‘\n’; Aslında bu işlem normal bir iteratörle sondan başa gidilerek de yapılabilirdi. Örneğin: list<int> x; list<int>::iterator iter; for (int i = 0; i < 100; ++i) x.push_back(i); 47 iter = x.end(); --iter; for (; iter != x.begin(); --iter) cout << *iter << ‘\n’; cout << *iter << ‘\n’; Reverese iterator aslında global STL fonksiyonlarının düz doğrultuda yazıldığı halde onların ters de çalışmasını sağlamak amacıyla kullanılmaktadır. Örneğin, iki iteratör arasını ekrana yazdıran Disp() isimli bir template fonksiyon olsun, bu fonksiyon yalnızca ileri doğrultuda yazılmış olsun. templeta <class T> void Disp(T iter1, T iter2) { T iter; for (iter = iter1; iter != iter2; ++iter) cout << *iter << ‘\n’; } Şimdi bu fonksiyonun aşağıdaki gibi çağırıldığını düşünelim: list<int> x; for (int i = 0; i < 100; ++i) x.push_back(i); Disp(x.begin(), x.end()); Derleyici template fonksiyonu T türünü list<int>::iterator olarak alıp açar. Fonksiyon da bağlı listenin tüm elemanlarını düz sırada yazar. Disp() fonksiyonu düz doğrultuda hareket ettiği halde reverse iterator kavramıyla biz ters doğrultuda işlem yaptırabiliriz. Disp(x.rbegin(), x.rend()); Şimdi derleyici T türünü list<int>::reverse_iterator olarak alıp fonksiyonu yazar. Bu durumda Disp() fonksiyonu geriye doğru yazma işlemini yapacaktır. 4- const Reverse Iteratorler: Bu iteratorler hem const hem reverse özellik gösterirler. Nesne tutan sınıfların const_reverse_iterator biçiminde bir tür ismi vardır. const reverse iteratorlere nesne tutan sınıfların rbegin() ve rend() üye fonksiyonlarıyla iterator alınabilir. STL Algoritmaları STL içerisindeki global template fonksiyonlara algoritma denilmektedir. STL template fonksiyonları iteratorlerle çalışacak biçimde yazılmışlardır, yani bu fonksiyonlar özellikle *, ++, --, !=, == gibi operatörlerle yazılmışlardır. Fonksiyonların parametreleri özellikle prototiplerinde iterator türü belirtilerek isimlendirilmişlerdir, bu da programcının bu fonksiyonları hangi iteratorlerle kullanabileceğini, başka bir deyişle bu fonksiyonların hangi 48 operatörler kullanılarak yazıldığını anlamasına olanak sağlar. STL algoritmaları “algorithm” başlık dosyası içerisinde bildirilmiştir. Bu bölümde bazı STL algoritmaları ele alınacaktır. STL algoritmaları çeşitli guruplara ayrılarak incelenebilir. Guruplandırma konusunda bir fikir birliği yoktur. Bazı yazarlar aşağıdaki guruplandırma biçimini tercih etmektedir: 123456- Değer değiştiren algoritmalar (modifying algorithms) Değer değiştirmeyen algoritmalar (nonmodifying algorithms) Silme yapan algoritmalar (removing algorithms) Sıralama yapan algoritmalar (sorting algorithms) Sıralanmış diziler üzerinde işlem yapan algoritmalar (sorted range algorithms) Sayısal işlem yapan algoritmalar (numeric algorithms) STL algoritmalarının hepsi başlangıç ve bitiş iterator parametrelerini aldıklarında başlangıç iteratorünü dahil, bitiş iteratorünü dahil değil olarak tanımlarlar. Yani [begin, end) aralığında çalışırlar. accumulate() Fonksiyonu Bu fonksiyon bir veri yapısındaki belirli aralıktaki değerlerin toplamını bulmak amacıyla kullanılmaktadır. template <class InputIterator, class T> T accumulate(InputIterator beg, InputIterator end, T initValue); Fonksiyon iki template argümanı almıştır, fonksiyonun üç parametresi vardır, ilk iki parametre başlangıç ve bitiş iterator durumlarıdır, üçüncü parametre toplamın başlangıç değeridir. Bütün durumlarda belirtilen aralık soldan kapalı sağdan açık aralıktır. Bu fonksiyon iki iterator arasını toplayacak bir işleve sahiptir. Fonksiyon geri dönüş değeri olarak toplam değeri vermektedir. Fonksiyon parametreleri olan iteratorlerin gurubu input iteratordür. Fonksiyon muhtemelen aşağıdaki gibi yazılmış olmalıdır: template <class InputIterator, class T> T accumulate(InputIterator beg, InputIterator end, T initValue) { T total = initValue; InputIterator iter; for (iter = beg; iter != end; ++iter) total += *iter; } return total; STL algoritmalarının hepsi normal dizilerle de çalışabilir, çünkü dizinin başlangıç ve bitiş adresleri bir iterator olarak kullanılabilirler. Burada dikkat edilmesi gereken nokta şudur: dizinin tüm elemanları üzerinde işlemler yapılmak istendiğinde başlangıç iteratoru olarak dizinin ilk elemanının adresi, bitiş iteratorü olarak sondan bir sonraki elemanın adresinin verilmesi gerekir. 49 Çünkü bütün STL algoritmaları daha önce de belirtildiği gibi soldan kapalı sağdan açık aralıklar üzerinde işlem yapmaktadır. accumulate() fonksiyonunun örnek kullanımları şöyle olabilir: list<int> x; for (int i = 0; i < 100; ++i) x.push_back(i); int total; total = accumulate(x.begin(), x.end(), 0); int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; total = accumulate(a, &a[10], 0); Bazı STL algoritmaları <numeric> dosyasının içerisindedir, accumulate() fonksiyonu bu dosya içerisindedir. Örnek: #include <iostream> #include <numeric> #include <list> using namespace std; void main() { list<int> x; int total; for (int i = 1; i <= 100; ++i) x.push_back(i); total = accumulate(x.begin(), x.end(), 0); cout << total << '\n'; } for_each() Fonksiyonu Bu fonksiyon bir dizideki ya da nesne tutan sınıftaki tüm elemanları belirli bir fonksiyona sokmak için kullanılır. Bu fonksiyon pek çok derleyicide aşağıdaki gibi yazılmıştır: template <class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f) { while (first != last) f(*first++); return f; } 50 Görüldüğü gibi for_each() fonksiyonu üç tane parametre alır, ilk iki parametre iterator ya da göstericidir, üçüncü parametre bir fonksiyon adresidir. Derleyici template fonksiyonu üçüncü parametre bir fonksiyon göstericisiymiş gibi açar. Buradaki fonksiyonun parametresi dizi ya da nesne tutan sınıf türünden normal bir parametre değişkeni ya da referans olmalıdır. Fonksiyon üçüncü parametresiyle verilen fonksiyon adresine geri döner. Üçüncü parametresiyle belirtilen fonksiyonun geri dönüş değeri herhangi bir biçimde olabilir. Örnek: #include <iostream> #include <algorithm> #include <list> using namespace std; void Disp(int x) { cout << x << endl; } void main() { int a[] = {1, 2, 3, 56, 65, 56, 12}; } for_each(a, a+7, Disp); for_each() fonksiyonu bir veri yapısının elemanları üzerinde belirlenen bir işlemi uygulamak için tercih edilmelidir. transform() Fonksiyonu Bu fonksiyon bir veri yapısının elemanları üzerinde bir işlem uygulayıp sonucu başka bir veri yapısına yazmak için kullanılır. Tipik yazım biçimi şöyledir: template <class InputIterator, class OutputIterator, class Function> OutputIterator transform(InputIterator first, InputIterator last OutputIterator result, Function f) { while(first != last) *result++ = f(*first++); } return result; Fonksiyon dört parametreyle kullanılır, ilk iki parametre kaynak veri yapısındaki iterator aralığını belirtir, üçüncü parametre hedef veri yapısındaki başlangıç yerini belirtir, son parametre uygulanacak işlemi belirten bir fonksiyon olmalıdır. Son parametreye ilişkin fonksiyonun parametresi veri yapısı türünden normal bir nesne ya da referans olmalıdır, geri dönüş değeri veri yapısı türünden olmalıdır. Örnek: #include <iostream> 51 #include <algorithm> #include <list> using namespace std; int Square(int a) { return a * a; } void Disp(int x) { cout << x << endl; } void main() { int a[] = {1, 2, 3, 56, 65, 56, 9}; list<int> b(7); } transform(a, a+7, b.begin(), Square); for_each(b.begin(), b.end(), Disp); Sınıf Çalışması: int türden bir bağlı liste ve 10 elemanlı bir dizi açınız, bağlı liste içerisindeki sayıları diziye karelerini alarak tersten yerleştiriniz. Açıklama: Bağlı liste a, dizi ise b olsun. Kare alan fonksiyon ismi Square ise işlem transform(a.rbegin(), a.rend(), b, Square); biçiminde yapılabilir. Cevap: #include <iostream> #include <algorithm> #include <list> using namespace std; int Square(int a) { return a * a; } void Disp(int x) { cout << x << endl; } void main() { 52 int a[10]; list<int> b; for (int i = 1; i <= 10; ++i) b.push_back(i); transform(b.rbegin(), b.rend(), a, Square); for_each(a, a+10, Disp); } sort() Fonksiyonu Bu fonksiyonun iki biçimi vardır: 1- template <class RandomIterator> void sort(RandomIterator first, RandomIterator last); 2- template <class RandomIterator, class Pred> void sort(RandomIterator first, RandomIterator last, Pred &r); sort() fonksiynunun birinci biçimi < operatörü kullanılarak yazılmıştır. Sort edilecek veri yapısı bir sınıf ise < operator fonksiyonunun yazılması gerekir. İkinci biçim üçüncü parametre olarak bir karşılaştırma fonksiyonu almıştır. Bu karşılaştırma fonksiyonu dizinin iki elemanı ile çağırılacaktır eğer soldaki eleman sağdaki elemandan küçükse sıfırdışı bir değere dönmelidir. Örnek: #include <iostream> #include <algorithm> using namespace std; void Disp(int x) { cout << x << endl; } void main() { int a[] = { 1, 4, 2, 3, 8, 7}; sort(a, a + 6); for_each(a, a + 6, Disp); } Fonksiyonun algoritmik karmaşıklığı O(nlogn) dir. Muhtemelen quick sort algoritması ile yazılmıştır. Person türünden bir class’ı numarasına göre sıraya dizen örnek: #include <iostream> #include <algorithm> using namespace std; 53 class Person{ private: char m_name[30]; int m_no; public: Person(const char *nm, int n) { strcpy(m_name, nm); m_no = n; } bool operator <(const Person &per) { return m_no < per.m_no; } void Disp() const { cout << m_name << " " << m_no << endl; } }; void Disp(const Person per) { per.Disp(); } void main() { Person per[] = { Person("ali serce", 256), Person("Kaan Aslan", 121), Person("Volkan Ozyilmaz", 244) }; sort(per, per + 3); for_each(per, per + 3, Disp); } Fonksiyon default olarak diziyi küçükten büyüğe sıralar, büyükten küçüğe sıralamak için söz konusu dizi bir sınıf dizisi ise sort() fonksiyonunun birinci versiyonunu sınıfın < operator fonksiyonu ters yazılarak sınıf düzenlenebilir. Bu işlemin en kolay yolu fonksiyonun ikinci versiyonunu kullanmak fakat son parametredeki karşılaştırma fonksiyonunu küçükse sıfır, küçük değilse sıfırdışı bir değere döndürmektir. Örnek: #include <iostream> #include <algorithm> using namespace std; void Disp(int a) { cout << a << endl; } bool Cmp(int a, int b) { return !(a < b); 54 } void main() { int a[] = {1, 5, 3, 7, 2, 7 , 8}; sort(a, a + 7, Cmp); for_each(a, a + 7, Disp); } reverse() Fonksiyonu Bu fonksiyon bir veri yapısını tersyüz etmekte kullanılır. template <class BidirectionalIT> void reverse(BidirectionalIT first, Bidirectional last); Örnek: #include <iostream> #include <algorithm> #include <list> using namespace std; void Disp(int a) { cout << a << endl; } void main() { list<int> x; for (int i = 1; i <= 10; ++i) x.push_back(i); } reverse(x.begin(), x.end()); for_each(x.begin(), x.end(), Disp); string sınıfı iterator işlemlerini desteklemektedir. string sınıfının iteratorleri random access iteratorlerdir. Örneğin string sınıfının tersyüz eden bir üye fonksiyonu yoktur bu işlem şöyle yapılabilir: #include <iostream> #include <string> #include <algorithm> using namespace std; void main() { 55 string a("ankara"); reverse(a.begin(), a.end()); cout << a << endl; } copy() Fonksiyonu template <class InIt, class OutIt> OutIt copy(InIt first, InIt last, OutIt x) { while (first != last) *x++ = *first++; return x; } Bu fonksiyon üç parametreye sahiptir. İlk iki parametre kaynak dizideki iterator aralığını belirtir, üçüncü parametre hedef dizideki kopyalamanın yapılacağı başlangıç iterator pozisyonunu belirtmektedir. Fonksiyon hedef dizideki kopyalama işleminden sonraki elemanın iterator değeriyle geri döner. Kullanımına örnek: int a[5] = { 3, 5, 7, 9, 3 }; int b[5]; copy(a, a + 5, b); ostream_iterator Sınıfı ostream_iterator sınıfı aşağıdaki gibi tanımlanmış bir template sınıftır: template <class T, class charT = char, class traits = char_traits<charT> > class ostream_iterator { //... }; Sınıf <iterator> başlık dosyası içerisindedir, ancak bu başlık dosyası <iostream> başlık dosyası içerisinden include edildiğinden yalnızca <iostream> dosyasının include edilmesi yeterlidir. Sınıf üç template parametresi alır. Birinci parametre zorunludur. Sınıfın iki başlangıç fonksiyonu vardır: 1- ostream_iterator(ostream &r); 2- ostream_iterator(ostream &r, const char *delim); Sınıf bir output iterator işlemini temsil eder, bu yüzden output iteratorlerin sahip olduğu operator fonksiyonlarına sahiptir. Sınıfın operatör fonksiyonları şunları yapmaktadır: 56 1- Sınıfın * operator fonklsiyonu hiçbir şey yapmaz, yalnızca nesnenin kendisine geri döner. Örneğin, ositer bu sınıf türünden bir nesne olsun, *ositer tamamen ositer ifadesine eşdeğerdir (yani bu operatör fonksiyonu içerisinde *this ile geri dönülmüştür). 2- Sınıfın ++ operatör fonksiyonu da tıpkı * operatör fonksiyonu gibi birşey yapmaz nesnenin kendisi ile geri döner. 3- Sınıfın template parametresi türünden parametreli bir atama operatör fonksiyonu vardır. Bu fonksiyon atanan değeri başlangıç fonksiyonunda belirtilen ostream nesnesi yoluyla ekrana yazdırır. Ekrana yazdırma işleminden sonra eğer nesne yaratılırken ikinci başlangıç fonksiyonu kullanılmışsa, ikinci başlangıç fonksiyonunda belirtilen yazı yazdırma işleminden sonra ayıraç olarak ekrana basılmaktadır. Örneğin aşağıdaki kodda ekrana 10-20basılacaktır. ostream_iterator<int> x(cout, “-“); x = 10; x = 20; = operatör fonksiyonunun olası yazılmış biçimi şöyledir: ostream_iterator operator =(const T &r) { m_cout << r << m_delim; return *this; } Sınıfın = operatör fonksiyonu nesnenin kendisine geri dönmektedir. ostream_iterator sınıfı algoritmalar kullanılarak ekrana ve dosyaya yazma amaçlı düşünülmüştür. ostream_iterator sınıfı sayesinde copy() fonksiyonunu kullanarak bir dizi ya da nesne tutan sınıf bir hamlede ekrana ya da dosyaya yazdırılabilmektedir. Örneğin: int a[] = { 3, 6, 4, 6, 2 }; ostream_iterator<int> x(cout, “ “); copy(a, a + 5, x); Şüphesiz yukarıdaki örnekte ekrana yazdırma işlemi * ya da ++ operatörleri yüzünden değil atama operatörü yüzünden yapılmaktadır. Yani copy() içerisinde şu tema kullanılmıştır: *ositer++ = val; Burada * ve ++ hiçbir şey yapmayıp ositer nesnesinin kendisine geri döndüğüne göre aslında bu ifade ositer = val; ile eşdeğerdir. Bu da val değerinin ekrana yazılmasına yol açacaktır. 57 ostream_iterator türünden nesne copy() fonksiyonunun parametresinde o anda geçici olarak da yaratılabilir. Örneğin: int a[] = { 4, 6, 4, 3, 2 }; copy(a, a + 5, ostream_iterator<int>(cout, “ “)); ostream_iterator sınıfının template parametresi aslında ekrana yazdırılacak bilginin türünü belirtmektedir. Sınıfın başlangıç fonksiyonundaki ostream nesnesi ise hangi nesne kullanılarak yazdırma yapılacağını belirtir. Tipik durum cout nesnesinin kullanılmasıdır. Ancak ofstream ya da fstream türünden nesneler kullanılarak dosyaya yazma yaptırılabilir. copy_backward() Fonksiyonu Bu fonksiyon tersyüz ederek kopyalama yapmaktadır. template <class BiIterSource, class BiIterDest> BiIterSource copy_backward(BiIterSource first, BiIterSource last, BiIterDest destLast); Fonksiyon ilk iki parametresinde kaynak dizinin iterator aralığını alır, son parametrede hedef dizinin son iterator değerini alır (yani sondan bir sonraki değeri). Kaynak diziyi arttırarak hedef diziyi azaltarak kopyalama yapmaktadır. Örneğin: int a[5] = { 3, 6, 5, 1, 4 }; int b[5]; copy_backward(a, a + 5, b + 5); copy(b, b + 5, ostream_iterator<int>(cout, “ “)); copy_backward() tamamen copy() fonksiyonu ile kopyalandıktan sonra reverse() ile tersyüz edilmesine eşdeğer bir işlem yapar. random_shuffle() Fonksiyonu Bu fonksiyon bir dizideki elemanları rastgele bir biçimde yerleştirmek için kullanılır. template <class RanIt> void random_shuffle(RanIt first, RanIt last); Bu fonksiyon iki iterator aralığı alarak aradaki elemanları karıştırır. Iterator random access iterator olmak zorundadır. Tipik olarak oyun programlarında rastgele bir oluşum sağlamak amacıyla kullanılır. Bu fonksiyon kendi içerisinde rastgele sayı üreten bir fonksiyon kullanıp karıştırma işlemi yapmaktadır. Rastgele sayı üreten fonksiyon olarak standart rand() 58 fonksiyonunun kullanılacağı standardizasyonda garanti edilmemiştir. Programın her çalışmasında farklı bir karışım elde etmek için böyle bir garantinin olması gerekir. Fonksiyonun ikinci versiyonu karıştırmada kullanılacak rastgele sayı üreten fonksiyonu da parametre olarak almaktadır. template <class RanIt, class RandFunc> void random_shuffle(RanIt first, RanIt last, RandFunc Func); Fonksiyonun son parametresi 0 ile (dizi uzunluğu – 1) aralığında rastgele sayı üreten bir fonksiyon olmalıdır. Sözkonusu fonksiyonun parametresi olmamalıdır. Örneğin programın her çalışmasında 5 elemanlı bir diziyi rastgele sıraya dizen kod şöyle yazılabilir: int MyRand() { return rand() % 5; } srand(time(NULL)); int a[] = {3, 8, 4, 7, 6}; random_shuffle(a, a + 5, MyRand); Sınıf Çalışması: Klavyeden alınan yazının sözcüklerini ters sırada yazan programı aşağıda belirtildiği gibi STL kullanarak yazınız. Açıklamalar: 1- Yazı klavyeden istream::getline() ya da standart gets() fonksiyonuyla alınır ve string türünden bir nesneye atanır. 2- string sınıfının find_first_of() fonksiyonu ile find_first_not_of() fonksiyonu bir döngü içerisinde çağırılarak substr() fonksiyonu da kullanılarak sözcükler elde edilir. 3- Elde edilen her sözcük string türünden bir bağlı listenin sonuna eklenir. 4- for_each() fonksiyonu ile reverse_iterator kullanarak ters sırada yazdırma işlemi yapılır. Cevap: /* reverse_write.cpp */ #pragma warning(disable:4786) #include #include #include #include <iostream> <algorithm> <list> <string> //4786 numaralı warning’i gösterme //256 karakterden uzun template //açılımları yüzünden bu olur using namespace std; 59 void Disp(const string &r) { cout << r << " "; } void main() { list<string> sList; char buf[100]; string s; string::size_type index1 = 0, index2; cout << "Enter text:"; cin.getline(buf, 100); s = buf; for (;;) { index2 = s.find_first_not_of(" \t", index1); if (index2 == string::npos) break; index1 = s.find_first_of(" \t", index2); if (index1 == string::npos) { sList.push_back(s.substr(index2)); break; } sList.push_back(s.substr(index2, index1 - index2)); } for_each(sList.rbegin(), sList.rend(), Disp); } Sınıf Çalışması: Türemiş sınıf nesnelerinin adreslerini taban sınıf türünden göstericilerden oluşan bir bağlı listede saklayıp for_each() fonksiyonunu kullanarak ya da manuel olarak bütün nesne adresleri için disp() isimli bir sanal fonksiyonu çağırınız. Açıklamalar: 1- Türetme şeması aşağıdaki gibi olacaktır: A B C D E 2- A sınıfı Disp() safsanal fonksiyonunu içeren soyut bir sınıf olabilir. 3- Program döngü içerisinde aşağıdaki gibi bir menü çıkartmalıdır: 60 1) 2) 3) 4) 5) 6) B nesnesi C nesnesi D nesnesi E nesnesi Listele Çıkış yarat yarat yarat yarat Nesne yarat seçenekleri seçildiğinde new operatörüyle ilgili sınıf nesnesi yaratılmalı ve A sınıfı türünden göstericilerden oluşan bir list sınıfına eklenmelidir. list<A *> objList; 4- Listele seçeneği seçildiğinde bağlı liste dolaşılarak her adres için Disp() sanal fonksiyonu çağrılır. list<A *> ::iterator iter; for (iter = objList.begin(); iter != objList.end(); ++iter) (*iter).Disp(); Aynı işlem for_each() fonksiyonuyla da yapılabilir. void Func(A *pObj) { pObj->Disp(); } for_each(objList.begin(), objList.end(), Func); Cevap: /* inheritance_list.cpp */ #include <iostream> #include <algorithm> #include <list> using namespace std; class A { public: virtual void Disp() = 0; }; class B:public A { public: virtual void Disp() {cout << "DispB\n";} }; class C:public A { public: virtual void Disp() {cout << "DispC\n";} 61 }; class D:public B { public: virtual void Disp() {cout << "DispD\n";} }; class E:public C { public: virtual void Disp() {cout << "DispE\n";} }; void Func(A *pObj) { pObj->Disp(); } void Close(A *pObj) { delete pObj; } void showMenu() { cout << "1 cout << "2 cout << "3 cout << "4 cout << "5 cout << "6 } - B nesnesi yarat" << C nesnesi yarat" << D nesnesi yarat" << E nesnesi yarat" << Listele" << '\n'; Exit" << '\n'; '\n'; '\n'; '\n'; '\n'; void main() { int num; list<A *> pList; for (;;) { showMenu(); cin >> num; switch (num) { case 1: pList.push_back(new B); break; case 2: pList.push_back(new C); break; case 3: pList.push_back(new D); break; case 4: pList.push_back(new E); break; case 5: for_each(pList.begin(), pList.end(), Func); break; case 6: 62 } for_each(pList.begin(), pList.end(), Close); exit(2); } } vector Sınıfı Dinamik olarak büyütülen bir dizi gibi çalışan nesne tutan bir sınıftır. vector için yine sınıf tarafından tahsis edilmiş bir alan ayrılır, push_back() ya da insert() fonksiyonlarıyla ekleme yapıldığında ayrılmış olan alan yetmezse bu fonksiyonlar tarafından otomatik olarak daha büyük bir alan tahsis edilir, yani otomatik büyütme sağlanır. vector iteratorleri random access iteratorlerdir. vector’de tahsisatlar her zaman büyütme yönünde yapılır. Aradan bir eleman silinse ya da sondan bir eleman silinse söz konusu dizi kaydırılarak küçültülür ama tahsis edilen alan küçültülmez. Genellikle vector sınıflarını yazanlar alan yetmediğinde her zaman önceki alanın iki katı kadar bir arttırım yaparlar. Böylece tahsisat sayısı düşürülmeye çalışılır. vector sınıfında araya eleman eklenirken ya da aradan eleman silinirken kaydırma yapılır. Yani bu işlemler doğrusal karmaşıklığa yol açan işlemlerdir. Tabii sona eleman eklemek sabit, yani O(1) karmaşıklıkta yapılmaktadır. Şüphesiz vector yapısında eleman ekleneceği zaman en hızlı ekleme sona eklemedir. vector yapısı her hangi bir elemana erişimin O(1) olduğu random access iteratorlerin kullanıldığı hızlı bir yapıdır. Bağlı listelere göre daha hızlı erişim sağlar ama ardışıl alan gereksinimi olduğundan bellek verimi daha düşüktür. vector Sınıfı ile list Sınıfının Karşılaştırılması 1- Her hangi bir elemana erişme vector sınıfında rastgele yani O(1), list sınıfında O(n) hızında yapılır. Yani vector sınıfı erişim bakımından daha hızlıdır. 2- Araya eleman ekleme ya da aradan eleman silme, yani insert ve delete işlemleri bağlı listelerde O(1), vector’lerde O(n) hızındadır. Yani bağlı listelerde bu işlemler daha hızlıdır. 3- vector yapısı büyük ardışıl alanlara gereksinim duyar. Bu durum büyük diziler için verimli bir bellek kullanımı oluşturmaz. Halbuki list bölünmüş bellek bölgelerinde çalışabilmektedir. 4- Her iki yapıda da sona eleman ekleme ya da sondan eleman silme sabit zamanlı, yani O(1) hızındadır. Ancak vector yapısında sona eleman ekleme bazı durumlarda (tahsis edilen alanın tükendiği durumlarda) yeniden tahsisat yüzünden gecikmektedir. Bu durumda olduğu gibi bazen ek maliyeti olan sabit zamanlı işlemlere “ek maliyetli sabit zamanlı işlemler (amortized constant time process)” denir. 63 vector Sınıfının Kullanımı Sınıfın Başlagıç fonksiyonları: 1- vector(); Default başlangıç fonksiyonu ile vector sınıfı yaratıldığında henüz hiçbir alan tahsis edilmez. Genellikle ilk eleman eklendiğinde önceden belirlenen bir ilk uzunluk ile tahsisat yapılır ve sonra bu alan iki kat biçiminde arttırılır. 2- vector(size_type n, const T &r = T()); Bu başlangıç fonksiyonunun birinci parametresi dizinin ilk eleman uzunluğudur. İkinci parametre tahsis edilen alanın hangi değerlerle doldurulacağını belirtir. Örneğin: vector<int> v(10); Burada ilk uzunluğu 10 olan int türünden bir vector oluşturulmuştur, bu alanın bütün elemanları 0’larla doldurulur. Ya da örneğin: vector<Person> x(10, Person(“ali”)); Burada başlangıç uzunluğu 10 olan, her biri Person yapısından bir dizi yaratılmıştır ve dizinin her elemanı ikinci parametresi ile belirtilen değerleri almaktadır. 3- vector(const_iterator first, const_iterator last); Bu fonksiyon iki iterator aralığındaki değerlerden bir vector nesnesi oluşturur. Bu başlangıç fonksiyonu template fonksiyondur, yani burada belirtilen iteratorler vector sınıfının iteratorleri olmak zorunda değildir. Anahtar Notlar: Normal bir sınıfın template üye fonksiyonu olabildiği gibi template bir sınıfın template üye fonksiyonu da olabilmektedir. Örneğin: template <class T> class A { public: template<class TYPE> void Func(TYPE t) { //... } }; Örneğin bir list sınıfının iteratorleri kullanılarak vector sınıfı oluşturulabilir. list<int> x; //... vector<int> y(x.begin(), x.end()); Başlangıç fonksiyonlarıyla yer ayrıldığında bütün elemanlar default olarak normal türler için 0, sınıflar için default başlangıç fonksiyonu ile değer alır. 64 vector Sınıfının size() ve capacity() Fonksiyonları vector sınıfı da daha önce belirtildiği gibi tahsisat sayısını azaltmak için geniş bir alan tahsis edip o alanın içini dizi gibi organize eder. size() fonksiyonu vector ile tutulan dizinin gerçek uzunluğunu belirtmektedir, capacity() ise tahsis edilen bloğun uzunluğunu vermektedir. Parametreli başlangıç fonksiyonunda hem parametreyle belirtilen uzunlukta alan tahsis edilir hem de bu alan default değerlerle doldurulur. Yani, vector<int> a(10); assert(a.size() == a.capacity()); Default başlangıç fonksiyonunda herhangi bir yer ayrılmaz, yani capacity() değeri sıfırdır. Ancak ilk eleman eklendiğinde size = 1, capacity ise derleyicinin aldığı default değerde olur. vector Sınıfının Eleman Ekleyen Fonksiyonları push_back() fonksiyonu sona ekleme yapar, sınıfın push_front() fonksiyonu yoktur. Ancak herhangi bir pozisyona eklemek için insert() fonksiyonları kullanılmaktadır. insert() fonksiyonunun üç biçimi vardır: 1- iterator insert(iter, val); 2- void insert(iter, n, val); 3- void insert(iter, iterfirst, iterlast); Birinci insert() fonksiyonu elemanı birinci parametresiyle belirtilen iterator pozisyonuna insert eder ve insert edilen iterator değerine geri döner. İkinci insert() fonksiyonunda belli bir iterator pozisyonuna belli bir değer n defa insert edilmektedir. Üçüncü insert() fonksiyonunda sınıfın herhangi bir iterator pozisyonuna başka bir dizinin bir iterator arasındaki bölge insert edilir (VC++6.0 member template özelliğini desteklemeyebilir, bu durumda burada belirtilen iterator aralığı vector sınıfının iterator aralığı olmak zorundadır). insert() fonksiyonlarıyla sona ekleme de yapılabilmektedir. insert() fonksiyonları tıpkı push_back() fonksiyonunda olduğu gibi kendi içerisinde gizlice kapasite arttırımı yapmaktadır. Örneğin: vecotr<int> v; ... v.insert(v.end(), 10); // v.push_back(10); Örnek Uygulama: #include <iostream> #include <vector> #include <stdlib.h> 65 #include <algorithm> using namespace std; void main(void) { vector<int> v; for (int i = 0 ; i < 10; ++i) v.push_back(rand() % 100); copy(v.begin(), v.end(), ostream_iterator<int>(cout, " ")); cout << "\n"; cout << "capacity :" << v.capacity() << "\n"; cout << "size :" << v.size() << "\n"; v.insert(v.begin(), 100); copy(v.begin(), v.end(), ostream_iterator<int>(cout, " ")); system("pause"); } vector Sınıfının Iteratorleri vector sınıfının iteratorleri random access iteratordür (yani aslında iterator doğrudan bir gösterici biçimindedir). Bu yüzden iter + n, iter – n, iter += n, iter -= n gibi işlemler bu iteratorler için geçerlidir. Ayrıca iterator [] işlemini de gerçekleştirmektedir, yani iter[n] işlemi de geçerlidir. vector sınıfının [] ve add() üye fonksiyonları da vardır. [] operatör fonksiyonu herhangi bir sınır kontrolü yapmazken add() fonksiyonu sınır dışına taşmalarda exception oluşturmaktadır. Iteratorler n değeri ile toplanıp çıkartılabileceğine göre herhangi bir pozisyona insert şöyle yapılabilir: v.insert(v.begin() + n, val); vector Elemanlarının Yerleşimi vector sınıfının elemanlarının bir dizi biçiminde ardışıl bir bölgede saklandığı standartlarda garanti altına alınmıştır. Bu nedenle v bir vector türünden nesne olmak üzere &v[n] ile &v[n + 1] ardışıl adreslerdir. Böylece vector sınıfı tamamen bir dizi gibi de kullanılabilir. &v[n] template parametresi olan T türünden bir adres belirtir. Örneğin: vector<char> v(20); strcpy(&v[0], “ankara”); Burada önce char türden 20 elemanlık bir vector nesnesi oluşturulmuştur (yani size = 20 ve capacity = 20’dir). &v[0] ile vectorün ilk elemanının adresi char * türünden elde edilmiştir. vector elemanlarının ardışıl olması garanti edildiğinden strcpy() fonksiyonu “ankara” yazısını vector elemanlarına sırasıyla yerleştirecektir. Özetle vector türünden bir 66 nesne dinamik büyümesi avantajının yanı sıra tamamen bir dizi gibi de kullanılabilmektedir (bir farkla: vector nesnesi olan v dizi ismi gibi bir adres belirtmez dolayısıyla vectorün ilk elemanının adresi &v[0] ile elde edilmelidir). vector Sınıfı ile Normal Dizilerin Karşılaştırılması vector sınıfı tamamen bir dizi gibi kullanılabilmektedir. 1- vector otomatik olarak insert() ve push_back() işlemleri ile büyütülür. Bu da kolay kullanım sağlar. 2- vector sınıfını normal dizi gibi kullandığımızda normal dizilere göre erişim göreli olarak daha yavaştır. reserve() Fonksiyonu reserve() fonksiyonu string sınıfında olduğu gibi vector sınıfında da kapasiteyi arttırmak amacı ile kullanılır. Tabii vector sınıfında aslında kapasite hiç bir zaman küçültülmemektedir. void reserve(size_type n); Fonksiyonun parametresi önceki kapasitenin değerinden büyük ya da önceki değere eşitse yeniden tahsisat yapılır ve vector alanı capacity = n yapılarak büyütülür. Tabii bu işlem size değerini etkilememektedir. Parametre kapasite değerinden küçük ise fonksiyon hiç bir şey yapmaz. Eğer çalışacağımız dizinin uzunluğunu kestirebiliyorsak aşağıdaki gibi hemen başlangıçta bir reserve() işleminin yapılması hız bakımından faydalı olabilir. vector<int> v; v.reserve(80); reserve() işleminden sonra elde edilen alan bir dizi gibi kullanılıp [] ile erişim sağlanabilir. Tabii henüz size = 0 olduğundan [] ile erişim size değerini güncellemeyecektir. size değeri yalnızca push_back(), insert() ve erase() işlemlerinden etkilenir. Örneğin insert() ve erase() işlemlerinde sınıf size değerini göz önüne alarak kaydırma yapacaktır. end() fonksiyonu ile verilen iterator şüphesiz string sınıfında olduğu gibi capacity değil size ile ilgilidir. vector Sınıfının Diğer Önemli Fonksiyonları vector sınıfının diğer nesne tutan sınıflarda olan klasik fonksiyonları vardır. Örneğin empty() fonksiyonu size değerine bakarak boş mu değil mi kontrolü yapar. front() ve back() fonksiyonları ilk ve son elemanı elde etmekte kullanılır. vector sınıfında push_front() ve pop_front() fonksiyonları tanımlı değildir, ancak push_back(), pop_back() fonksiyonları klasik işlemleri yapar. clear() fonksiyonu tamamen vector 67 sınıfını boşaltır. Yani bu fonksiyon sınıfın tuttuğu alanı tamamen free hale getirmektedir. Bu işlemden sonra size = capacity = 0 olur. resize() fonksiyonu size değerini büyütmekte, yani sona eleman eklemekte kullanılır. resize() ile küçültme yapılmaya çalışılırsa size küçülür ama alan küçültmesi yapılmadığından dolayı capacity aynı kalır. Sınıfın bitiş fonksiyonu nesnenin tuttuğu tüm alanları boşaltmaktadır. Sınıf Çalışması: Bir bağlı liste bir de vector nesnesi tanımlayınız. Bağlı listeye 1 ile 100 arasında rastgele 20 eleman ekleyiniz. Bağlı listedeki elemanları vector nesnesine kopyalayınız. vector nesnesini sort() fonksiyonu ile sort ediniz. sort edilmiş vectorü yeniden bağlı listeye kopyalayınız ve bağlı listedeki elemanları yazdırınız. Açıklamalar: Bağlı listenin sort edilmesi gerektiği zaman bu işlem iki biçimde yapılabilir: 1- Bağlı listenin sort() fonksiyonu ile sort işlemi yapılabilir. Bu işlem çok yavaş bir işlemdir. 2- Bağlı liste vector sınıfına ya da bir diziye taşınır, orada sort() işlemi uygulanıp geri yazılır. Bu yöntem daha hızlıdır. Cevap: /* vector_sort.cpp */ #include #include #include #include #include <iostream> <stdlib.h> <list> <vector> <algorithm> using namespace std; int main() { list<int> list; vector<int> vec; for (int i = 0; i < 20; ++i) list.push_back(rand() % 100 + 1); cout << "random list :" << endl; copy(list.begin(), list.end(), ostream_iterator<int>(cout, " ")); vec.reserve(list.size()); vec.resize(list.size()); copy(list.begin(), list.end(), vec.begin()); sort(vec.begin(), vec.end()); copy(vec.begin(), vec.end(), list.begin()); cout << "\n"; cout << "sorted list :" << endl; copy(list.begin(), list.end(), ostream_iterator<int>(cout, " ")); cout << "\n"; system("PAUSE"); 68 } return 0; vector Sınıfının Eleman Silen Fonksiyonları vector sınıfının iki erase() fonksiyonu vardır. Bu fonksiyonlar elemanı silip kaydırma yapıp size değerini güncellerler. Yani bu işlemler doğrusal bir karmaşıklığa sahiptir. 1- void erase(iter); 2- void erase(iterfirst, iterlast); Bu işlemlerle capacity değeri hiç bir şekilde etkilenmez. Sınıf Çalışması: vector sınıfı kullanarak aşağıdaki kuyruk sistemini yazınız. template <class T> class Queue { public: Queue(size_t size); ~Queue(); void Put(const T &r); void Get(T &r); bool IsEmpty(); void Disp(); private: vector<T> v; //... }; Cevap: /* vector_queue.cpp */ #include <iostream> #include <vector> #include <algorithm> using namespace std; template <class T> class Queue { public: Queue(size_t size); ~Queue(); void Put(const T &r); void Get(T &r); bool IsEmpty(); void Disp(); private: vector<T> v; }; 69 template <class T> Queue<T>::Queue(size_t size) { v.reserve(size); } template <class T> Queue<T>::~Queue() { v.clear(); } template <class T> void Queue<T>::Put(const T &r) { v.push_back(r); } template <class T> void Queue<T>::Get(T &r) { r = v.front(); v.erase(v.begin()); } template <class T> bool Queue<T>::IsEmpty() { return v.empty(); } template <class T> void Queue<T>::Disp() { copy(v.begin(), v.end(), ostream_iterator<int>(cout, " ")); cout << "\n"; } void main() { int val; Queue<int> q(10); for (int i = 0; i < 5; ++i) q.Put(i); q.Disp(); for (int j = 0; j < 5; ++j) { q.Get(val); cout << val << endl; } } 70 Editör Tasarımına İlişkin Notlar Editörler satır editörler ve tam ekranlı editörler olmak üzere ikiye ayrılırlar. Tam ekranlı (full screen) editörlerde kullanıcı ok tuşlarıyla tüm ekran üzerinde gezinebilir. Yatay ve düşey scroll işlemleri söz konusudur. Bu tür editörlerde aslında ekranda editördeki bilginin belirli bir bölümü gösterilir. Editördeki tüm bilgi bellekte ya da diskte ayrıca tutulmalıdır. Programcı ekranda bilginin hangi kısmının görüntülendiğini bilmek zorundadır. Veri yapısı bakımından en önemli problem insert ve delete problemleridir. Çünkü bu işlemlerin görüntüde yapılması problem değildir ama veri yapısı üzerinde yapılması problemlidir. Genellikle iki algoritmik teknik kullanılır: 1- Editördeki bilgiler tamamen birebir bir karakter dizisi içerisinde tutulur. Insert ve delete işlemlerinde blok kaydırmaları yapılır. 64K’ya kadar editörlerde bu yöntemin kullanılması ciddi bir probleme yol açmaz (Windows’un edit kontrolünde muhtemelen bu yöntem kullanılmıştır). 2- Insert ve delete işlemlerinin verimini arttırmak için bağlı liste tekniği kullanılabilir. Tabii her karakteri bağlı liste elemanı yapmak verimli değildir. Her satır bir blok olarak bağlı listede tutulabilir. Böylece bir satır üzerinde insert ve delete işlemleri bütünü etkilemez yalnızca o satırı etkiler. Sarma (wrapping) yapan editörlerde satırlar değişken uzunlukta olabilmektedir. Ancak sarma yapmayan editörler çok daha yaygın kullanılır. Bu editörlerde satırın bir maximum uzunluğu vardır. Satır daha fazla karakter içeremez. Sarma yapan editörlerde blok uzunluğu olarak ne alınacaktır? Burada bloklar yetmedikçe otomatik olarak büyütülebilir. Satırların büyütülmesi otomatik malloc(), realloc() sistemi ile yapılabilir (yani vector sınıfı bu iş için idealdir) ya da her satırın blokları yine ayrı bir bağlı listede tutulabilir. Çokbiçimliliğin Anlamı ve Kullanımı Çokbiçimlilik nesne yönelimli programlama tekniğinde aşağıdaki gibi üç anlama gelmektedir: 1- Taban sınıfın bir üye fonksiyonunu türemiş sınıfların her birinin kendine özgü bir biçimde çalıştırması. Örneğin Shape taban sınıfının move() diye bir sanal fonksiyonu olabilir, bu sınıftan türetilmiş her şeklin hareketi kendine özgü olabilir. 2- Daha önce yazılmış olan kodların daha sonra yazılmış olan kodları çağırabilmesi durumu. Örneğin Shell sınıfının process() fonksiyonu execute() isimli sanal fonksiyonu çağırıyor olsun, şimdi Shell sınıfından LineEditor gibi bir sınıf türetip bu fonksiyonu yazarsak eskiden yazılmış olan kodlar LineEditor sınıfının execute() fonksiyonunu çağıracaktır. 3- Türden bağımsız program yazılması. Programcı taban sınıf türünden bir gösterici ya da referansla çalışır. Bunu genel bir tür olarak kullanır. Programın işlevi o sınıftan sınıf türetilerek değiştirilebilir. Çokbiçimlilik içeren uygulamalarda genellikle bir türetme şeması ve bu şemanın en tepesinde bir taban sınıf bulunur. Şemada yukarıdakiler daha genel durumları, aşağıdakiler daha özel durumları temsil eder. Çokbiçimli uygulamalarda en sık kullanılan yöntemlerden biri taban sınıf 71 göstericilerine ilişkin bir veri yapısı oluşturmak ve çeşitli türemiş sınıf nesnelerinin adreslerini bu veri yapısında saklamaktır. Böylelikle heterojen sınıflar sanki aynı sınıfmış gibi bir veri yapısında saklanabilmektedir. Programcı istediği bir zaman bu veri yapısını dolaşarak taban sınıfıtaki sanal fonksiyonları çağırıp her nesnenin kendine özgü bir iş yapmasını sağlayabilmektedir. Bu tür uygulamalarda veri yapısının türü taban sınıf türünden göstericiler içermelidir. Veri yapısının nasıl ve hangi algoritmik yöntemle oluşturulduğu ikinci derecede önemli bir konudur. Diğer nesne yönelimli dillerde de çokbiçimlilik basitleştirilmiş biçimde ama yukarıdaki anlamıyla kullanılmaktadır. Java ve C#’da Çokbiçimlilik Java ve C#’da gösterici olmamasına karşın bütün sınıf ve dizi türleri aslında tamamen bir gösterici gibi işlem görmektedir. Bu dillerde en tepede Object isimli bir sınıf bulunur. Programcı bu sınıftan syntax olarak bir türetme yapmamış olsa bile sınıfın default olarak Object sınıfından türetildiği varsayılır. Bu dillerde gösterici yoktur ama aslında göstericiler gizli olarak kullanılmaktadır. Bu dilerde Object sınıfı tüm sınıfların en tepedeki taban sınıfı görevini yapmaktadır. Örneğin aşağıdaki Java ve C++ kodları tamamen eşdeğerdir: Java ya da C# Sample s; s = new Sample(); Object o; o = s; o.ToString(); C++ Sample *s; s = new Sample(); Object *o; o = s; o->ToString(); Görüldüğü gibi Java’da yerel ya da global sınıf nesnesi tanımlamak mümkün değildir. Bütün sınıf nesneleri ve diziler new operatörü ile heap üzerinde yaratılmalıdır. Yukarıdaki örnekte ToString() Object sınıfının sanal bir fonksiyonudur ve aslında Sample sınıfnın sanal fonksiyonu çağırılmaktadır. Java’da bir fonksiyonu sanal yapmak için virtual gibi bir anahtar sözcük kullanılmaz. Bütün fonksiyonlar default olarak zaten sanaldır. Türemiş sınıfta bir fonksiyon taban sınıfın aynı isimli fonksiyonları ile yazılırsa sanallık mekanizması devreye girer. Bütün sınıflara ve dizi türlerine ilişkin nesne isimleri aslında gizli birer göstericidir, aktarım sırasında adresiyle geçirilmektedir. Örneğin: void Func(Object o) { ... } Sample s = new Sample(); Func(s); Bu dillerde sınıf ve dizi dışındaki tüm türler normal bir biçimde değerle aktarılmaktadır. Java ve C#’daki nesne tutan sınıfların hepsi Object türünden nesneleri tutar, yani C++’a göre aslında Object sınıfı türünden göstericilerden oluşmaktadır. Bu dillerde nesne tutan sınıf içerisine eleman eklemek çok kolay bir biçimde yapılabilmektedir. Ancak tüm nesne tutan sınıflar 72 Object sınıfına ilişkin olduğu için sınıfı dolaşıp sanal fonksiyonları çağırabilmek için önce bir aşağıya dönüşüm uygulamak gerekebilir. Örneğin Triangle Shape sınıfından türetilmiş bir sınıf olsun, Shape sınıfı da default olarak Object sınıfından türetilmiş olsun. Triangle t = new Triangle(); list.Add(t); Shape s = (Shape) list.Get(); Java ve C# gibi dillerde çöp toplayıcı (garbage collector) mekanizması dile entegre edilmiştir. Bu dillerde new ile tahsis edilen nesneler hiçbir kod tarafından kullanılmıyor durumuna gelince çöp toplayıcı tarafından otomatik olarak silinirler. Java’da int, long gibi doğal türler bir sınıf değildir, bu türleri nesne tutan sınıflarda saklayabilmek için çeşitli sarma sınıflar kullanılır. Örneğin: list.Add(new Integer(i)); Ancak C#’da doğal türler de Object sınıfından türemiş birer sınıf nesnesi gibi kabul edilir. Örnek Bir Çizim Programı Örnek uygulamada daire, dikdörtgen, üçgen gibi heterojen geometrik şekiller bir çizim programı tarafından çizilmektedir. Kullanıcı bu şekiller üzerinde click yaparak bu şekilleri taşımak ya da silmek için seçebilecektir. Power point gibi bütün çizim programlarının genel sistematiği bu şekildedir. Bu örnekte bu tür programların çokbiçimlilik özelliği kullanılarak nasıl nesne yönelimli bir teknikle tasarlanacağı ele alınmaktadır. Şüphesiz bütün çizim şekillerinin bilgileri bir veri yapısı içerisinde tutulmalıdır. Bu veri yapısı bağlı liste olmalıdır ama türü ne olmalıdır? Heterojen şekillerin C’de saklanabilmesi için neredeyse tek yol bir tür bilgisini de şekil ile birlikte saklamak olabilir. Yani örneğin aşağıdaki gibi Shape türünden bir bağlı liste oluşturulabilir. typedef struct _SHAPE { int type; void *pShape; } SHAPE; Burada programcı type elemanından faydalanarak gerçek şekli tespit eder ve yapının void * elemanının türünü değiştirerek işlemleri yapar. Tasarım C++’da tamamen çokbiçimli düzeyde yapılabilir. Taban sınıf olarak Shape isimli bir sınıf alınabilir, bu sınıftan şekil sınıfları türetilebilir. 73 Shape Triangle Shape Circle Bağlı liste Shape * türünden olmalıdır. std::list<Shape *> g_shapes; Uygulamanın kendisini App isimli bir sınıf ile temsil edersek bu bağlı listenin global olmak yerine bu sınıfın bir veri elemanı olması daha uygun olur. Bir şekil çizildiği zaman şekle ilişkin bir sınıf nesnesi dinamik olarak yaratılır ve bağlı listeye eklenir. Böylece bağlı liste heterojen nesnelerin adreslerini tutan bir biçime getirilmiş olur. Fareyle click yapıldığında hangi şekle click yapıldığının anlaşılması için bağlı listenin dolaşılarak Shape sınıfının IsInside() sanal fonksiyonu çağırılır. Her şekil sınıfının IsInside() isimli fonksiyonu bir noktanın kendisinin içerisinde olup olmadığını tespit etmek amacıyla yazılmalıdır. virtual bool IsInside(int x, int y) const = 0; Shape sınıfının hangi sanal fonksiyonları olmalıdır? Uygulamanın genişliğine bağlı olarak çok çeşitli sanal fonksiyonlar olabilir. Örneğin, Windows programlamada WM_PAINT mesajında bütün şekillerin yeniden çizilebilmesi için bir Draw() fonksiyonu eklenebilir. Yani Draw() fonksiyonu DC’yi parametre olarak alıp kendini hangi sınıfın Draw() fonksiyonu ise ona göre çizilmelidir. Örneğin bir şeklin silinmesi çok basittir. Tek yapılacak şey şeklin bağlı listeden çıkartılması ve görüntünün tazelenmesidir. Şeklin diske kaydedilmesi için bir dosya formatı tasarlanabilir. Dosya formatı için aşağıdaki gibi bir değişken uzunlukta kayıt içeren yöntem uydurulabilir: Tür Tür ... Başlık Uzunluk Uzunluk ... İçerik İçerik ... Başlık kısmı dosya hakkında genel bilgilerin bulunduğu bir kısımdır. Örneğin dosya içerisinde kaç şekil vardır? Dosya gerçekten de istediğimiz türden bir dosya mıdır? Bunun için bir magic number tespit edilebilir. Formatın versiyon numarası nedir? Sonra her şekil değişken uzunlukta kayıtlar biçiminde dosyaya sıralı bir biçimde yazılır. Örneğin tür WORD bir bilgi olabilir ve şeklin ne şekli olduğunu belirtir. Sonraki alan kayıdın uzunluğudur, bu alan iki nedenden dolayı gerekmektedir. 1- Kayıtlar üzerinde sıralı erişimi sağlamak için 74 2- Line noktalardan oluşur, yani her line şekli diğerinden farklı uzunlukta yer kaplayabilir. Uzunluk alanı uzunlukları değişebilen şekillerin algılanması için gerekmektedir. Nihayet içerik kısmında şeklin ham verileri bulunur. Çokbiçimliliğe İlişkin Diğer Bir Örnek: Tetris Programı Tetris oyun programı tipik olarak çokbiçimlilik özelliği yoğun bir biçimde kullanılarak tasarlanabilir. Oyunun tasarımı için önce oyundaki elemanları sınıflarla temsil etmek gerekir (transformation). Örneğin düşen şekiller, puanlama mekanizması, ekran işlemleri, uygulamanın kendisi birer sınıfla temsil edilebilir. Şekillerin düşmesi ve hareket etmesi tipik olarak çokbiçimli bir mekanizma ile sağlanabilir. Şöyleki, şekiller örneğin Shape gibi bir sınıftan türetilmiş sınıflarla temsil edilir. Shape sınıfının sola döndürme, sağa döndürme, sola hareket etme, sağa hareket etme ve aşağıya doğru hareket etme fonksiyonları olur. Bütün bu fonksiyonlar sanal ya da saf sanal alınabilir. Şekillerin hareketleri tamamen türden bağımsız olarak Shape sınıfı ile temsil edilir. Algoritmalar belirli bir şeklin hareket etmesine göre değil genel bir şeklin hareket etmesine göre düzenlenir. Her şekil kendi hareketini sanallık mekanizması içinde yapacaktır. Örneğin, Shape *pShape = createNewShape(); for (;;) { sleep(50); pShape->MoveDown(); //... } görüldüğü gibi createNewShape() fonksiyonu ile new operatörü kullanılarak bir tetris şekli yaratılır. Algoritma tamamen pShape göstericisine dayalı olarak tasarlanacaktır. Yani şekil bir yandan MoveDown() sanal fonksiyonu ile düşürülür, bir yandan da bekleme yapmadan klavyeden tuş alınır ve alınan tuşa bakılarak şekil genel bir şekilmiş gibi hareket ettirilir. Böylece oyunu oynayan algoritmalarda bir genellik sağlanmış olur. Oyuna yeni bir şeklin eklenmesi kolaylaşır. Çünkü bu durumda oyunu oynayan kısmın kodlarında bir değişiklik yapılmaz. Çünkü oyunu oynayan kısım hangi şekil olursa olsun çalışacak şekilde yazılmıştır. Şekil sınıfları çizim işlemini yapacak biçimde tasarlanmalıdır. Şeklin hareket etmesi ve döndürülmesi sırasında sınıf kendi çizimini kendisi yapar. Nesne yönelimli programlama tekniğinde mümkün olduğu kadar dış dünyadaki fiziksel nesneler ve kavramlar sınıflarla temsil edilmelidir. Örneğin tetris oyununda ekran ve klavye işlemleri bir sınıfla temsil edilebilir. Bu durumda şekil sınıfları ekran ile klavye işlemlerini yapan sınıfı kullanmak zorunda kalacaktır. Burada eleman olarak kullanmak yerine (composition) gösterici yoluyla kullanma (aggregation) tercih edilmesi gereken bir durumdur. Yani özetle ekran ve klavye işlemlerini yapan sınıf nesnesi bir kere dışarıda yaratılmalı bu nesnenin adresi başlangıç fonksiyonu yoluyla şekil sınıflarına geçirilerek şekil sınıfları içerisindeki bir gösterici veri elemanında saklanmalıdır. Madem ki tüm şekil sınıfları ekran ve klavye işlemini yapan sınıfı kullanacaklar, o halde gösterici veri elemanının taban sınıf olan Shape sınıf elemanında tutulması daha anlamlıdır. 75 STL Stack Sistemi stack sınıfı adaptör bir sınıftır, yani başka bir STL sınıfından faydalanılarak yazılmıştır (yani stack sınıfı yazılırken başka bir sınıf veri elemanı biçiminde kullanılarak (composition) yazılmıştır). Ancak stack sınıfında kendisinden faydalanılan sınıf bir template parametresi yapılmıştır, yani değiştirilebilir. template <class T, class Container = deque<T> > class stack { //... Container c; }; Görüldüğü gibi sınıfın iki template parametresi vardır, birinci parametre veri yapısında tutulacak bilginin türünü belirtir, ikinci parametre stack yapısının oluşturulması için hangi veri yapısının kullanılacağını belirtir. Default olarak deque sınıfı kullanılmıştır. stack sınıfı yardımcı bir sınıf kullanılarak çok kolay yazılabilir. stack sınıfının fonksiyonları şunlardır: 123456- bool empty() const; size_type size() const; void push(const T &x); void pop(); T &top(); const T &top() const; Ayrıca sınıfın diğer nesne tutan sınıflarda olduğu gibi karşılaştırma operatör fonksiyonları da vardır. stack sınıfı iterator kullanımını desteklememektedir (çünkü iteratore ilişkin yararlı bir işlem yapmak bu sınıfta mümkün değildir). Stack veri yapısı LIFO tarzı çalışan bir kuyruk sistemidir, stack sistemine push() fonksiyonuyla eleman yerleştirilir ve pop() fonksiyonuyla son yerleştirilen eleman atılır. top() üye fonksiyonu stack göstericisinin gösterdiği yerdeki elemanı alır, yani top() fonksiyonuyla elemanı aldıktan sonra pop() fonksiyonuyla silmek gerekir. Bilindiği gibi stack sisteminde stack’in yukarıdan ve aşağıdan taşması (stack overflow ve stack underflow) gibi hata kaynakları vardır. Stack sistemi başka nesne tutan sınıflar kullanılarak yazıldığından ve bu sınıflar da dinamik olarak büyütüldüğünden stack’in yukarıdan taşması durumuyla pek karşılaşılmaz. Stack’in yukarıdan taşması tahsisat hatasıyla anlaşılır. Stack’in aşağıdan taşması rastlanabilecek bir durumdur ve bu durumda ne olacağı, yani çıkacak olumsuzluklar standart olarak tespit edilmemiştir. top() ve pop() fonksiyonlarını kullanırken size > 0 olmasına dikkat edilmelidir. stack sınıfı <stack> dosyasında bulunur. 76 Çokbiçimliliğe ve Stack Sistemine Diğer Bir Örnek: Undo Sistemi Bir undo sistemi için şu işlemler yapılmalıdır: 1- Undo işlemine konu olacak tüm durumlar belirlenmelidir. 2- Her durumun ek birtakım bilgileri olmalıdır. Örneğin işlem blok silme ise hangi aralıktaki bloğun silinmesi ve silinen bilgiler gibi. 3- Tüm undo işlemleri stack veri yapısı içerisinde ifade edilmelidir. Böyle bir uygulamanın C’de yazılması algısal bakımdan zorluk içerir. Çünkü yapılan undo işelemleri farklı işlemlerdir ve farklı heterojen yapılar içerir. Veri yapısı yine stack sistemi olurdu ancak stack sistemini oluşturan yapının bir elemanı farklı türleri gösterebilen bir gösterici biçiminde alınırdı. Programcı stack’ten bilgiyi çektiğinde önce onun türüne bakar daha sonra bu göstericiyi uygun türe dönüştürerek işlemlerini yapar. Tabii böyle bir sistemde aşırı heap işlemlerinin olacağı açıktır. Örneğin: typedef struct _OPERATIONS { int type; void *pOperation; }OPERATIONS; typedef struct _BLOCKDELETE { int first, last; char *pContents; }BLOCKDELETE; C++’da undo işlemleri türetme ve çokbiçimlilik özellikleri kullanılarak çok daha etkin bir biçimde yürütülebilir. Yapılan her undo işlemi bir taban sınıftan türetilen sınıflarla ifade edilir. Taban sınıf soyut olabilir. Operation RemoveChar Remove block InsertChar InsertBlock .... virtual void TakeBack() = 0; Tasarımın ana noktaları şöyledir: 1- Her sınıf undo işlemlerine ilişkin gerekli bilgileri kendi içerisinde tutar. Örneğin RemoveChar sınıfı silinen karakteri ve onun editördeki yerini tutar. InsertBlock insert edilen bloğun yerini tutabilir. 2- Her sınıf geri alma işlemini kendine özgü biçimde, yani çokbiçimli olarak yapar. Undo işlemi stack’in tepesinden bilgiyi çekip TakeBack() sanal fonksiyonunun çağırılmasıyla yapılır. 77 3- Undo veri yapısı Operation * türünden bir stack sisteminde tutulur ve TakeBack() sanal fonksiyonu aşağıdaki gibi çağırılır: stack<Operation *> undoStack; //...... Operation *pOperation = undoStack.top(); pOperation->TakeBack(); undoStack->pop(); Uygulamada undo işleminin kendisi için bir sınıf tasarlanabilir. Örneğin: class UndoProc { public: UndoProc(){} ~UndoProc(); void Record(Operation *pOperation); void Undo(); private: std::stack<Operation *> m_process; //... }; Şimdi bir Undo işlemine konu olacak olay gerçekleştiğinde bu işlem Record() fonksiyonu ile kayıt edilir. Undo yapılmak istendiğinde Undo() fonksiyonu çağırılır. void UndoProc::Record(Operation *pOperation) { if (!m_process.empty()) { Operation *pOperation = m_process.top(); pOperation->TakeBack(); m_process.pop(); delete pOperation; } } Bu tür uygulamalarda kesinlikle taban sınıfın bitiş fonksiyonu sanal alınmalıdır. UndoProc sınıfının bitiş fonksiyonunun da stack sisteminde tutulan göstericilere ilişkin bölgeyi de boşaltması gerekir. UndoProc::~UndoProc() { while (!m_process.empty()) { delete m_process.top(); m_process.pop(); } } 78 Fonksiyon Nesneleri (Smart Sınıflar) Bir sınıfın fonksiyon gibi davranabilmesi için o sınıfın fonksiyon çağırma operatörünün yazılmış olması gerekir. Fonksiyon gibi davranabilen sınıflara smart sınıflar, bu türden sınıf nesnelerine ise fonksiyon nesneleri denilmektedir. Örneğin: template <class T> void Func(T f) { //... f(...); //... } Şimdi fonksiyon şöyle çağırılmış olsun. class X { //... }; Func(X()); Burada derleyici template fonksiyonu açarken T türünü X sınıfı olarak alır. Dolayısıyla fonksiyonun derlenebilmesi için sınıfın fonksiyon çağırma operatör fonksiyonunun yazılmış olması gerekir. Fonksiyon Çağırma Operatör Fonksiyonları Bir sınıfın farklı parametrik yapıya sahip birden fazla fonksiyon çağırma operatör fonksiyonu olabilir. Bu operatör fonkisyonlarının geridönüş değerleri herhangi bir biçimde olabilir. Fonksiyon çağırma operatör fonksiyonu şu biçimde çağırılabilir: class X { //... void operator()(int a); void operator()(int a, int b); //... }; X a; a(10,20); a.operator()(10,20); STL’de Smart Sınıfların Kullanımı STL içerisinde pek çok algoritma bir fonksiyon parametresi istemektedir. Bu tür durumlarda bu algoritmalara fonksiyon parametresi geçmek yerine smart sınıf kullanmak çok daha kullanışlı bir yöntemdir. Çünkü sınıfın veri elemanlarında çeşitli bilgiler tutulabilir ve fonksiyon çağırma 79 operatör fonksiyonları bu bilgileri kullanabilir. Örneğin bir dizide belirli bir sayıdan büyük olan elemanları sıfırlamak isteyelim. Ancak bu sayı değişebilsin. Şimdi eğer biz for_each() fonksiyonunu kullanıyorsak ilgili değerler de birden fazlaysa her biri için farklı fonksiyon yazmamız gerekir. Şöyle ki int a[] = { 3, 8, 4, 5, 2, 8, 20, 67, 34, 1 }; void Func1(int &r) { if (r > 10) r = 0; } for_each(a, a + 10, Func1); Burada Func1() fonksiyonu yanlızca 10 değeri için çalışır. Oysa biz bu işlemi smart sınıfa yaptırırsak karşılaştırılacak eleman sınıfın bir veri elemanında tutulabilir ve karşılaştırma istenilen elemana göre yapılabilir. class MakeZero { public: MakeZero(int val) : m_val(val) {} void operator()(int &r) { if (r > m_val) r = 0; } private: int m_val; }; int a[] = { 3, 8, 4, 5, 2, 8, 20, 67, 34, 1 }; for_each(a, a + 10, MakeZero(10)); for_each(a, a + 10, MakeZero(20)); C++’da Faaliyet Alanları ve İsim Arama Faaliyet alanı bir değişkenin kullanılabildiği program aralığıdır. C++’da dört tür faaliyet alanı vardır: 1234- Blok faaliyet alanı Fonksiyon faaliyet alanı Sınıf faaliyet alanı Dosya faaliyet alanı C++’da karmaşık pek çok durum için faaliyet alanı kavramı yetersiz kalmıştır, bu yüzden isim arama (name lookup) kavramı geliştirilmiştir. Derleyici bir isimle karşılaştığında onu sırasıyla 80 nerelerde arayacaktır? İsim arama işleminde arama aşama aşama yapılır, isim bir yerde bulunduğunda arama kesilir. İsim hiçbir yerde bulunamazsa bu durum error oluşturur. C++ derleyicisi önce ismi isim arama özelliğine göre arar, bulursa bulduktan sonra erişim kontrolüne bakar, daha sonra da kullanım kontrolü uygulayarak ifadenin geçerli olup olmadığına bakar.Yani önce isim bulunmakta sonra erişime bakılmaktadır. Bu kurallara ek olarak şöyle ilginç bir kural daha eklenmek zorunda kalınmıştır. Normal olarak bir fonksiyon ismi çağırılma yerine bağlı olarak içiçe namespace’ler içerisinde ve global namespace içerisinde aranır. Ancak buna ek olarak fonksiyon parametrelerinin namespace’leri içerisinde de aranmaktadır. Örneğin: std::string x; Func(x); Burada Func(), std namespace’i içerisinde de aranacaktır. Exception Handling Mekanizması Exception handling mekanizması derleyici için zor bir mekanizmadır. Kullanılması çalışabilen programda yer ve zaman kaybı oluşturur. Bir throw işlemi oluştuğunda derleyici try bloğu girişinden itibaren tüm yerel sınıf nesneleri için ters sırada bitiş fonksiyonu çağırır. Throw işlemine karşılık bir catch bloğu bulunamazsa std::terminate() fonksiyonu çağırılarak program sonlandırılır. terminate() fonksiyonu programı sonlandırmak için abort() fonksiyonunu çağırır, abort() ise “Abnormal program termination!” yazısını basarak programdan çıkar. Bu durumda çağırılacak olan terminate() fonksiyonu set_terminate() fonksiyonuyla set edilebilir. Bu fonksiyonu yazacak olan kişi çeşitli işlemleri yaptıktan sonra orijinal std::terminate() fonksiyonunu çağırabilir. Throw işlemi sırasında heap üzerinde tahsis edilmiş olan sınıf nesneleri ya da global sınıf nesneleri için bitiş fonksiyonu çağırılmamaktadır. Başlangıç ve Bitiş Fonksiyonlarında Throw İşlemleri Başlangıç fonksiyonlarının herhangi bir noktasında throw oluşmuş olsun,hangi nesneler için bitiş fonksiyonları çağırılacaktır? Throw işlemine kadar başlangıç fonksiyonları tam olarak bitirilmiş olan sınıf nesneleri için bitiş fonksiyonları ters sırada çağırılır. Örneğin: class A { //... A(); B b; C c; }; A::A() : b(), c() 81 { X d, e; //... throw; } try { A a; } catch (...) { } Burada throw işemi oluştuğunda sırasıyla e, d, c ve b nesneleri için bitiş fonksiyonu çağırılır. A nesnesinin kendisi için bitiş fonksiyonu çağırılmaz çünkü başlangıç fonksiyonu tam olarak bitmemiştir. Bir sınıf nesnesi new operatörü ile dinamik olarak tahsis edildiğinde sınıf nesnesi için çağırılan başlangıç fonksiyonununda throw oluşmuşsa sınıf nesnesi için bitiş fonksiyonu çağırılmaz ancak tahsis edilen alan da derleyici tarafından otomatik olarak boşaltılır. Örneğin: try { A *pA; pA = new A(); } catch(...) { } Burada A sınıfının başlangıç fonksiyonu içerisinde throw oluşursa catch içerisinde delete pA yapmaya gerek yoktur. Bu işlem zaten derleyici tarafından yapılmaktadır. Ancak tabii bitiş fonksiyonu yine çağırılmaz. Genel olarak bitiş fonksiyonu içerisinde throw işlemi yapılması tavsiye edilmez. Çünkü bilindiği gibi throw işlemi sırasında yerel sınıf nesneleri için bitiş fonksiyonu çağırılırken o bitiş fonksiyonlarının içerisinde yeniden throw uygulanırsa derleyici tarafından std::terminate() çağrılarak program sonlandırılır. Bu nedenle böyle bir potansiyel yüzünden bitiş fonksiyonu içerisinde throw uygulanması uygun değildir. Şüphesiz bitiş fonksiyonu içerisinde throw’un uygulanmaması bitiş fonksiyonunun dışına throw edilmemesi anlamına gelir. Yoksa try-catch işlemi bitiş fonksiyonu içerisinde yapılabilir. Bu durumda terminate() fonksiyonu çağırılmaz. new Operatör Fonksiyonunun Başarısızlığı Global operatör new fonksiyonu başarısız olduğunda eskiden set_new_handler() fonksiyonu ile set edilen fonksiyonu çağırırdı, eğer bu fonksiyonda hiçbir set işlemi yapılmadıysa new operatörü NULL üretiyordu. 1996 ve sonrasında global new operatör fonksiyonu başarısızlık durumunda std::bad_alloc sınıfına throw etmektedir. Yani artık set_new_handler() fonksiyonu ile set edilen fonksiyonların çağırılması zorunlu değildir, derleyicileri yazanlara bırakılmıştır. Başarısızlık durumunda operator new fonksiyonu geri dönmemektedir. Bu nedenle artık operator new fonksiyonunun başarısızlılığının try–catch işlemi ile ele alınması gerekir. 82 Programlarda new işlemi için try–catch işlemleri yapılmasa da olur. Programın az bir heap alanı varsa çökeceği kabul edilir. Exception Specification Exception specification bir fonksiyonun en fazla dışarıya hangi türler için throw uygulayabileceğini belirleyen bir syntax’dır. Örneğin: void Func(int a, int b) throw (X, Y, Z); void Func(int a, int b) throw (X, Y, Z) { //... Sample(); } Exception specification’un fonksiyonun hem prototipinde hem de tanımlamasında aynı biçimde belirtilmesi gerekir. Bu biçimdeki exception belirlemelerinin dışında her hangi bir biçimde fonksiyonun dışına bir throw işlemi yapılırsa derleyici tarafından std::unexpected() fonksiyonu çağırılmaktadır. Yukarıdaki örnekte X, Y, Z birer sınıf isimleri olsun. Şimdi biz Func() fonksiyonu içerisinde int bir tür ile throw edersek ve bunu işlemeyip akışı dışarıya kaçırırsak derleyici tarafından std::unexpected() fonksiyonu çağırılır. Aynı durum Func() fonksiyonun çağırdığı Sample() içerisinde de olsaydı ve akışı Func() fonksiyonunun dışına int türü ile throw etseydi aynı durum oluşurdu. Exception specification okunabilirliği ve kod kontrolünü güçlendirmek için eklenmiştir. Fonksiyonun prototipine bakan kişi fonksiyonun en kötü olasılıkla hangi türler ile throw edeceğini anlar ve catch bloklarını ona göre düzenler. Exception specification kullanan ve kullanmayan aşağıdaki iki fonksiyon tamamen eşdeğer çalışmaktadır. void Func() throw(X, Y) { //... } /* Yukarıdaki ile aşağıdaki eşdeğerdir. */ void Func() { try { } //... } catch(X) { throw; } catch(Y) { throw; } catch(...) { std::unexpected(); } 83 throw() belirlemesi dışarıya hiçbir throw işlemi yapılamayacağını belirtir. Örneğin: void Func() throw() { //... } void Func() { try { //... } catch(...) { std::unexpected(); } } Nihayet exception specification kullanılmaması fonksiyonun her türlü değerle dışarıya throw edebileceği anlamına gelir. Exception specification sınıfın başlangıç ve bitiş fonksiyonlarına da uygulanabilir. std::unexpected() fonksiyonu default olarak std::terminate() fonksiyonunu çağırmaktadır. Çağırılacak fonksiyon set_unexpected() fonksiyonu ile set edilebilir. throw ile catch Geçişi Arasında İşlemler Bilindiği gibi C++’da bir fonksiyonun parametre değişkenlerinin isimleri yazılmayabilir. Örneğin: void Func(int) { //... } tanımlaması geçerlidir. Tabii parametre fonksiyonun içerisinden kullanılamaz ama fonksiyon parametre varmış gibi çağırılmalıdır. Aynı durum catch blokları için de söz konusudur. Örneğin: catch (int) { //... } throw işlemi bir ifade ile yapıldığında bu ifadenin değerinin catch parametresine aktarılması tamamen fonksiyon çağırma işleminde yapıldığı gibi yapılmaktadır. Bunun için derleyici önce throw ifadesinin türüyle aynı tür olan muhtemelen static ömürlü bir geçici nesne alır ve bu geçici nesne yoluyla aktarımı gerçekleştirir. Örneğin: throw [ifade]; 84 catch (<tür> <param>) { //... } temp = ifade; param = temp; throw ifadesinden geçici bölgeye yapılan atama tıpkı fonksiyonun return işleminde olduğu gibi gerçekleşir. Eğer throw ifadesi bir sınıf türündense geçici bölge de sınıf türündendir. Bu durumda geçici bölge için kopya başlangıç fonksiyonu çağırılır. catch parametresi de sınıf türündense catch parametresi için de kopya başlangıç fonksiyonu çağırılır. Geçici bölge catch bloğu sonlanana kadar tutulmaktadır. Örneğin geçici bölge sınıf türündense ve catch parametresi de sınıf türündense akış catch bloğunu bitirdiğinde önce catch parametresi için sonra geçici bölge için bitiş fonksiyonları çağırılacaktır. try – catch aktarımları genellikle programlarda karşımıza üç biçimde çıkar; 1- throw ifadesi ve catch parametresinin C++’ın doğal türlerinden ya da bir sınıf türünden olması durumu. Örneğin: throw 100; catch (int a) { //... } throw X(); catch (X a) { //... } Genellikle nesne yönelimli kütüphanelerde throw ifadesi C++’ın doğal türlerine ilişkin değil bir sınıf türüne ilişkin olur. Bu durum etkin bir yöntem değildir. Çünkü bir sınıf türü ile throw edildiğinde geçici bölge ve catch parametreleri için başlangıç ve bitiş fonksiyonları çağırılacaktır. 2- throw ifadesi C++’ın doğal türüne ilişkin ya da bir sınıf türüne ilişkindir. Ancak catch ifadesi aynı türden bir referanstır. Örneğin: throw x; catch (int &a) { //... } throw X(); 85 catch (X &a) { //... } Bu durumda catch parametresi olan referans geçici bölgenin adresini tutmaktadır. Bu teknik önceki teknikten biraz daha iyidir çünkü catch parametresi için başlangıç ve bitiş fonksiyonları çağırılmamaktadır. STL’de bu yöntem kullanılmıştır. 3- throw ifadesi C++’ın doğal türünden ya da bir sınıf türünden adrestir. catch parametresi ise aynı türden bir göstericidir. Bu durumda geçici bölge de gösterici türünden olacaktır. Yani geçici bölge için de başlangıç ve bitiş fonksiyonları çağırılmayacaktır. Bu durumda throw ile aktarılan adresin stack’teki (yani yerel) bir nesneye ilişkin olmaması gerekir. Global ya da heap üzerindeki bir nesnenin adresi olmalıdır. Örneğin: throw new int; catch (int *p) { //... } throw new X(); catch (X *pX) { //... } Eğer tahsisat heap üzerinde yapılmışsa throw işlemi için tahsis edilen alanın boşaltılması catch bloğunu düzenleyen programcı tarafından yapılmalıdır. Örneğin: throw new X(); catch(X *pX) { //... delete pX; } Pek çok sınıf kütüphanesinde bu yöntem tercih edilmektedir. Örneğin MFC’de kütüphane içerisindeki fonksiyonlar CException denilen bir sınıftan türetilen sınıf nesnelerinin adresleriyle throw etmektedir. Örneğin MFC’de tipik bir try – catch işlemi şöyle yapılmaktadır: try { CFile f(“a.dat”, ...); } catch (CFileException *pFileException) { 86 } //... pFileException->Delete(); Burada MFC için özel bir durum söz konusudur. MFC kütüphanesindeki fonksiyonlar bazen global nesneleri adresleriyle de throw edebilmektedir. Bu nedenle exception nesnesinin silinmesi delete operatörü ile değil CException sınıfının Delete() fonksiyonu ile yapılmaktadır. Delete() fonksiyonu adresin heap üzerinde tahsis edilip edilmediğine bakar. Edilmişse delete operatörü ile nesneyi siler. Global bir tahsisat söz konusuysa nesneyi silmez. Exception İşlemleri İçin Bir Sınıf Sisteminin Kullanılması Profesyönel sınıf kütüphanelerinde throw – catch işlemleri için bir sınıf sistemi kullanılır. Kütüphane içerisindeki bu tür sınıf sistemlerine exception sınıfları denir. Doğal türlerle throw yapmak yerine sınıflarla throw yapmak ve bunu için bir sınıf sistemi kullanmak çok daha etkin bir yöntemdir. Genellikle kütüphaneleri düzenleyenler exception mekanizması için en tepede bir exception sınıfı bulundurup exception işlemlerini konulara ayırıp bu sınıftan türetilmiş sınıflar biçiminde temsil ederler. Exception sınıfları çokbiçimli olarak da düzenlenebilir. Bu durumda en tepedeki exception sınıfının sanal fonksiyonları olur. Bu tür kütüphanelerde programcı tüm konulara ilişkin exception durumlarını yakalamak isterse catch parametresini taban sınıf türünden alır. Tabii çokbiçimlilik de söz konusu ise catch parametresinin taban sınıf türünden bir gösterici olması en normal durumdur. Örneğin MFC’de bir bloktaki akışta her türlü exception işlemini yakalamak için aşağıdaki gibi bir düzenleme yapılabilir: try { //... } catch (CException *pException) { //... } İfadesiz throw İşlemleri İfadesiz throw işlemleri genellikle akış bakımından catch bloklarının içerisinde yapılır. Bu işlemlerde amaç exception durumunu kısmi olarak ele alıp sanki hiç ele alınmamış gibi bir dışardaki catch bloğuna atmaktır. Örneğin: try { Func(); } catch (X *pX) { //... } 87 Func() { try { Sample(); } } catch (X *pX) { //... throw; } Burada Sample() fonksiyonu içerisinde bir throw oluştuğunda bu durum önce kısmen ele alınmıştır, daha sonra dıştaki catch bloğuna bırakılmıştır. İfadesiz throw kullanıldığında yeni bir geçici nesne oluşturulmaz. Dıştaki catch bloğunun parametresine yeniden eski geçici nesnenin içeriği atanır. Şüphesiz catch bloğunun içerisinde normal yani ifadeli bir throw da kullanılabilir. İfadeli throw ile ifadesiz throw arasındaki tek fark geçici bölgenin korunması yani bir önceki throw işleminin kullanılıp kullanılmamasıdır. Java ve C# Dillerirnin C++ Bakımından Değerlendirilmesi Son yıllarda basit nesne yönelimli dillere olan gereksinim artmıştır. Java Sun firması tarafından basit bir nesne yönelimli programlama dili olarak tasarlanmıştır. C# javanın biraz daha iyileştirilmiş biraz daha C++’a yaklaştırılmış ve Microsoft teknolojileriyle entegre edilmiş biçimidir. Java ve C# dillerinin tasarımındaki ikinci büyük kavram çalışabilen kodun taşınabilirliği (binary portibility) yani Java ve C# derleyicilerinin ürettiği kod o anda çalıştığımız makinanın işlemcisinin makina komutları değildir. Aslında hiç bir işlemcinin makina komutları değildir, bir arakoddur. Java derleyicilerinin çıktısı *.class, C# derleyicilerinin çıktısı *.exe biçimindedir. Bu arakoda Java terminolojisinde “byte code”, .NET terminolojisinde “Microsoft Intermediate Language” denir. C#’ın ürettiği *.exe kodu normal bir *.exe kodu değildir. Byte code ve MIL kodları başka bir programdan faydalanılarak çalıştırılmaktadır. Örneğin bir java programının derlenip çalıştılırması için şöyle yapılmaktadır: javac x.java java x.class javac, java derleyicisidir. java isimli program ise byte code’ları yorumlayarak çalıştıran “Java Virtual Machine” dir. C#’da ise *.exe kodunun içerisinde ara kod vardır. Ancak programda küçük bir giriş fonksiyonu bulunur. Bu giriş fonksiyonu “MScore.dll” isimli dll’den çağırma yapar ve akış dll’e geçer. Bu dll de ara kodları yorumlayarak çalıştırır. Yani özetle .NET ortamındaki *.exe programı dışarıdan bakıldığında normal bir program gibi çalıştırılmaktadır. Java’da C++’da olan şu konular basitleştirilme yapmak için çıkarılmıştır: - Önişlemci Göstericiler 88 - Operatör fonksiyonları Template işlemleri Default parametre kavramı Çoklu türetme Yerel sınıf nesneleri tanımlama Diğer küçük özellikler C# biraz daha C++’a yaklaştırılmıştır. Örneğin kısmen operatör fonksiyonları eklenmiştir. Hatta istenirse kısmen gösterici bile kullanılabilmektedir. Java ve C#’da aslında bir sınıf türünden değişken tanımlandığında bu C++’a göre göstericidir. Bütün sınıf nesneleri heap üzerinde new opertörü ile tahsis edilmek zorundadır. Yerel olarak tanımlanamaz. Aşağıdaki Java, C# kodu ile C++ kodu tamamen eş değerdir. Java ve C# X a; a = new X(); a.Func(); C++ X *a; a = new X(); a->Func(); Java ve C#’da bir sınıf nesnesi aslında o sınıf türünden gösterici olduğuna göre sınıf nesnelerinin fonksiyonlara aktarılması da aslında gizlice adres yoluyla yapılmaktadır. Örneğin aşağıdaki kodlar eş değerdir. Java ve C# Sample (X a) { //... } X b = new X(); Sample (b); C++ Sample (X *a) { //... } X *b = new X(); Sample (b); Java ve C#’da doğal türler yine stack’de C++’daki gibi yaratılırlar. Bir fonksiyonun parametresi örneğin int türdense aktarım adres yoluyla yapılmaz. Java ve C#'da doğal türler için dinamik tahsisat yapmak mümkün değildir. Bunlar tıpkı C++'daki gibi stack'de tutulurlar. Ancak diziler ve sınıf nesneleri tamamen dinamik olarak yaratılmak zorundadır. Tipik bir dizi tanımlaması şöyledir: int [] a; a = new int[size]; Bu işlemin C++'daki eşdeğeri, int *a; a = new int[size]; şeklindedir. Diziler de fonksiyonlara sınıflarda olduğu gibi aslında adres yoluyla geçirilmektedir. Örnek: void Func(int [] a) { 89 } //... int [] x = new int[20]; Func(x); Dizilere ayrıca Java ve C#'da uzunluk geçirilmez, dizilerin sanki size isimli bir veri elemanı vardır ve bu eleman dizinin uzunluğunu tutmaktadır. Örneğin: void Func(int [] a) { for (int i = 0; i < a.size; ++i) a[i] = 0; } int [] x = new int[20]; Func(x); Java ve C#'da bir sınıf hiçbir sınıftan syntax bakımından türetilmemiş olsa bile sanki Object isimli bir sınıftan türetilmiş gibi işlem görür. Yani, bütün sınıfların en taban sınıfı Object sınıfıdır. Java ve C#'da bütün nesne tutan sınıflar yalnızca Object sınıfı türünden nesne tutarlar. Bu durum C++'a göre bu sınıfların Object türünden göstericileri tuttuğu anlamına gelir. Örneğin: List l = new list(); l.Add(); Java'da nesne tutan sınıflar nasıl doğal türlere ilişkin bilgileri tutarlar? Bunun için Java'da her doğal tür için Object sınıfından türetilmiş sarma sınıflar kullanılmaktadır. Bu sarma sınıfların tek görevi ilgili doğal türü saklamaktır. Örneğin biz 10 gibi int bir değeri doğrudan nesne tutan sınıf içerisinde saklayamayız, bunun için bu 10 değerini Integer isimli sınıfla ifade etmek gerekir. Örneğin: l.Add(new Integer(10)); Java'da her fonksiyon otomatik olarak zaten sanal gibi işlem görmektedir, yani her üye fonksiyonun başında sanki C++'la kıyaslarsak virtual anahtar sözcüğü yazılmış gibidir. Taban sınıftaki bir fonksiyon türemiş sınıfta aynı parametrik yapıyla yeniden yazılırsa sanallık mekanizması devreye girmiş olur. Java ve C#'da C++'da olmayan olumlu özellikler de vardır. final anahtar sözcüğü sınıf bildiriminin ve herhangi bir üye fonksiyonun başına getirilebilir. Sınıf bildiriminin başına getirilirse o sınıftan bir sınıf türetilemez, üye fonksiyonun başına getirilirse türemiş sınıfta bu fonksiyon yeniden yazılamaz. Görüldüğü gibi bu dillerde somut sınıf kavramının da syntax bakımından bir karşılığı vardır. Java ve C#'da abstract anahtar sözcüğü üye fonksiyonların ve sınıf bildirimlerinin başına getirilebilir. Üye fonksiyonun başına getirilirse fonksiyon C++'daki anlamıyla saf sanal olur, yani fonksiyonu tanımlamak error oluşturur. Bir sınıfın en az bir abstract üye fonksiyonu varsa bu 90 durumda sınıf bildiriminin başına da abstract yazmak zorunludur. Sınıfın hiçbir abstract fonksiyonu olmadığı halde sınıf bildiriminin başına yine abstract anahtar sözcüğü getirilebilir ve sınıf yine soyut yapılabilir, bu durumun C++'da karşılığı yoktur. Yani sınıfın hiçbir saf sanal fonksiyonu olmadığı halde yine sınıf sanal olarak belirlenebilmektedir. Java ve C#'da global fonksiyon ve global değişken kavramları yoktur. Bütün fonksiyonlar ve değişkenler bir sınıf ile ilişkilendirilmek zorundadır. Bu durumda global fonksiyonlar yerine statik üye fonksiyonlar, global değişkenler yerine sınıfın static veri elemanları kullanılır. Java ve C#'da :: operatörü yoktur, bunun yerine nokta operatörü kullanılır. Nokta operatörünün solundaki operand sınıf ismiyse nokta operatörü :: operatörü olarak yorumlanmaktadır. Java ve C#'da sınıfın static üye fonksiyonları ve static veri elemanları sınıf_ismi.eleman_ismi biçiminde kullanılır. Java ve C#'da tüm üye fonksiyonlar sınıfın içerisine yazılıp bitirilmek zorundadır. Prototip ya da bildirim gibi bir kavram yoktur. Kullanılan sınıf daha yukarıda yazılmak zorunda değildir, aynı dosya içerisindeki sınıfların herhangi bir sırada yazılmasının hiçbir olumsuzluğu yoktur, yani bir sınıf daha aşağıda tanımlanmış bir sınıfı kullanabilir. Hatta C#'da yalnızca *.exe dosyanın projeye dahil edilmesiyle başka hiçbir ön bildirim yapmadan bir sınıfın kullanılması mümkündür. Üstelik o *.exe'nin aynı dil kullanılarak oluşturulmuş olmasına gerek yoktur. Bu dillerde her üye fonksiyon ve veri elemanının başında bölüm belirten anahtar sözcük yazılmalıdır. class A { public void Func1() { //... } public void Func2() { //... } }; Java ve C#'da çoklu türetme yoktur, bunun yerine interface denilen bir kavram yerleştirilmiştir. interface aslında saf sanal fonksiyonlardan oluşan bir sınıf gibidir. Java ve C#'ın en önemli ve kolaylaştırıcı özelliklerinden birisi çöp toplayıcı (garbage collector) denilen bir mekanizmanın olmasıdır. Çöp toplayıcı otomatik olarak derleyici tarafından heap üzerinde tahsis edilmiş ama hiçkimse tarafından kullanılmayan bölgeleri free hale getiren bir mekanizmadır. Çöp toplama işlemi sayesinde tahsis edilen alanların programcının izlemesi durumu ortadan kaldırılır ve böylece büyük bir kolaylık sağlanır. Çöp toplama işlemi genellikle derleyicinin düşük öncelikli bir thread'i tarafından arka planda yapılmaktadır. Bu dillerde bu nedenle bitiş fonksiyonlarının önemli bir işlevi kalmamaktadır, çünkü nesnenin ne zaman silineceği belli olmamaktadır. Çöp toplayıcı bir üye fonksiyon biçiminde de programcı tarafından çağırılabilmektedir. 91 Atomlarına Ayırma Çalışması Atom bir programlama dilinde kendi başına bir anlamı olan en küçük birimdir. Atomlar genellikle 6 bölüme ayrılırlar: 1- Anahtar sözcükler: Dil için özel anlamı olan, derleyicinin özel bir işlem uyguladığı sözcüklerdir. Değişken olarak kullanılması yasaklanmıştır. 2- Operatörler: Bir işleme yol açan, o işlem sonrasında bir değer üretilmesine yol açan atomlardır. 3- Değişkenler: İsmini programcının istediği gibi verebildiği atomlardır. Örneğin fonksiyon isimleri, nesne isimleri ve typedef isimleri gibi. 4- Sabitler: Doğrudan yazılmış sayılar biçiminde olan atomlardır. Örneğin x = 10 ifadesinde x bir değişken, = bir operatör, 10 ise bir sabittir. 5- Stringler: İkitırnak içerisindeki yazılar ikitırnaklar da dahil olmak üzere string atomlarıdır. 6- Ayıraçlar: Bunlara noktalama işaretleri de denir. Bütün bu grubun dışında kalan, ifadeleri ayırmak için kullanılan atomlardır. Çeviriciler, Derleyiciler ve Yorumlayıcılar Herhangi bir dilde yazılmış programı başka bir dile dönüştüren programlara çevirici programlar (translators) denir. Kaynak Dil Amaç Dil Çevirici Program Kaynak dil yüksek seviyeli bir dil (C, Pascal, Basic gibi), amaç dil sembolik makina dili ya da saf makina dili ise böyle çevirici programlara derleyici (compiler) denir. Programı satır satır ele alarak çalışma zamanı sırasında ne yapılmak istendiğini anlayıp bu işlemleri yapan programlara yorumlayıcı (interpreter) denir. Ürettiği sembolik makina dili ya da saf makina dili o anda çalışılan makinanın işlemcisine ilişkin değil de başka bir makinanın işlemcisine ilişkin ise böyle derleyicilere çapraz derleyici (cross compiler) denir. Programlar üzerinde çevirme, derleme, yorumlama, düzenleme, değerlendirme (profiler) işlemlerinin yapılabilmesi için ilk aşamada programın atomlarına ayrılması gerekir. Çalışma sorusunun konusu bir C programının atomlarına ayrılmasıdır. 92 STL Algoritmalarında Kullanılan Kavramlar STL algoritmalarında kullanılan çeşitli kavramlar şunlardır: 1- Fonksiyon Nesneleri (Function Objects): Bir sınıfın fonksiyon gibi davranması durumudur. Sınıfın fonksiyon çağırma operatör fonksiyonu yazılır, böylece algoritmaya sınıf nesnesi parametre olarak geçilir. Derleyici template parametresinin o sınıf türünden olduğunu düşünür. İçeride bu sınıf nesnesinin fonksiyon gibi çağırılması hata oluşturmaz. STL içerisinde fonksiyon nesnesi olarak kullanılabilecek standart bazı template sınıflar vardır. Şüphesiz bu template sınıfların fonksiyon çağırma operatör fonksiyonları da yazılmıştır. Bu sınıfların fonksiyon çağırma operatör fonksiyonları bir ya da iki parametre alırlar, <functional> başlık dosyası içerisinde tanımlanmışlardır. STL içerisindeki fonksiyon gibi davranan sınıflar şunlardır, aşağıdaki tabloda 1. sütun fonksiyon sınıfının başlangıç fonksiyonunu (yani geçici nesne yaratma ifadesini), 2. sütun fonksiyon çağırma operatör fonksiyonunun parametre sayısını, 3. sütun ise fonksiyon çağırma operatör fonksiyonunun geri dönüş değerinin ne olduğunu göstermektedir. Şüphesiz bu sınıfların template parametreleri fonksiyon çağırma operatör fonksiyonunun parametrelerinin türlerini belirtmektedir. Sınıf negate<T>() plus<T>() minus<T>() multiplies<T>() divides<T>() equal_to<T>() not_equal_to<T>() less<T>() greater<T>() less_equal<T>() greater_equal<T>() logical_not<T>() logical_and<T>() logical_or<T>() () operatörün parametre sayısı 1 2 2 2 2 2 2 2 2 2 2 1 2 2 () operatörün işlemi -param param1 + param2 param1 - param2 param1 * param2 param1 / param2 param1 == param2 param1 != param2 param1 < param2 param1 > param2 param1 <= param2 param1 >= param2 !param param1 && param2 param1 || param2 2- Geridönüş Değeri bool Türünden Olan Fonksiyonlar (Predicate): STL'de bazı algoritmalar bir fonksiyon parametresi alırlar ve bu fonksiyonun geri dönüş değerini doğru ya da yanlış olarak yorumlarlar. Örneğin sort() fonksiyonunun ikinci biçimi bir fonksiyon parametresi alır, bu fonksiyon iki parametrelidir ve geri dönüş değeri bool türündendir. İngilizce geri dönüş değeri bool türünden olan bu tür fonksiyonlara "predicate" denilmektedir. Predicate tek parametreli (unary) ve çift parametreli (binary) olabilir. sort() fonksiyonundaki predicate çift parametrelidir (binary predicate). STL içerisindeki fonksiyon nesneleri oluşturan template sınıfların bir bölümü predicate göreviyle kullanılabilir. 3- Fonksiyon Adaptörleri (Function Adapters): Fonksiyon adaptörleri geri dönüş değeri bir sınıf olan fonksiyonlardır. Bu fonksiyonlar çağırıldıklarında algoritmanın template parametresi 93 fonksiyonun geri dönüş değeri olan sınıf türünden olur. Tipik olarak bind1st(), bind2nd() fonksiyonları birer fonksiyon adaptörüdür. STL Algoritmalarının Sınıflandırılması STL algoritmaları değişik kişiler tarafından değişik biçimlerde sınıflandırılmıştır. 1- Veri Yapısı Üzerinde Değişiklik Yapmayan Algoritmalar (Nonmodifying Algorithms): Bu algoritmlar veri yapısı içerisindeki elemanları kullanan ama onları değiştirmeyen algoritmalardır. 2- Veri Yapısı Üzerinde Değişiklik Yapan Algoritmalar (Modifying Algorithms): Bu algoritmlar veri yapısı içerisindeki elemanların değerlerini değiştiren algoritmalardır. 3- Veri Yapısından Eleman Silen Algoritmalar (Removing Algorithms): Bu algoritmalar veri yapısından eleman silerler. 4- Veri Yapısı Üzerindeki Elemanların Yerlerini Değiştiren Algoritmalara (Mutating Algorithms): Örneğin tipik olarak reverse algoritmasında olduğu gibi. 5- Veri Yapısını Sort Eden Algoritmalar (Sorting Algorithms): Veri yapısını sort eden algoritmalardır. 6- Sort Edilmiş Veri Yapıları Üzerinde İşlem Yapan Algoritmalar (Sorted Range Algorithms): Bu algoritmaları kullanabilmek için önce veri yapısının sort edilmiş olması gerekir. 7- Veri Yapısı Üzerinde Sayısal İşlem Yapan Algoritmalar (Numeric Algorithms): Örneğin veri yapısı içerisindeki elemanların toplamını, çarpımını bulan algoritmalar bu guruptandır. Veri Yapılarında Değişiklik Yapmayan Algoritmalar for_each() find() İki iterator arasındaki elemanları bir fonksiyona sokar. İki iterator arasında arama yapar. Arama başarılıysa elemanın bulunduğu iterator değerine, başarısızsa aralığın son iterator değerine geri döner. find_if() İki iterator arasında arama yapar, ancak karşılaştırmayı == operatörüyle değil programcının belirlediği bir fonksiyonla yapar. Geri dönüş değeri find() fonksiyonundaki gibidir. find_first_of() Bu fonksiyonun da iki versiyonu vardır. Fonksiyondan amaç bir veri yapısı içerisinde başka bir veri yapısının herhangi bir elemanının ilk görüldüğü yerin bulunmasıdır. Fonksiyonun birinci biçimi karşılaştırmak için == operatörünü, ikinci biçimi programcının verdiği fonksiyonu kullanmaktadır. Geri dönüş değeri find() fonksiyonunda olduğu gibidir. adjacent_find() Bu fonksiyon veri yapısının yan yana aynı olan ilk iki elemanını tespit etmekte kullanılır. Örneğin: 1388977 94 count() count_if() equal() search() find_end() search_n() min_element() max_element() Geri dönüş değeri find() fonksiyonunda olduğu gibidir. Bu fonksiyonun da fonksiyonlu ve fonksiyonsuz iki versiyonu vardır. Bu fonksiyon bir veri yapısı içerisinde belirli bir elemanın kaç tane olduğunu bulmakta kullanılır. count() fonksiyonu gibidir ancak karşılaştırmayı fonksiyon ile yapar. Bu fonksiyon iki veri yapısının belirtilen iterator aralıklarını bire bir eşitlik koşulu içerisinde karşılaştırır. Tüm elemanlar karşılıklı olarak birbirlerine eşit ise true, değilse false değeri ile geri döner. Fonksiyon memcmp() fonksiyonu gibi bir anlamla kullanılmaktadır. Bu fonksiyonun da fonksiyonlu ve fonksiyonsuz iki versiyonu vardır. Bu fonksiyon bir dizi içerisinde başka bir diziyi aramakta kullanılır. Bulunursa bulunduğu yerin iterator değeriyle, bulunamazsa son iterator değeriyle geri döner. strstr() fonksiyonunun yaptığı gibi bir işlem yapmaktadır. Bu fonksiyonun da fonksiyonlu ve fonksiyonsuz olmak üzere iki biçimi vardır. Tıpkı find() fonksiyonu gibidir, ancak ilk bulunan iterator değeriyle değil, son bulunan iterator değeriyle geri döner. Bu fonksiyon bir dizi içerisinde bir elemandan ardışıl n tane varsa onun yerini bulmaktadır. Bu fonksiyonlar veri yapısındaki en küçük ve en büyük elemanları bulmak için kullanılır. Her iki fonksiyonun da fonksiyonlu ve fonksiyonsuz versiyonları vardır. Fonksiyonsuz versiyonları < operatörüyle çalışır. Veri Yapısı Üzerinde Değişiklik Yapan Algoritmalar copy() copy_backward() Bu fonksiyonlar iki iterator aralığını başka bir iteratorle belirtilen yerden başlayarak başka bir veri yapısına kopyalamak için kullanılır. copy() fonksiyonu baştan sona doğru, copy_backward() ise sondan başa doğru kopyalama yapar. Çakışan bloklarda kullanılmamalıdır. Örnek: #include <algorithm> #include <iostream> using namespace std; void main() { int a[] = {3, 8, 5, 7, 4}; int b[5]; copy_backward(a, a + 5, b + 5); copy(b, b + 5, ostream_iterator<int>(cout, "\n")); } transform() Bu fonksiyonun iki biçimi vardır. Birinci biçim tek parametreli bir fonksiyon ile, ikinci biçim çift parametreli bir fonksiyon ile kullanılır. Bu fonksiyonlar bir veri yapısındaki elemanlar üzerinde bazı işlemlerin yapılarak başka bir veri yapısına aktarıldığı durumlarda kullanılabilir. Bu fonksiyonlarla aynı veri yapısı üzerinde güncellemeler yapılabilir. 95 #include <algorithm> #include <iostream> #include <functional> using namespace std; void main() { int a[] = {3, 8, 5, 7, 4}; } fill() fill_n() transform(a, a + 5, a, negate<int>()); copy(a, a + 5, ostream_iterator<int>(cout, "\n")); Bu fonksiyonlar bir veri yapısını belirli değerlerle doldurmak amacıyla kullanılır. fill() iki iterator arasını programcının istediği bir değerle doldurur, fill_n() başlangıç iteratorünü ve doldurulacak eleman sayısından hareketle doldurma işlemini yapar. #include <algorithm> #include <iostream> using namespace std; void main() { int a[] = {3, 8, 5, 7, 4}; fill(a + 2, a + 5, 0); copy(a, a + 5, ostream_iterator<int>(cout, "\n")); fill_n(a + 1, 2, 9); copy(a, a + 5, ostream_iterator<int>(cout, "\n")); } generate() generate_n() Bu fonksiyonlar tıpkı transform() fonksiyonu gibi çalışır, ancak onun basit bir biçimidir. Bir fonksiyon parametresi alırlar, fonksiyonun parametresi yoktur, belirli bir iterator aralığı fonksiyon çağırılarak geri dönüş değeri ile doldurulur. generate_n() başlangıç iterator değeri ve uzunluk parametresiyle çalışır. #include <algorithm> #include <iostream> #include <cstdlib> using namespace std; void main() { int a[10]; generate( a, a + 10, rand); copy(a, a + 10, ostream_iterator<int>(cout, "\n")); } replace() replace_if() Bu fonksiyonlar veri yapısı içerisinde bir değeri başka bir değerle yer değiştirirler. Örneğin, bir dizi içerisindeki bütün 3’leri 5 yapmak gibi. 96 bulunup bulunmadığını anlamak için == operatörünü, replace_if() programcının belirlediği bir fonksiyonu kullanır. replace() elemanın #include <algorithm> #include <iostream> using namespace std; template <class T> class Cmp { public: Cmp(int val) : m_val(val) {} bool operator()(T val) { return val > m_val; } private: T m_val; }; void main() { int a[10] = {3, 8, 4, 7, 6, 9, 7, 8, 9, 21}; } replace(a, a + 10, 7, -1); copy(a, a + 10, ostream_iterator<int>(cout, "\n")); replace_if(a, a + 10, Cmp<int>(7), 0); copy(a, a + 10, ostream_iterator<int>(cout, "\n")); replace_copy() Bu fonksiyonlar tamamen replace() ve replace_if() fonksiyonları replace_copy_if() gibidir, ancak sonucu başka bir veri yapısına aktarırlar. #include <algorithm> #include <iostream> using namespace std; template <class T> class Cmp { public: Cmp(int val) : m_val(val) {} bool operator()(T val) { return val > m_val; } private: T m_val; }; void main() { int a[10] = {3, 8, 4, 7, 6, 9, 7, 8, 9, 21}; 97 } replace_copy_if(a, a + 10, ostream_iterator<int>(cout, "\n"), Cmp<int>(7), 0); Veri Yapısından Eleman Silen Algoritmalar Aslında eleman silmek sonuçta veri yapısının küçültülmesi anlamına gelir. Yani silinen eleman yok edildiğine göre veri yapısının küçülmesi gerekir. Ancak STL’deki silme algoritmaları yanlızca kaydırma yapar. Yani gerçek bir silme yapmaz. Silen fonksiyonlar yanlızca silinecek elemana doğru bir kaydırma yaparlar, yani dizinin sonunda son elemanın kopyalarından oluşur. Veri yapısını küçültmek için silme işleminden sonra veri yapısına özgü clear() fonksiyonlarının çağırılması gerekir. Örneğin: dizi = 13479 dizi = 14799 remove() remove_if() 3 silinecek olsun silindikten sonra Bu fonksiyonlar iki iterator arasındaki belirlenen elemanları silerler. Silinecek eleman const T &val parametresi ile alınır. remove() silinecek elemanı == operatörünü kullanarak, remove_if() unary pradicate alarak tespit etmektedir. Fonksiyonların geri dönüş değerleri silinme uygulandıktan sonra daraltılmış olan dizinin sonunu gösteren iteratordür. Böylelikle bir nesne tutan sınıf söz konusu olduğunda fonksiyonun geri dönüş değeri olarak verdiği iteratorden nesne tutan sınıfın sonuna kadar erase() fonksiyonu ile silme yapılabilir. Şüphesiz remove algoritması bağlı listeler için etkin çalışan algoritmalar değildir. Çünkü bağlı listelerde araya eleman eklemek ve aradan eleman silmek çok kolay bir işlemdir. Halbuki remove() bu kolaylıktan faydalanamamaktadır. Bağlı listelerde eleman silmek için en etkin yöntem find() fonksiyonu ile elemanın bulunması ve bağlı listenin erase() fonksiyonu ile silinmesidir. #include <iostream> #include <algorithm> #include <vector> using namespace std; void main() { vector<int> a; a.push_back(1); a.push_back(3); a.push_back(4); a.push_back(3); a.push_back(9); vector<int>::iterator iter; iter = remove(a.begin(), a.end(), 3); 98 a.erase(iter, a.end()); copy(a.begin(), a.end(), ostream_iterator<int>(cout, "\n")); } remove_copy() remove_copy_if() unique() Bu fonksiyonlar iki veri yapısını parametre olarak alır. Silinecek elemanların dışındaki elemanları hedef veri yapısına kopyalar. Bu fonksiyonun da iki versiyonu vardır. Birincisi fonksiyonsuz, ikincisi fonksiyonlu yani predicate alan versiyondur. Fonksiyonlar iki iterator arasındaki peşi sıra olan elemanlardan yanlızca bir tanesini alan diğerlerini silen bir yapıda çalışırlar. Örneğin: dizi = 1339448 unique() fonksiyonundan sonraki durum dizi = 13948xx unique_copy() Bu fonksiyon yanlızca yan yana aynı elemanlar olduğu durumda silme yapar. Bu fonksiyonun da iki versiyonu vardır: fonksiyonlu ve fonksiyonsuz. Bu fonksiyonlar unique() gibi çalışırlar ama kaynak veri yapısı üzerinde silme yapmak yerine sonucu başka bir veri yapısına kopyalarlar. Veri Yapısındaki Elemanların Yerlerini Değiştiren Algoritmalar Bu algoritmalar yanlızca yer değiştirme işlemi yaparlar. sort işlemi yapan fonksiyonlar ayrı bir gurup oluşturduğu için bu guruba dahil edilmemiştir. reverse() reverse_copy() rotate() rotate_copy() random_shuffle() Bu fonksiyonlar tamamen veri yapısını ters yüz ederler. Örneğin string sınıfının bir reverse() fonksiyonu yoktur. Bu işlem global reverse() fonksiyonu ile yapılabilir. Bu fonksiyonlar sola döndürme işlemi yaparlar. Fonksiyonlar iki iterator aralığını sola n defa döndürürler. Bu fonksiyonun iki biçimi vardır. Dizideki elemanları rastgele karıştırır. Karıştırma işleminde rastsal sayı fonksiyonu olarak rand() ya da programcının belirlediği bir fonksiyon kullanılmaktadır. Veri Yapısını Sort Eden Fonksiyonlar sort() stable_sort() Bu fonksiyonun iki biçimi vardır. Klasik olarak (n log n) karmaşıklığa sahiptir (yani qsort algoritması kullanılmıştır). Birinci biçim < operatörünü kullanarak sort işlemini yapar. İkinci biçim < işlemini yapacak olan binary predicate alır. Bu fonksiyonlarla sort işleminde değerleri aynı elemanların sort edilmiş dizideki sırası rastgeledir. Tamamen sort() fonksiyonu gibi kullanılır. sort() 99 fonksiyonundan farkı sort edilmiş dizideki aynı elemanların yerlerinin sort edilmemiş durumdaki ile aynı sırada olmasının garanti edilmesidir. partial_sort() Bu fonksiyon n elemanlı bir veri yapısında n eleman da dikkate alınarak, ilk m kadar elemanın sıralı olmasını sağlamak için tasarlanmıştır. Örneğin 100 koşucu tek tek koşularını tamamlasın süreler bir veri yapısının içerisine yazılsın. İlk 10 dereceyi alanları tespit etmek için bu algoritma kullanılabilir. Süphesiz aynı işlem veri yapısını tamamen sort edip ilk 10 tanesini almak biçiminde de yapılabilir. Ancak bu algoritmanın karmaşıklığı diğerinden daha düşüktür. Bu gereksinmelerin olduğu yerlerde hız bakımından tercih edilebilir. Bu fonksiyonun da iki versiyonu vardır. Fonksiyonlar üç iterator almaktadır. İkinci parametredeki iterator hangi noktaya kadar sıralama yapılacağını belirtir. Bu iteratorün gösterdiği eleman dahil değildir. partial_sort_copy() Sort edilen aralık başka bir veri yapısına kopyalanır. nth_element() Bu algoritmanın fonksiyonlu ve fonksiyonsuz olmak üzere iki versiyonu vardır. Algoritma başlangıç bitiş ve aradaki bir noktayı gösteren üç iterator alır. Bu üç iterator diziyi (first, middle), (middle, last) biçiminde iki bölgeye ayırır. Fonksiyon öyle bir düzenleme yapar ki ilk bölümdeki sayıların hepsi ikinci bölümdeki sayılardan daha büyük olur. Yani aslında bu algoritma bir yarışma sonucunda ilk n kişinin ya da son m kişinin sırasız bir biçimde tespit edilmesi işlemini gerçekleştirir. Sort Edilmiş Veri Yapıları Üzerinde İşlem Yapan Algoritmalar Bu algoritmaları kullanabilmek için dizinin daha önce sort edilmiş olması gerekir. binary_search() Diziyi sürekli ikiye bölerek yapılan arama işlemidir. Karmaşıklığı log 2n biçimindedir. En hızlı arama algoritmasıdır ancak dizinin sıraya dizilmesini gerektirdiği için daha çok yeni eleman eklenmesinin az olduğu ancak arama işlemlerinin çok fazla sayıda yapıldığı sistemler için etkindir. Fonksiyonun fonksiyonlu ve fonksiyonsuz olmak üzere iki biçimi vardır. Fonksiyonlar aranacak elemanı const T &val parametresi ile isterler. Fonksiyonların geri dönüş değerleri bool türündendir. Eleman bulunursa true, bulunamaz ise false değeri ile geri dönerler. Bu fonksiyon sadece elemanın bulunup bulunamadığı bilgisine geri döner. #include <iostream> #include <algorithm> using namespace std; void main() { int val; 100 int a[] = { 3, 8, 9, 72, 14, 25, 17, 7, 42, 53}; sort(a, a+10); cout << "Aranacak eleman :"; cin >> val; if (binary_search(a, a+10, val)) cout << "Bulundu\n"; else cout << "Bulunamadı\n"; } merge() Bu fonksiyonların da fonksiyonlu ve fonksiyonsuz iki versiyonları vardır. Fonksiyonlar merge edilecek sıralı dizinin başlangıç ve bitiş değerine ilişkin ikişer iterator ve hedef dizinin başlangıcına ilişkin bir iterator (yani toplam beş iterator) alırlar. Sınıf Çalışması: Bir text dosyadaki satırları string sınıflarından oluşan bir vector'e yerleştiriniz. Bu vector'ü ortadan iki ayrı vector'e kopyalayınız. Bu iki ayrı vector'ü kendi aralarında sort ediniz. Sonucu birinci vector üzerinde merge ederek ekrana yazdırınız. /* merge.cpp */ #include #include #include #include #include #include <iostream> <fstream> <algorithm> <vector> <string> <cstdlib> using namespace std; void main() { ifstream ifs; string str; vector<string> v1, v2, v3; ifs.open("x.txt"); if (!ifs) { cerr << "Cannot open file\n"; exit(1); } while (getline(ifs, str)) { v1.push_back(str); } v2.resize(v1.size() / 2); v3.resize(v1.size() / 2); copy(&v1[0], &v1[v1.size() / 2], v2.begin()); copy(&v1[v1.size() / 2], &v1[v2.size()], v3.begin()); sort(v2.begin(), v2.end()); sort(v3.begin(), v3.end()); merge(v2.begin(), v2.end(), v3.begin(), v3.end(), v1.begin()); copy(v1.begin(), v1.end(), ostream_iterator<string>(cout, "\n")); 101 } Not: vector sınıfının iteratorleri (yani sınıfın begin() ve end() fonksiyonları ile alınan iteratorleri aslında gösterici olmak zorunda değildir. Gerçi en mantıklı tasarım vector iteratorlerinin doğrudan gösterici alınmasıdır. Bu durumda vector sınıfının örneğin başlangıç ve ortasına ilişkin iki iterator'ü alınacak olsa bu işlem [v.begin(), v.begin() + v.size() / 2] biçiminde yapılmamalıdır. Bu işlemin en garanti yöntemi vector elemanlarının adreslerinin iterator olarak kullanılmasıdır. Çünkü vector elemanlarının peşi sıra yerleştirildiği garanti altına alınmıştır [&v[0], &v[v.size() / 2]] . includes() lower_bound() upper_bound() Bu fonksiyonun da fonksiyonlu ve fonksiyonsuz olarak iki versiyonu vardır. Fonksiyon sort edilmiş iki veri yapısının başlangıç ve bitiş iteratorlerini alır. Birinci dizinin ikinci diziyi kapsayıp kapsamadığına bool değeri ile geri döner. Bu fonksiyonlar ikili arama yöntemini kullanarak sıralı bir dizide insert işlemi yapmaya yönelik iterator değerini geri verirler. binary_search() fonksiyonuna göre daha kullanışlı fonksiyonlardır. Çünkü bu fonksiyonun işlevini kapsayan bir yapıdadırlar. Bu fonksiyonların her ikisinin de fonksiyonlu ve fonksiyonsuz versiyonları vardır. Fonksiyonlar veri yapısının başını ve sonunu temsil eden iki iterator ve const T &val biçiminde bir değer alırlar. lower_bound() val parametresi ile girilen elemana eşit ya da bu elemandan büyük olan ilk elemana ilişkin iterator değerine geri dönerler. Örneğin aşağıdaki dizide fonksiyon 16 ile çağırıldığında verilecek iterator değeri koyu ile gösterilen elemana ilişkindir. 3 7 9 13 17 17 23 Aranacak val değeri 17 olsaydı da aynı iterator değeri elde edilecekti. upper_bound() fonksiyonu ise aranacak val değerinden büyük olan ilk elemanın iterator değerine geri dönmektedir. Yukarıdaki örnekte 16 upper_bound() ile aranıyorsa 23'e ilişkin iterator değeri ile fonksiyon geri döner. lower_bound() ile upper_bound() arasındaki tek fark elemanın bulunması durumunda lower_bound() fonksiyonunun bulunan eleman ilkinin iterator değeri ile geri dönmesi, upper_bound() fonksiyonunun sondan bir sonrakinin iterator değeri ile geri dönmesidir. Örneğin yukarıdaki dizide 17 değerini aramış olalım. 3 7 9 13 17 17 23 lower_bound() upper_bound() Her iki fonksiyonda başarısızlık durumunda end iterator'ü ile geri dönerler. 102 /* lower_bound.cpp */ #include <iostream> #include <algorithm> #include <vector> using namespace std; void main() { int x; vector<int> v; vector<int>::iterator iter; for (int i = 0; i < 10; ++i) v.push_back(rand() % 100); cout << "Val = "; cin >> x; sort(v.begin(), v.end()); copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n")); iter = lower_bound(v.begin(), v.end(), x); v.insert(iter, x); copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n")); } Veri Yapısı Üzerinde Sayısal İşlemler Yapan Algoritmalar accumulate() inner_product() Veri yapısı içerisindeki tüm sayıları + operatörü ile toplar. İki veri yapısının karşılıklı elemanlarının çarpımlarının toplamlarını bulur. Fonksiyon Adaptörleri Fonksiyon adaptörleri STL içerisinde bulunan normal global fonksiyonlardır. Ancak sıradan fonksiyonlardan farkı bir sınıf türüne geri dönmeleridir. Böylece bu fonksiyonlar STL algoritmalarının parametresi olarak çağırıldığında o parametreye ilişkin template parametresi fonksiyonun geri dönüş değeri türünden bir sınıf olur. Örneğin Func() fonksiyonu geri dönüş değeri X sınıfı türünden olan global bir fonksiyon olsun. Bu fonksiyon for_each() içerisinde şöyle çağırılmış olsun: for_each(v.begin(), v.end(), Func(...)); Şimdi derleyici for_each() fonksiyonunu yazarken son parametresinin açılımını X türünden alır. STL içerisinde çeşitli sınıflara geri dönen çeşitli fonksiyon adaptörleri vardır. Bu fonksiyon adaptörleri STL algoritmalarında çok yararlı işlemler yapacak biçimde kullanılabilmektedir. 103 Insert İşlemi Yapan Fonksiyon Adaptörleri copy() fonksiyonu hedef veri yapısına yeni eleman eklemez, yalnızca atama yapar, dolayısıyla copy() fonksiyonunu kullanmadan önce hedef veri yapısı için yeterli yerin tahsis edilmiş olması gerekir. İşte copy() algoritmasıyla insert işlemi yapabilmek için özel fonksiyon adaptörleri kullanılmaktadır. template <class FI1, class FI2> FI2 copy(FI1 first, FI1 last, FI2 dest) { while (first != last) *dest++ = *first++; return dest; } Insert işleminde back_inserter() ve front_inserter() isimli fonksiyon adaptörleri kullanılır. Bu fonksiyonların tek bir parametresi vardır parametre olarak nesne tutan sınıf türünden bir nesne alırlar. back_inserter() fonksiyonunun geri dönüş değeri back_insert_iterator türünden, front_inserter() fonksiyonunun geri dönüş değeri front_insert_iterator türünden sınıf nesneleridir. back_inserter() ve front_inserter() fonksiyonları sözü edilen sınıf türünden bir yerel nesne yaratarak parametresiyle belirtilen sınıf nesnesini yukarıda sözü edilen sınıfların protected veri elemanına yerleştirirler. Örneğin, back_inserter(v) çağırımı sonucunda back_insert_iterator sınıfı türünden bir sınıf nesnesi oluşturulur ve buradaki v değeri bu sınıfın protected bir veri elemanına yerleştirilmiştir. back_insert_iterator ve front_insert_iterator sınıflarının * ve ++ operatör fonksiyonları hiçbir şey yapmayıp nesnenin kendisine geri dönerler. Ancak = operatör fonksiyonları sırasıyla push_back() ve push_front() işlemlerini yapmaktadır. Böylelikle copy() algoritması adeta öne ve arkaya eleman ekleyen bir algoritma haline gelir. Örnek: #include <algorithm> #include <iostream> #include <list> using namespace std; void main(void) { int a[] = {3, 8, 7, 4, 6, 9, 17, 21, 42, 100}; list<int> l; copy(a, a + 10, front_inserter(l)); copy(l.begin(), l.end(), ostream_iterator<int>(cout, "\n")); } Bunların dışında bir de genel inserter() isimli bir fonksiyon adaptörü vardır. Bu fonksiyon adaptörü iki parametre alır, birinci parametresi bir container sınıf nesnesi, ikinci parametresi o nesnedeki bir iterator pozisyonudur. Bu fonksiyon insert_iterator isimli bir sınıfa geri döner ve sınıfın protected iki veri elemanına bu değerleri yazar. Bu sınıfın da * ve ++ operatör 104 fonksiyonları birşey yapmamaktadır, ancak = operatör fonksiyonu belirlenen pozisyona insert işlemi yapıp iterator değerini bir arttırmaktadır. Örnek: #include <algorithm> #include <iostream> #include <vector> using namespace std; void main(void) { int a[] = {3, 8, 7, 4, 6, 9, 17, 21, 42, 100}; vector<int> v(10); copy(a, a + 10, inserter(v, &v[5])); copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n")); } bind1st() ve bind2nd() Fonksiyon Adaptörleri Algoritmalarda kullanılan en güçlü fonksiyon adaptörleridir. Bu adaptörler sayesinde pek çok yararlı durumlar oluşturulabilmektedir. bind2nd() bind1st()'den daha yaygın kullanılmaktadır. İki adaptör benzer amaçlarla kullanılır. bind2nd() binder2nd isimli bir sınıf ile, bind1st() ise binder1st isimli bir sınıf ile geri döner. bind2nd() ve bind1st() fonksiyonları iki parametre alan fonksiyonlardır, bu fonksiyonların birinci parametreleri binary işlem yapan bir fonksiyon nesnesi, ikinci parametreleri T türünden birer değerdir. Örneğin: bind2nd(greater<int>(), 42); Görüldüğü gibi bu fonksiyonların birinci parametreleri binary operatör gibi çalışan birer fonksiyon nesnesidir. Bu fonksiyonların geri döndürdüğü binder2nd ve binder1st sınıflarının bu fonksiyonların parametrelerini tutan iki veri elemanı vardır. Yani geri dönüş değeri olarak verilen sınıf nesnesi içinde bu iki parametre de tutulmaktadır. Aslında bind2nd() ve bind1st() tek parametreli bir fonksiyon nesnesi gibi çalışmaktadır. Bu fonksiyonların geri döndürdükleri sınıfların tek parametreli birer fonksiyon çağırma operatörü vardır. Örneğin bu fonksiyonlar for_each() algoritmasının parametresi yapılırsa for_each() veri yapısı içerisindeki tüm elemanları bu fonksiyon nesnesine parametre yapacaktır. bind2nd() fonksiyonunun şöyle çağırıldığını düşünelim: bind2nd(op, val); Bu fonksiyon binder2nd isimli bir sınıf ile geri döner ve bu sınıfın tek parametreli () operatör fonksiyonu da şöyledir: bool operator()(T elem) { 105 return op(elem, val); } Burada elem parametresi veri yapısı içerisindeki herbir elemanı temsil etmektedir. Bu durumda, bind2nd(op, val); işlemi op(elem, val); işleminin yapılmasına yol açar. bind1st() ile bind2nd() arasındaki tek fark operandların yerleridir. bind1st(op, val); çağırması ile op(val, elem); işlemi yapılır. Görüldüğü gibi bind2nd() ile bind1st() arasındaki tek fark ve isimlendirme sistemi val parametresinin yeri ile ilgilidir. bind2nd() ve bind1st() Fonksiyonlarının Kullanımına İlişkin Örnekler 1- _if sonekli ya da unary predicate alan algoritmalardaki kullanım: Bir veri yapısında örneğin 10'dan büyük elemanları silecek olalım. #include <algorithm> #include <iostream> #include <list> using namespace std; void main(void) { int a[] = {3, 8, 7, 4, 6, 9, 17, 21, 42, 100}; list<int> l; copy(a, a + 10, back_inserter(l)); list<int>::iterator iter; iter = remove_if(l.begin(), l.end(), bind2nd(greater<int>(), 10)); l.erase(iter, l.end()); copy(l.begin(), l.end(), ostream_iterator<int>(cout, "\n")); } Bilindiği gibi remove() fonksiyonları gerçek bir silme yapmaz, ancak silinmiş diziyi oluşturur ve bu dizinin son değerinin iteratoru ile geri döner. 106 2- find_if() fonksiyonu ile bir veri yapısı içerisindeki 10'dan büyük ilk değeri bulacağımızı düşünelim: #include <algorithm> #include <iostream> #include <functional> using namespace std; void main(void) { int a[] = {3, 8, 7, 4, 6, 9, 17, 21, 42, 100}; int *pIter; } pIter = find_if(a, a + 10, bind2nd(greater<int>(), 10)); if (pIter == a + 10) cout << "basarisiz...\n"; else cout << "buldu : " << *pIter << "\n"; 3- bind fonksiyonları for_each() algoritmasıyla ya da transform() algoritmasıyla etkin bir biçimde kullanılabilir. Örneğin: string'lerden oluşan bir vector içerisinde, içerisinde "person" (veya başka bir yazı) geçen string'leri silecek olalım. Bu işlem doğrudan bir fonksiyon nesnesi yazılarak da yapılabilir. Burada diğer kullanımları anlayabilmek için bind2nd() fonksiyonuyla işlem yapılacaktır. Cevap: #include #include #include #include <algorithm> <iostream> <functional> <vector> using namespace std; template <class T1> struct equal_to_str : public std::binary_function<T1, T1, T1> { T1 operator() (T1 str1, T1 str2) const { return strstr(str1, str2); } }; void main(void) { char *a[] = { "ankara", "istanbul", "izmit", "burdur" }; vector<char *> v; copy(a, a + 4, back_inserter(v)); copy(v.begin(), v.end(), ostream_iterator<char *>(cout, "\n")); vector<char *>::iterator iter; iter = remove_if(v.begin(), v.end(), bind2nd(equal_to_str<char *>(), "stanb")); 107 } v.erase(iter); copy(v.begin(), v.end(), ostream_iterator<char *>(cout, "\n")); 4- int bir veri yapısı içerisindeki 3'e bölümünden elde edilen kalanları ekrana yazdıran programı transform() ve modulus fonksiyon ve fonksiyon nesnelerini kullanarak ekrana yazdırınız. #include <algorithm> #include <iostream> #include <functional> using namespace std; void main(void) { int a[] = {3, 8, 7, 4, 6, 9, 17, 21, 42, 100}; } transform(a, a + 10, ostream_iterator<int>(cout, "\n"), bind2nd(modulus<int>(), 3)); Anahtar Notlar: Standartlara eklenmemiş olan copy_if() algoritması aşağıdaki gibi yazılabilir. #include <functional> #include <iostream> #include <algorithm> using namespace std; template <class A, class B, class C> B copy_if(A first, A last, B dest, C f) { while (first != last) { if (f(*first)) *dest++ = *first; ++first; } } return dest; void main(void) { int a[] = {3, 9, 21, 42, 18, 7, 8, 12, 13, -2}; copy_if(a, a + 10, ostream_iterator<int>(cout, "\n"), bind2nd(greater<int>(), 10)); } Özetle bind1st() ve bind2nd() tek parametreli bir fonksiyon gibi işlem görür ve kullanılır. Bu tek parametreli fonksiyon algoritma tarafından çağırıldığında aslında fonksiyonun ilk parametresindeki iki parametreli fonksiyon çağırılır. 108 C++ ve Standart C Fonksiyonları C++'da C'nin standart kütüphane fonksiyonlarının hepsi standardizasyonda belirtilmiştir. Ancak iki önemli farklılık vardır: kullanılabilir, bu durum 1- Standart başlık dosyalarının isimleri değiştirilmiştir. Değiştirme x.h olan başlık dosyasının cx haline getirilmesi biçiminde yapılmıştır. Örneğin: stdlib.h başlık dosyası cstdlib, stdio.h başlık dosyası cstdio biçimindedir. Standart C fonksiyonları için artık bu dosyaların include edilmesi daha uygundur, çünkü standartlara göre artık standart C'nin x.h dosyalarını C++ derleyicileri artık bulundurmak zorunda değildir. Tabii dosyaları cx biçiminde include edersek bu sefer eski derleyicilerde problem çıkacaktır. 2- Standart C fonksiyonlarının isimleri std isim aralığı içerisindedir. Bugünkü C++ derleyicilerinin hemen hepsi aynı zamanda C derleyicisi olarak da kullanılabilmektedir. Bu nedenle başlık dosyalarının iki kere yazılması yerine C için tek bir başlık dosyası oluşturulup C++'dan bu include edilmiştir. Örneğin cstdlib dosyası şöyle oluşturulmuştur: #include <stdlib.h> namespace std { using ::malloc, ::atoi, ...; } Bu düzünlemede maalesef bu fonksiyon isimleri global namespace içerisine de sokulmuş durumdadır. Örneğin yalnızca cstdlib include edilip sanki global bir biçimde atoi() kullanılabilir, ama tabii programcının bunları std isim aralığı içerisinde olduğunu bilerek varsayması doğrudur. Üye Fonksiyon Göstericileri ve Referansları Global fonksiyonlara ilişkin göstericiler C++’da tamamen C'deki gibi tanımlanır. Ancak C++'da yalnızca fonksiyon göstericisi değil fonksiyon referansı kavramı da vardır. Bilindiği gibi bir referansa nesnenin kendisi ile ilkdeğer vermek gerekir. Nesnenin adresinin alınarak referansa yerleştirilmesi derleyici tarafından yapılır. C'de bir fonksiyonun ismi zaten sağ taraf değeri olarak fonksiyonun adresini belirtir. Peki o halde fonksiyon referansına nasıl ilkdeğer vereceğiz? İşte fonksiyonun ismi C++'da hem fonksiyonun kendisi hem de onun adresi anlamına gelmektedir. Dolayısıyla fonksiyon isminin bir daha adresinin alınması da kabul edilmiştir. Aşağıdaki ilkdeğer vermelerin hepsi C++'da geçerlidir. void (&r)(void) = Func; void (*p)(void) = Func; void (*p)(void) = &Func; 109 C++'da farklı parametre yapılarına sahip aynı isimli fonksiyon olabildiği için derleyiciler fonksiyon ismi kullanıldığında atamanın solundaki fonksiyon göstericisi ya da referansının bildirimine bakarak hangisi olduğunu anlarlar. Referans ya da gösterici yoluyla fonksiyonun çağırılması C'de olduğu gibi fonksiyon çağırma operatörüyle yapılır. Mesela: r(); p(); (*p)(); Üye fonksiyonların isimleri de yine üye fonksiyonların başlangıç adresleridir, ancak üye fonksiyon türünden fonksiyon göstericileri X bir sınıf ismi olmak üzere aşağıdaki gibi tanımlanır: void (X::*p)(void); Burada * atomunun yeri doğrudur. Bu tanımlamaya göre: 1- p bir üye fonksiyon göstericisidir. 2- p göstericisine yalnızca X sınıfının geri dönüş değeri void, parametresi void olan fonksiyonların adresleri atanabilir. class Sample { public: void Func(); //... }; void (Sample::*p)(void); p = &Sample::Func; // p = Sample::Func; C++'da fonksiyon göstericisi tanımlarken parametre parantezinin içini boş bırakmak tamamen void yazmakla aynıdır. Üye fonksiyon referansı da tamamen benzer şekilde tanımlanır: void (Sample::&p)(void) = Sample::Func; Üye fonksiyon göstericileri sınıfın veri elemanı olmak zorunda değildir, yani global ya da yerel bir nesne biçiminde tanımlanabilir. Ayrıca bir sınıfın içerisinde fonksiyon göstericisi tanımlandığında o default olarak üye fonksiyon göstericisi anlamına da gelmez. Yine üye fonksiyon göstericileri aynı syntax ile tanımlanmalıdır. Örneğin, aşağıda m_p1 bir global fonksiyon göstericisi, m_p2 bir üye fonksiyon göstericisidir. class Sample { public: void Func(); void (*m_p1)(void); void (Sample::*m_p2)(void); //... }; 110 Üye Fonksiyon Göstericilerini ya da Referanslarını Kullanarak Üye Fonksiyonların Çağırılması p bir üye fonksiyon göstericisi olsun, p() gibi bir çağırma geçerli değildir. Çünkü üye fonksiyonlar sınıf nesneleriyle çağırılmalıdır. İlk akla gelen aşağıdaki gibi bir çağırımdır ama o da geçerli değildir: Sample x; x.p(); Çünkü bu syntax'da derleyici p ismini x nesnesnesinin ilişkin olduğu sınıfın (burada Sample) faaliyet alanında arar. Halbuki p sınıfın faaliyet alanında olan bir isim olmak zorunda değildir. Üye fonksiyon göstericilerini çağırmak için C++'a .* ve ->* biçiminde iki yeni operatör eklenmiştir. Bu operatörler binary infix operatörlerdir, C++ öncelik tablosunun ikinci düzeyinde bulunurlar. C'nin klasik unary operatörleri C++'da tablonun üçüncü önceliğine indirilmişlerdir. Yani bu operatörler ++ ve -- gibi unary operatörlerden yüksek öncelikli, ancak (), [] gibi operatörlerden düşük önceliklidir. .* operatörünün sol tarafındaki operand bir sınıf nesnesinin kendisi, sağ tarafındaki operand ise o sınıf türünden bir üye fonksiyon göstericisi olmalıdır. Tipik çağırma aşağıdaki gibi yapılır: Sample x; void (Sample::*p)(void); p = &Sample::Func; (x.*p)(); Burada parantezler zorunludur çünkü fonksiyon çağırma operatörünün önceliği .* operatöründen fazladır. ->* operatörü de aynı biçimde üye fonksiyonu çağırmak için kullanılır ama bu operatörün sol tarafındaki operand sınıf türünden nesne değil adres olmalıdır. Sample *pX = new Sample(); void (Sample::*p)(void); p = &Sample::Func; (pX->*p)(); Bir sınıfın veri elemanı olarak kendi sınıfı türünden bir fonksiyon göstericisine sahip olması durumuna sıkça rastlanır. Örneğin: class Sample { public: void Func(); void (Sample::*m_pf)(); }; void Sample::Func() { (this->*m_pf)(); //... } 111 Burada Func() üye fonksiyonu içerisinde m_pf üye fonksiyon göstericisinin gösterdiği üye fonksiyon Func() fonksiyonunun çağırıldığı aynı nesne ile çağırılmış olur. Özellikle sınıfın static bir üye fonksiyon gösterici dizisinin olması ve bu dizi içerisinde üye fonksiyonların adreslerinin tutulması ve bu fonksiyonların da sınıfın başka bir üye fonksiyonu içerisinden çağırılması gibi durumlarla karşılaşılmaktadır. Örneğin MFC'deki mesaj haritaları bu tür yapılardır. C++'da taban sınıf türünden üye fonksiyon göstericisine türemiş sınıfın üye fonksiyon adresi atanamaz. Çünkü bu durum gösterici hatasına yol açabilecek tehlikeli bir durumdur. Yani taban sınıf türünden üye fonksiyon göstericisi taban sınıf türünden nesneyle çağırılır, fakat aslında çağırılacak olan fonksiyon türemiş sınıfa ait olacağından bu gösterici hatasına yol açar. class X { public: void FuncX(); }; class Y : public X { public: void FuncY(); }; void (X::*pX)(); pX = &Y::FuncY; X x; (x.*pX)(); //error Ancak bunun tersi, yani türemiş sınıf üye fonksiyon göstericisine taban sınıf üye fonksiyonunun adresinin atanması normal ve geçerli bir durumdur. Çünkü türemiş sınıf nesnesiyle taban sınıf üye fonksiyonunun çağırılması gibi bir durum oluşur. void (Y::*pY)(); pY = &X::FuncX; Y y; (y.*pY)(); //normal mem_fun_ref() ve mem_fun() Adaptör Fonksiyonları STL içerisinde pek çok algoritma global fonksiyonların çağırılması üzerine dayandırılmıştır, halbuki pek çok durumda bir sınıfın üye fonksiyonunun çağırılması istenir. Örneğin, çokbiçimli bir sınıf sistemi olduğunu düşünelim ve taban sınıf göstericilerine ilişkin bir nesne tutan sınıfımız olsun. list<Base *> x; Şimdi biz bu bağlı liste içerisindeki her gösterici için Func() isimli bir sanal fonksiyon çağıracak olalım. Iterator'lerden faydalanılarak aşağıdaki gibi bir kod yazılabilir: 112 list<Base *>::iterator iter; for (iter = x.begin(); iter != x.end(); ++iter) (*iter)->Func(); İşte bu işlem aşağıdaki gibi de yapılabilir: for_each(x.begin(), x.end(), mem_fun(&Base::Func)); Burada bu işlem bağlı listenin içerisindeki herbir adres ile sınıfın Func() üye fonksiyonunun çağırılmasını sağlamaktadır. mem_fun() ile mem_fun_ref() fonksiyonları arasında gösterici ya da nesnenin kendisiyle çağırılması bakımından bir fark vardır. Şüphesiz nesnenin kendisiyle çağırılması durumunda çokbiçimlilik devreye girmez. Örneğin: list<A> x; ... for_each(x.begin(), x.end(), mem_fun_ref(&A::Func)); Gerçekte neler olmaktadır? Aslında mem_fun_ref() fonksiyonunun geri dönüş değeri mem_fun_ref_t sınıfı türünden bir nesnedir. Olaylar şöyle gerçekleşmektedir: 1- mem_fun_ref() fonksiyonunun geri dönüş değeri mem_fun_ref_t türünden bir sınıf nesnesidir. Bu durumda for_each() fonksiyonunun son parametresi bu sınıf türünden olacaktır. 2- mem_fun_ref_t sınıfının veri elemanı içerisinde mem_fun_ref() fonksiyonunun içerisinde belirtilen üye fonksiyon adresi tutulur. 3- mem_fun_ref_t sınıfının () operatör fonksiyonunun parametresi template türünden nesnedir ve bu fonksiyon bu nesne ile ilgili üye fonksiyonu çağırır. Örneğin Microsoft derleyicilerinin functional başlık dosyasında tasarımı aşağıdaki gibidir: template<class _R, class _Ty> inline mem_fun_ref_t<_R, _Ty> mem_fun_ref(_R (_Ty::*_Pm)()) { return mem_fun_ref_t<_R, _Ty>(_Pm); } Görüldüğü gibi fonksiyonun parametresi bir üye fonksiyon göstericisi, geri dönüş değeri ise template sınıf türünden bir nesnedir. template<class _R, class _Ty> class mem_fun_ref_t : public unary_function<_Ty, _R> { public: explicit mem_fun_ref_t(_R (_Ty::*_Pm)()) : _Ptr(_Pm) {} _R operator()(_Ty &_X) const { return (_X.*_Ptr)(); 113 } private: _R (_Ty::*_Ptr)(); }; mem_fun() fonksiyonu ve mem_fun_t sınıfları tamamen buradaki gibidir. Tek fark üye fonksiyonun .* ile değil ->* ile çağırılmasıdır. Görüldüğü gibi burada çağırılacak üye fonksiyon herhangi bir geri dönüş değerine sahip olabilir ama parametresi void olmalıdır. Fakat bunların yanısıra mem_fun1() ve mem_fun1_ref() fonksiyonları ve sınıfları da vardır. Bu fonksiyonların farkı çağırılacak üye fonksiyonun void değil bir parametresinin olmasıdır. mem_fun() ve mem_fun_ref() Fonksiyonlarına İlişkin Uygulamalar Örnek 1: #include #include #include #include <iostream> <functional> <algorithm> <list> using namespace std; class A { public: virtual bool Func() = 0; }; class B : public A { public: virtual bool Func() { cout << "I am B::Func\n"; return true; } }; class C : public A { public: virtual bool Func() { cout << "I am C::Func\n"; return true; } }; void main(void) { list<A *> x; x.push_back(new B()); 114 x.push_back(new C()); x.push_back(new B()); for_each(x.begin(), x.end(), mem_fun(&A::Func)); } Örnek 2: #include #include #include #include #include <list> <iostream> <algorithm> <functional> <string> using namespace std; void main(void) { list<string> x; x.push_back("Ali"); x.push_back("Mehmet"); x.push_back("Ziya"); x.push_back("Cenk"); x.push_back("Ali"); const char *pstr[5]; transform(x.begin(), x.end(), pstr, mem_fun_ref(&string::c_str)); copy(pstr, pstr + 5, ostream_iterator<const char *>(cout, "\n")); } Not: Yukarıdaki kod VC++ 98'de çalışmamaktadır. Sınıf Çalışması: string'lerden oluşan bir bağlı liste kurunuz, bazı elemanların içini erase() fonksiyonu ile siliniz, daha sonra mem_fun_ref() fonksiyonunu kullanarak boş olan elemanları remove_if() ve ardından erase() fonksiyonuyla siliniz. Çağırılacak üye fonksiyon string::empty() olmalıdır. Cevap: #include #include #include #include #include #include <iostream> <string> <list> <algorithm> <stdlib.h> <functional> using namespace std; void main (void) 115 { list<string> l; list<string>::iterator iter; l.push_back("adana"); l.push_back("izmit"); l.push_back("sakarya"); l.push_back("samsun"); l.push_back("bolu"); l.push_back("sivas"); l.push_back("ankara"); iter = l.begin(); iter++; (*iter).erase(); for (int i = 0; i < 3; i++) iter++; (*iter).erase(); iter = remove_if(l.begin(), l.end(), mem_fun_ref(&string::empty)); l.erase(iter, l.end()); copy(l.begin(), l.end(), ostream_iterator<string>(cout, "\n")); system("pause"); } Not: Yukarıdaki kod VC++ 98'de çalışmamaktadır. mem_fun1() ve mem_fun1_ref() fonksiyonları anlamlı bir biçimde bind fonksiyonlarıyla kullanılabilir. Örneğin: bind2nd(mem_fun1_ref(&X::Func), val); Burada sonuçta X sınıfının tek parametreli Func() fonksiyonu hep val parametresiyle çağırılmaktadır. İşlemin mekanizması biraz karışıktır. pair Sınıfı Bu sınıf first ve second isimli iki public veri elemanına sahiptir. Bu iki elemanın türü iki template parametresi türündendir. Sınıf yalnızca birbirleriyle ilişkili iki elemanı birlikte tutmakta kullanılır. Bildirimi şöyledir: template <class T1, class T2> struct pair { T1 first; T2 second; pair() {} pair(const T1 &x, const T2 &y) { first = x; 116 second = y; } template <class u, class v> pair (const pair <u, v> &p) { first = p.first; second = p.second; } }; Görüldüğü gibi pair sınıfı yalnızca iki elemanı tutmakta kullanılır, ancak STL içerisinde bir yardımcı sınıf olarak da kullanılmaktadır. Özellikle fonksiyonun birbiriyle ilişkili iki bilgi vermesi durumunda fonksiyonun geri dönüş değerinin pair sınıfı türünden olması biçiminde kullanımlara rastlanmaktadır. Örneğin, bir fonksiyon bir arama yapsın ve bir numara bulacak olsun. Numara int türünden olsun ve her değeri alabilsin. Bu durumda elde edilecek numara her değeri alabileceğine göre başarısızlık nasıl anlaşılacaktır? işte bunun için fonksiyon aşağıdaki gibi tasarlanabilir: pair<bool, int> SearchNumber(const char *name); Kullanımı şöyle olabilir: pair<bool, int> result; result = SearchNumber("aliserce"); if (!result.first) { cerr << "error\n"; exit(1); } cout << result.second << endl; new ve delete Operatörlerinin Ayrıntılı Bir Biçimde İncelenmesi new ve delete işlemleri yapıldığında derleyici dinamik tahsisatları yapabilmek için global düzeyde tanımlanmış operator new() ve operator delete() fonksiyonlarını kullanır. Örneğin X bir sınıf olmak üzere, p = new X(); bu işlemde önce operator new() fonksiyonu çağrılarak sizeof(X) kadar bir alan tahsis edilir, sonra bu tahsisattan elde edilen adres this göstericisi yapılarak sınıfın başlangıç fonksiyonu çağırılır. Burada eğer X sınıfının bir operator new() fonksiyonu var ise global olan değil öncelikle sınıfa ilişkin olan çağırılacaktır. new ve delete operatör fonksiyonlarının tekil ([]'siz) ve çoğul ([]'li) versiyonları vardır. C++ derleyicileri global düzeyde (yani hiçbir namespace içerisinde değil) aşağıdaki tahsisat için gereken operatör fonksiyonlarını bulundurmalıdır: 117 1- void *operator new(std::size_t size) throw(std::bad_alloc); Bu operatör fonksiyonu new T; yapıldığında çağırılan operatör fonksiyonudur. Yani biz p = new T; // p = new T(); yaptığımızda derleyici aslında şu işlemi yapmaktadır: p = (T *)operator new(sizeof(T)); operator new() fonksiyonu doğrudan bizim tarafımızdan da çağırılabilir. Örneğin: int *p; p = (int *) operator new(sizeof(int)); Fonksiyon başarısızlık durumunda bad_alloc isimli bir sınıf ile throw eder. Yani, eğer tahsisatın başarısı kontrol edilmek isteniyorsa aşağıdaki gibi yapılabilir: try { p = new T; //... } catch (std::bad_alloc) { //... } Bu fonksiyonun doğrudan çağırılabilmesi için <new> dosyasının include edilmesine gerek yoktur. operator new() fonksiyonu programcı tarafından global düzeyde yazılabilir, bu durumda programcının yazdığı fonksiyon new işlemlerinde çağırılır. Örneğin: void *operator new(size_t size) throw(std::bad_alloc) { void *pBuf; pBuf = malloc(size); if (pBuf == NULL) throw std::bad_alloc(); return pBuf; } Global operator new() fonksiyonunu yazdığımız zaman kütüphanedeki global sınıf nesneleri için çağırılan başlangıç fonksiyonları içerisinde new yapılmışsa yine bizim yazdığımız fonksiyon çağırılacaktır. Yani yazdığımız fonksiyonun main() fonksiyonuna girişten daha önce çağırılmış olması normaldir. Yukarıdaki global operator new fonksiyonunun eski versiyonunda exception specification kullanılmamıştır. 118 2- void *operator new(std::size_t size, const std::nothrow_t &) throw(); Bu versiyonu kullanmak için prototipinin bulunduğu <new> dosyasını include etmek gerekir. <new> içerisinde aşağıdaki tanımlamalar da yapılmıştır: namespace std { struct nothrow_t {}; extern const nothrow_t nothrow; } Bu biçimi kullanmak aşağıdaki gibi olabilir: p = new(nothrow) T; nothrow nothrow_t türünden <new> içerisinde tanımlanmış global bir nesnedir. Yalnızca tür bilgisi oluştursun diye tanımlanmıştır. Bu fonksiyonun öncekinden tek farkı başarısızlık durumunda bad_alloc değerine throw etmemesi, NULL ile geri dönmesidir. Yani programcı exception handling mekanizmasını kullanmak istemezse bu şekli kullanabilir. 3- void *operator new[] (std::size_t size) throw(std::bad_alloc); Bu biçim birinci biçimin []'li versiyonudur. Örneğin aşağıdaki durumda bu fonksiyon çağırılır: p = new T[n]; Bu durumda derleyici aşağıdaki gibi bir çağırma yapar: p = (T *)operator new(n * sizeof(T)); Derleyiciler bu fonksiyonu doğrudan return operator new(size); biçiminde yazarlar. Yani, biz []'li versiyonunu yazmasak ama []'siz versiyonunu yazsak []'li tahsisat yaptığımızda en sonunda bizim yazdığımız []'siz versiyonu çağırılır. 4- void *operator new[] (std::size_t size, const std::nothrow_t &) throw(); Bu versiyon ikinci biçimin []'li versiyonudur. Derleyicinin default olarak bulundurduğu bu fonksiyon []'siz versiyonunu çağırır. 5- void *operator new(std::size_t size, void *ptr) throw(); Bu placement versiyonu ileride ele alınacağı gibi başlangıç fonksiyonu çağırmakta kullanılır. Örneğin: new(p) T(); 119 Kütüphanedeki default versiyon ikinci parametresine dönmekten başka birşey yapmaz ve aşağıdaki gibi yazlımıştır: void *operator new(std::size_t size, void *ptr) throw() { return ptr; } Bu biçim önceden yaratılmış bir alan için başlangıç fonksiyonunu çağırmak amacıyla yaygın olarak kullanılır. Bilindiği gibi new operatörü kullanıldığında derleyici yukarıda belirtilen operator new() fonksiyonlarından birini çağırıp bir adres elde etmekte ve o adresi this göstericisi olarak kullanıp başlangıç fonksiyonunu çağırmaktadır. Şimdi biz s isminde yerel bir diziyi sanki bir sınıf gibi kullanıp başlangıç fonksiyonunu çağırmak isteyelim. Sınıfın başlangıç fonksiyonu normal bir fonksiyon gibi çağırılamadığı için tek yöntem aşağıdaki gibidir: char s[sizeof(T)]; new(s) T(); Burada new mekanizması kandırılarak aslında bir tahsisat yapılmadan başlangıç fonksiyonu çağırılmaktadır. Yukarıdaki kodda operator new() fonksiyonunun placement versiyonu çağırılacak, bu da hiç birşey yapmadan s'in kendisine geri dönecek ve böylece s için başlangıç fonksiyonu çağırılacaktır. Bu durumda dinamik tahsisat sırasında başlangıç fonksiyonunda oluşan throw işleminin otomatik free işlemine yol açmasını da dikkate alarak, p = new T(); işleminin tamamen sembolik eşdeğeri, p = (T *)operator new(sizeof(T)); try { new(p) T(); } catch(...) { operator delete(p); throw; } 6- void *operator new[] (std::size_t size, void *) throw(); Bu biçim tekil placement versiyonunun []’li biçimidir. Bu fonksiyon da hiç bir şey yapmadan ikinci parametresi ile belirtilen adrese geri döner. Bir grup sınıf nesnesi için başlangıç fonksiyonu çağırmak için kullanılır. Örneğin: X s[sizeof(X) * SIZE]; new(s) X[SIZE]; Burada önce new operatörünün yukarıda belirtilen []’li placement versiyonu çağırılır. Elde edilen adres kullanılarak SIZE kadar eleman için tek tek başlangıç fonksiyonu çağırılır. 120 Burada ele alınan altı new tahsisat fonksiyonunun hepsi eğer programcı aynısından yazarsa yer değiştirilme özelliğine sahiptir. 7- void operator delete(void *ptr) throw(); Bu normal []’siz delete fonksiyonudur. Örneğin delete p; gibi bir işlemde derleyici önce p adresindeki nesne için bitiş fonksiyonunu çağırır daha sonra bu operator delete fonksiyonunu çağırarak boşaltma işlemini gerçekleştirir. Yani delete p; işleminin karşılığı şöyledir: p->~X(); operator delete(p); 8- void operator delete[] (void *ptr) throw(); Bu biçim []’li delete işlemi için çağırılan operatör fonksiyonudur. Yani delete[] p; gibi bir işlem yapıldığında eğer p bir sınıf türünden ise derleyici önce daha önce tahsis edilen dizi elemanları için bitiş fonksiyonunu çağırır, sonra da bu delete operatör fonksiyonunu çağırır. []’li delete işlemlerinde derleyici kaç eleman için bitiş fonksiyonunu çağıracağını nereden bilecektir? Derleyici nasıl bir kod üretmelidir ki daha önce tahsis edilen tüm dizi elemanları için bitiş fonksiyonları çağırılsın? Derleyici bu bilgiyi derleme zamanı içerisinde elde edemez. Kullanılan tahsisat algoritmasından da bu bilgiyi elde edemez. Bu bilgiyi []’li tahsisat işlemi sırasında tahsis edilen alanın içerisine yazmak zorundadır. Bu nedenle []’li versiyon kullanılarak X sınıfı türünden n elemanlı bir alan n * sizeof(X) değerinden daha büyük olabilir. 9- void operator delete(void *ptr, void *) throw(); Bu operatör fonksiyonu hiçbir şey yapmaz. Yalnızca durum tespitinin yapılması için düşünülmüştür. Anımsanacağı gibi bir sınıf türünden dinamik tahsisat yapıldığı durumlarda tahsisat başarılı olup sınıfın başlangıç fonksiyonu çağırıldığı zaman başlangıç fonksiyonunda throw oluşmuş ise akış catch bloğuna gidiyor. Ancak tahsis edilen alan otomatik olarak boşaltılıyor. İşte bu otomatik boşaltma sırasında da yine delete operatör fonsiyonu çağırılmaktadır. Tahsisat hangi türden new operatör fonksiyonu ile yapılmış ise o türden delete operatör fonksiyonu ile boşaltılır. Örneğin aşağıdaki gibi placement new operatörü ile başlangıç fonksiyonu çağırılırken başlangıç fonksiyonu içerisinde throw oluşmuş olsun new(p) X(); 121 işte derleyici tahsis edilen alanın otomatik boşaltımı için yine yukarıdaki placement delete operatörünü çağıracaktır. Bu placement delete operatörünün ciddi bir işlevi yoktur. Ancak programcı çeşitli durum tespit mesajlarını bu fonksiyonu içerisine yazdırabilir. 10- void operator delete[] (void *ptr, void *) throw(); Bu fonksiyon da hiç bir şey yapmaz. []’li placement new fonksiyonunun delete versiyonudur. 11- void operator delete(void *ptr, const std::nothrow_t &) throw(); nothrow biçimli bir delete normal olarak anlamlı değildir. Çünkü zaten normal delete operatör fonksiyonları da herhangi bir değere throw edemez. Bu biçim de sınıf türünden nothrow biçimli new operatör foksiyonu kullanılarak yapılan tahsisatlarda oluşan throw işlemlerinde delete operatör fonksiyonu olarak kullanılmaktadır. Çünkü yukarıda da belirtildiği gibi tahsisat sırasındaki otomatik silme işlemlerinde hangi türden new ile işlem yapılmışsa o türden delete derleyici tarafından otomatik olarak çağırmaktadır. Bu fonksiyonun kütüphanedeki orijinali normal operator delete fonksiyonunu çağırmaktadır. 12- void operator delete[](void *ptr, const std::nothrow_t &) throw(); Bu biçim []’li delete operatörünün nothrow’lu versiyonudur. allocator Sınıfı STL içerisinde allocator isimli bir template sınıf vardır. Bu sınıf tek bir template parametresi alır. template <class T> class allocator{ //... }; allocator sınıfı dinamik tahsisat yapmakta kullanılan bir sınıftır. Yani bu sınıfın üye fonksiyonları tahsisat yapar, tahsis edilmiş alanı boşaltır, tahsis edilmiş alan için başlangıç ve bitiş fonksiyonlarını çağırır. STL içerisindeki nesne tutan sınıfların hepsi template parametresi olarak tahsisat işlemlerinde kullanılacak bir sınıf ister ve tahsisat işlemlerini bu sınıfın üye fonksiyonlarını kullanarak yapar. Yani örneğin list sınıfı tahsisatı doğrudan new operatörünü kullanark değil allocator sınıfının üye fonksiyonunu kullanarak yapar. Böylelikle programcı isterse kendisi bir tahsisat sınıfı yazabilir ve tahsisat işleminde nesne tutan sınıfların kendi tahsisat sınıflarını kullanmasını sağlayabilir. Nesne tutan sınıfların hepsi programcıdan bir tahsisat sınıfı ister ve bu tahsisat sınıfı türünden nesneyi sınıfın protected bölümünde tanımlar. Bu nesneyi kullanarak tahsisat işlemlerini yapar. Tabii nesne tutan sınıflar ile çalışırken tahsisat sınıfına ilişkin template parametresi default değer almıştır. Programcı tahsisat sınıfını belirtmez ise allocator sınıfı tahsisat sınıfı olarak kullanılacaktır. Örneğin list sınıfı aşağıdaki gibidir: 122 template <class T, class A = allocator<T> > class list { public: //... protected: A a; }; Görüldüğü gibi tasarımcı list sınıfının üye fonksiyonlarını yazarken gereken tahsisatlar için doğrudan new operatörünü kullanmamıştır. Tahsisat işlemlerini tahsisat sınıfının üye fonksiyonlarını çağırarak yapmıştır. Programcı yeni bir tahsisat sınıfı yazacaksa standartlarda belirtilen typedef isimlerini ve üye fonksiyonlarını aynı isimle yazmak zorundadır. Çünkü bu isimler doğrudan nesne tutan template sınıflar tarafından kullanılmaktadır. Bu nedenle yeni bir tahsisat sınıfı yazacak olan kişiler bu işlemi kolay yapmak için zaten var olan allocator sınıfından sınıf türetip yalnızca gereken fonksiyonları o sınıf için yazarlar. Yani örneğin myallocator isimli yeni bir tahsisat sınıfı yazacak olalım. Bu işlem türetme yöntemi ile şöyle olabilir: template <class T> class myallocator : public allocator<T> { //... }; Tahsisat Sınıfları Neden Kullanılır? Programcı bir tahsisat sınıfı yazmamışsa tahsisat sınıfı olarak default template parametresinden hareketle allocator sınıfı kullanılacaktır. Bu allocator sınıfının tahsisat yapan fonksiyonları da tahsisat işlemlerinde global tahsisat fonksiyonlarını kullanır. Sonuç olarak aksi belirtilmediği sürece nesne tutan sınıflar için tahsisatlar yine operator new() ve operator delete() fonksiyonları ile yapılmış olur. Ancak programcı başka heap alanları kullanıyor olabilir. Hatta başka tahsisat fonksiyonları kullanıyor olabilir. Hatta bu alanlardan tahsisat yapan başka tahsisat fonksiyonları kullanıyor olabilir. Bu durumda bu nesne tutan sınıfların istenilen heap alanlarından tahsisat yapabilmesi için ayrı tahsisat sınıflarının yazılması gerekir. Örneğin Win32’de birden fazla heap yaratılabilmektedir. operator new() ve operator delete() fonksiyonları CRT(C runtime library) heap’ini kullanırlar (Win32’de operator new() malloc() fonksiyonunu, malloc() fonksiyonu HeapAlloc() API fonksiyonunu çağırır. HeapAlloc() API fonksiyonu da CRT üzerinden tahsisat yapar). Şimdi biz Win32’de CreateHeap() fonksiyonu ile başka bir heap yaratmış olalım ve STL list sınıfının bu heap üzerinden tahsisat yapmasını isteyelim. Bu durumda biz bir tahsisat sınıfı yazmalıyız. Tahsisat sınıfındaki tahsisat yapan fonksiyonun da bizim yarattığımız heap’den tahsisat yapmasını sağlamalıyız. 123 allocator Sınıfının Elemanları Sınıfta aşağıdaki typedef isimleri bulunmak zorundadır. typedef typedef typedef typedef typedef typedef typedef size_t ptrdiff_t T const T T const T T size_type; difference_type; *pointer; *const_pointer; &reference; &const_reference; value_type; size_t ve ptrdiff_t türlerinin ne olduğu bilindiği gibi derleyicileri yazanlara bırakılmıştır. size_t işaretsiz herhangi bir tür olabilir. ptrdiff_t de herhangi bir tür olabilmektedir. Genellikle derleyiciler size_t türünü unsigned int, ptrdiff_t türünü ise signed int olarak alırlar. C'de bu türler stddef.h dosyasında C++'da ise cstddef dosyasında typedef edilmiştir. allocate() Fonksiyonu Tahsisat sınıfının tahsisat işlemini bu fonksiyon yapar. pointer allocator(size_type n, allocator<void>::const_pointer hint = 0); Fonksiyonun birinci parametresi template türü T olmak üzere kaç tane T türünden tahsisat yapılacağıdır. Fonksiyonun ikinci parametresi const void * türündendir ve default NULL değeri alır. Orijinal allocator sınıfının bu fonksiyonu global operator new() fonksiyonunu çağırarak tahsisat yapar. Fonksiyonun geri dönüş değeri tahsis edilen alanın başlangıç adresidir ve T * türündendir. Fonksiyonun ikinci parametresi tahsisat fonksiyonunun performansını arttırmak amacı ile düşünülmüştür. Yani bu parametreye daha önce tahsis edilmiş bir bloğun adresi geçirilirse belki de tahsisat algoritması daha iyi yöntemler kullanabilecektir. Bu fonksiyon yalnızca tahsisat işlemini yapar, yani aslında doğrudan operator new(n * sizeof(T)) parametresi ile çağırılmış şeklidir. Bütün nesne tutan sınıflar tahsisatlarını tahsisat sınıfının allocate() fonksiyonu ile yaparlar. /* allocate.cpp */ #include <iostream> #include <algorithm> using namespace std; int main() { allocator<int> x; allocator<int>::pointer p; 124 p = x.allocate(10, NULL); memset(p, 0, 10 * sizeof(int)); copy(p, p+10, ostream_iterator<int>(cout, "\n")); } return 0; deallocate() Fonksiyonu Bu fonksiyon tamamen tahsis edilen alanın boşaltılması amacı ile kullanılır. Nesne tutan sınıflar free işlemi için bu fonksiyonu çağırırlar. void deallocate(pointer p, size_type n); Fonksiyonun birinci parametresi boşaltılacak bellek bölgesinin başlangıç adresidir. İkinci parametre boşaltılacak alandaki eleman sayısıdır. Aslında bilindiği gibi tahsis edilen eleman sayısı zaten tahsisat algoritmaları tarafından bir biçimde bilinir. Ancak tahsisat sınıfında esnek davranılmıştır. Yani eleman sayısı tahsisat fonksiyonları tarafından bilinmese de boşaltma gerçekleşebilir. Orijinal allocator sınıfının bu fonksiyonu global operator delete() fonksiyonunu çağırmaktadır. Bu fonksiyonda sadece boşaltma yapar. Yani bitiş fonksiyonunun çağırılmasına yol açmaz. Fonksiyonun ikinci parametresi aslında modern tahsisat algoritmalarında hiç kullanılmaz. Ancak programcı belki kullanılıyordur diye bu parametreyi doğru yazmalıdır. construct() Fonksiyonu allocate() fonksiyonu ile sınıf için yer tahsis edilir fakat başlangıç fonksiyonunun çağırılmasına yol açmaz. Başlangıç fonksiyonu construct() fonksiyonu tarafından çağırılır. void construct(pointer p, const_reference val); Fonksiyonun birinci parametresi başlangıç fonksiyonu çağırılacak nesnenin adresidir. İkinci parametresi başlangıç fonksiyonunda kullanılacak sınıf nesnesini belirtir. Yani aslında val parametresi aynı sınıf türünden olduğuna göre kopya başlangıç fonksiyonunun çağırılmasına yol açmaktadır. allocator() fonksiyonunun orijinali new(p) T(val); değeri ile geri döner. #include <iostream> #include <algorithm> using namespace std; class X { 125 public: X() { cout << "default constructor called\n"; } X(int a, int b) : m_a(a), m_b(b) { cout << "2 parameter constructor called\n"; } X(const X &r) { cout << "copy consturctor called\n"; m_a = r.m_a; m_b = r.m_b; } ~X() { cout << "destructor called\n"; } int m_a, m_b; }; int main() { allocator<X> x; allocator<X>::pointer p; p = x.allocate(1, NULL); x.construct(p, X(10, 20)); cout << p->m_a << '\n' << p->m_b << '\n'; x.deallocate(p, 1); } return 0; destroy() Fonksiyonu Bu fonksiyon bir eleman için bitiş fonksiyonunu çağırır. void destroy(pointer p); Fonksiyon p->~T() işlemini yapar. 126 Örnek Bir Tahsisat Sınıfı /* myallocator.cpp */ #include <iostream> #include <algorithm> #include <vector> using namespace std; template <class T> class myallocator : public allocator<T> { public: pointer allocate(size_type n, allocator<void>::const_pointer hint = 0) { m_val += n * sizeof(T); return allocator<T>::allocate(n, hint); } static int m_val; }; int myallocator<int>::m_val = 0; int main() { vector<int, myallocator<int> > x; x.push_back(10); x.push_back(20); cout << myallocator<int>::m_val << endl; return 0; } Nesne Tutan Sınıflardan Türetme Yapılması Bazen nesne tutan sınıflar doğrudan değil de türetme yapılarak kullanılır. Türetme yapılmasının nedeni işlev genişletme olabileceği gibi başka nedenler de olabilir. Örneğin tipik bir neden adreslerden oluşan bilgilerin tutulduğu veri yapılarında otomatik silme işlemlerinin sağlanmasıdır. Örneğin A bir sınıf olmak üzere A sınıfı türünden adresleri tutan bir bağlı liste olsun. Bilindiği gibi bu tür durumlar çok biçimli uygulamalarda çok sık rastlanmaktadır. Şimdi biz dinamik olarak tahsis edilen alanların adreslerini bağlı listeye yerleştireceğiz. Örneğin: list<A *> x; ... x.push_back(new A()); x.push_back(new A()); Şimdi burada bağlı listesinin faaliyet alanı bittiğinde x için bitiş fonksiyonu çağırılacaktır. Ancak bitiş fonksiyonu yalnızca kendi gösterici olan düğümleri siler. Dinamik tahsis edilmiş olan A 127 nesnelerini silmez. Bu nedenle bu tür durumlarda bağlı listenin faaliyet alanı bitmaden onların gösterdiği alanların silinmesi gerekir. for (list<A *>::iterator iter = x.begin(); iter != x.end(); ++iter) delete *iter; İşte eğer nesne tutan sınıf adresleri tutuyorsa nesne tutan sınıf için çağırılan bitiş fonksiyonu o adreslerin gösterdikleri alanları free hale getirmez. Yalnızca o adresleri tutmakta kullanılan göstericileri free hale getirir. Bu işlemin otomatik yapılmasını sağlamak için nesne tutan sınıftan bir sınıf türetilir ve o sınıf için bitiş fonksiyonu yazılır.Bitiş fonksiyonunda bu göstericilerin gösterdiği yerler boşaltılır, program içerisinde de türetilen sınıf kullanılır. Örneğin: class listptr : public list<A *> { public: ~listptr() { for (iterator iter = begin(); iter != end(); ++iter) delete *iter; } }; int main() { listptr x; x.push_back(new A()); x.push_back(new A()); } Yeni Tür Dönüştürme Operatörleri Bilindiği gibi C++'da C'de kullanılan klasik tür dönüştürme operatörü aynı şekilde kullanılmaktadır. Yine ayrıca tür dönüştürme operatörünün fonksiyonel biçimi denilen biçimi de C++'a özgü olarak kullanılır. Örneğin: (int) a; int (a); //Normal biçim //Fonksiyonel biçim Ancak bunların dışında tamamen konulara ayrılarak uzmanlaştırılmış özel tür dönüştürme operatörleri de vardır. C++'ın yeni tür dönüştürme operatörleri şunlardır: static_cast const_cast reinterpret_cast dynamic_cast Bu operatörlerin kullanım syntaxı şöyledir: operator_ismi<dönüştürülecek tür>(dönüştürülecek ifade) 128 Örneğin: a = static_cast<int>(b); Aslında normal tür dönüştürme operatörü bunların hepsinin yerini tutar. Yeni operatörler belirli konularda uzmanlaştığı için daha güvenli kabul edilmektedir. static_cast Operatörü Bu operatör standart dönüştürmelerde kullanılır, yani: 1- C'nin normal türleri arasında yapılan dönüştürmelerde. Örneğin: long b; int a; a = static_cast<int>(b); 2- Türemiş sınıf türünden adresin taban sınıf göstericisine dönüştürülmesi durumunda. Örneğin: A *pA; B b; pA = static_cast<A *>(&b); 3- Taban sınıf türünden adresin türemiş sınıf türüne dönüştürülmesi durumunda. Örneğin: A *pA; B b; B *pB; pA = static_cast<A *>(&b); pB = static_cast<B *>(pA); const_cast Operatörü Bu operatör const ve/veya volatile özelliklerini bir göstericiden kaldırmak için kullanılır, yani örneğin const int * türünü int * türüne dönüştürmek için bu operatör kullanılmalıdır. Örnek: const int x; int *p; p = const_cast<int *>(&x); Bu operatör const/volatile özelliğini kaldırarak başka bir türe dönüştürme yapamaz. Örneğin: int *pi; const char *pcc; 129 pi = const_cast<int *>(pcc); //error Görüldüğü gibi bu operatörde dönüştürülecek tür dönüştürülecek ifade ile aynı türden olmalıdır. Bu operatörle gerekmese bile const olmayan adresten const adrese dönüştürme yapılabilir. reinterpret_cast Operatörü Bu operatör bir adresi başka türden bir adrese dönüştürmek için ve adreslerle adres olmayan türler arasındaki dönüştürmeler için kullanılır. Örneğin: int *pi; char *pc; ... pc = reinterpret_cast<char *>(pi); pi = reinterpret_cast<int *>(pc); Bu operatör const/volatile özelliklerini kaldırmaz. Bu operatörle taban sınıf türemiş sınıf arasında da dönüştürme yapılabilir. Ancak bu operatör const bir adresten başka türün const olmayan bir adresine dönüşüm yapmaz. Bu işlem aşağıdaki gibi iki aşamada yapılabilir: const char *pcc; int *pi; ... pi = reinterpret_cast<int *>(const_cast<char *>(pcc)); RTTI Özelliği, typeid ve dynamic_cast Operatörleri RTTI (Run Time Type Information) özelliği aslında temel olarak çokbiçimlilik konusunda bir göstericinin ya da referansın gerçekte hangi türemiş sınıfı gösterdiğini tespit etmek için düşünülmüştür. Örneğin bazen türemiş sınıfın adresi bir taban sınıf göstericisine atanır sonra yeniden orijinal türe dönüştürülmek istenir. Ancak programcı çeşirli nedenlerden dolayı orijinal türü tespit edemiyor olabilir. Bir türetme şeması olsun en tepedeki taban sınıfın A sınıfı olduğunu varsayalım. A sınıfı türünden pA isimli bir gösterici tanımlamış olalım, programın çalışma zamanı sırasında pA'ya herhangi bir türemiş sınıf nesnesinin adresi atanmış olsun. Biz bunu bilmiyorsak çalışma zamanı sırasında tespit edebilir miyiz? İşte RTTI konusunun ana noktasını bu oluşturmaktadır. RTTI mekanizması derleyiciye pekçok yük getirdiği için ve çalışabilen kodun verimini düşürebildiği için derleyicilerin pekçoğunda bu özellik isteğe bağlı bir biçimde yüklenebilmektedir. RTTI özelliği VC++ derleyicisinde Project/Settings/C-C++/C++ Language/RTTI kısmından ayarlanabilir ve bu özellik default olarak kapalıdır. RTTI özelliği için type_info isimli bir sınıf kullanılır. Bu sınıf <typeinfo> dosyasında bildirilmiştir. 130 class type_info { public: virtual ~type_info(); bool operator==(const type_info &rhs) const; bool operator!=(const type_info &rhs) const; bool before(const type_info &rhs) const; const char *name() const; }; type_info sınıfı da diğer sınıflarda olduğu gibi std namespace'i içerisinde bildirilmiştir. Sınıfın == ve != operatör fonksiyonu tamamen iki sınıfın aynı türden olup olmadığını kontrol etmek için kullanılır. name() üye fonksiyonu ilgili türün ismini elde etmekte kullanılır. before() fonksiyonu ilgili çokbiçimli sınıfın türetme ağacında daha yukarıda olup olmadığı bilgisini elde etmekte kullanılır. typeid Operatörü typeid RTTI konusunda kullanılan bir operatördür. Kullanımı şöyledir: typeid(ifade) Bu operatör ifadenin türünü tespit eder ve ifadenin türüne uygun const type_info & türünden bir değer üretir. Burada belirtilen ifade herhangi bir tür ismi olabilir (sizeof operatöründe olduğu gibi) ya da normal bir ifade olabilir. İfade çokbiçimli bir sınıf içerisinde bir nesne belirtiyorsa elde edilecek bilgi çalışma zamanı sırasındaki gerçek sınıfa ilişkindir. Örneğin p çokbiçimli bir türetme şeması içerisinde bir gösterici olsun *p typeid operatörüne operand yapılırsa p'nin gösterdiği gerçek sınıfın tür bilgisi elde edilir. Yani B sınıfı A sınıfından türetilmiş olsun aşağıdaki örnekte B sınıfının bilgileri elde edilecektir: A *pA = new B(); typeid(*pA) //Burada B ile ilgili bilgiler elde edilir Ancak derleyiciler bu bilgileri sanal fonksiyon tablolarından elde ettiği için sınıf sisteminin bir çokbiçimlilik özelliğine sahip olması gerekir. Bunu yapmak için en pratik yöntem en taban sınıfa bir sanal bitiş fonksiyonu eklemektir. type_info sınıfının atama operatör fonksiyonları ve başlangıç fonksiyonları sınıfın private bölümüne yerleştirilmiştir, bu yüzden type_info sınıfı türünden bir nesne tanımlanamaz ve typeid operatörünün ürettiği değer başka bir sınıf nesnesine atanamaz. Bu değer doğrudan aşağıdaki gibi kullanılmalıdır: cout << typeid(int).name() << "\n"; typeid operatörüyle derleyicinin yaptığı işlemleri şöyle özetleyebiliriz: 1- Eğer RTTI özelliği etkin hale getirilmişse derleyici bütün çokbiçimli olmayan sınıflar için ve C++'ın doğal türleri için statik düzeyde bir tane type_info sınıfı tahsis eder ve typeid operatörü doğrudan bu sınıf ile geri döner. 131 2- Eğer çokbiçimli bir sınıf sistemi sözkonusuysa derleyici type_info sınıf bilgilerini sınıfların sanal fonksiyon tablolarında tutar böylece p bu sınıf sisteminde bir adres olmak üzere typeid(*p) ifadesi ile p göstericisinin ilişkin olduğu sınıfa ilişkin bilgi değil, onun gerçekte gösterdiği sınıfa ilişkin bilgi elde edilmektedir. Burada önemli bir nokta eğer sınıf sistemi çokbiçimli değilse typeid(*p) ile p'nin gösterdiği yerdeki gerçek sınıfa ilişkin değil p'nin türüne ilişkin değer elde edilir. Örnek: #include <iostream> class A { public: virtual ~A() {} }; class B : public A{ public: virtual ~B() {} }; using namespace std; void main() { A *pA = new B(); cout << typeid(*pA).name() << endl; // class B yazar } type_info sınıfının == ve != operatör fonksiyonları aslında sınıfların isimlerine bakarak bir karşılaştırma yapar. Biz bu operatör fonksiyonlarını çokbiçimli bir yapı içerisinde bir göstericinin içerisindeki adresin gerçekte belirli bir sınıfı gösterip göstermediğini anlamak için kullanırız. Örneğin pA A sınıfı türünden bir gösterici olsun, şimdi biz pA'nın gerçekte B sınıfını gösterip göstermediğini aşağıdaki gibi anlayabiliriz: if (typeid(*pA) == typeid(B)) { //... } Türetme şemasında aşağıdan yukarıya doğru yapılan dönüştürmeler (upcast) normal dönüştürmelerdir. Halbuki yukarıdan aşağıya yapılan dönüştürmeler (downcast) eğer haklı bir gerekçe yoksa güvensiz bir dönüştürmedir. Örneğin türemiş sınıf nesnesinin adresi taban sınıf göstericisine atanabilir ve sonra yeniden türemiş sınıf adresine dönüştürülebilir. Buradaki dönüştürme haklı bir dönüştürmedir. Bilindiği gibi yukarıya ve aşağıya dönüştürme işlemeri aslında static_cast operatörüyle yapılabilir. Ancak static_cast göstericinin gösterdiği yere bakarak bu dönüştürmenin yerinde olup olmadığına bakmaz, halbuki dynamic_cast RTTI özelliğini dikkate alarak eğer dönüştürme yerindeyse bu işlemi yapar. dynamic_cast dönüştürmenin uygun olup olmadığını şöyle belirler: RTTI mekanizmasını kullanarak dönüştürülecek türün göstericinin gösterdiği gerçek tür içerisinde var olup olmadığını araştırır. Eğer dönüştürülecek tür göstericinin gösterdiği yerdeki gerçek bileşik türün parçalarından biriyse 132 dönüştürme yerindedir ve dynamic_cast dönüştürmeyi yapar. Örneğin şöyle bir türetme şeması olsun: A E B C D Şimdi D türünden bir nesne olsun bunun adresini doğrudan dönüştürme yapmadan A türünden bir göstericiye atayabiliriz: D d; A *pA; pA = &d; Şimdi pA'nın B, C ve D türüne dönüştürülmesi uygun ve güvenli işlemlerdir. Ancak E türüne dönüştürülmesi uygun ve güvenli değildir ve gösterici hatasına yol açabilir. Çünkü A, B ve C, D nesnesinin içerisinde vardır ama E nesnesi D'nin içerisinde yoktur. Ancak yine de static_cast operatörüyle pA E türüne dönüştürülebilir. E *pA = static_cast<E *>(pA); Çünkü static_cast işleminde derleyici yalnızca ismi üzerinde static olarak türetme şemasına bakmaktadır. Halbuki dynamic_cast dönüştürülecek türün göstericinin gösterdiği gerçek nesne içerisinde olup olmadığına bakarak dönüştürmeyi yapmaktadır. Tıpkı typeid operatöründe olduğu gibi dynamic_cast operatöründe de doğru işlemlerin yapılabilmesi için türetmenin çokbiçimli olması gerekir (yani tabandaki sınıfın en az bir sanal fonksiyonunun olması gerekir). dynamic_cast dönüştürmeyi yapabilirse dönüştürülmüş adrese, yapamazsa NULL değerine geri döner. Örnek: /* dynamic_cast.cpp */ #include <iostream> using namespace std; struct A { public: virtual ~A() {} }; struct B : A { 133 }; struct C : B { }; struct D : C { }; struct E : A { }; void main() { D d; A *pA; pA = &d; C *pC; pC = dynamic_cast<C *>(pA); if (pC == NULL) cout << "gecersiz donusturme\n"; else cout << "gecerli donusturme\n"; } Referansa Dönüştürme İşlemleri Göstericilerle referanslar aslında tamamen benzer türlerdir. Şimdiye kadar çokbiçimlilik örneklerinin çoğu göstericiler üzerine yapıldı halbuki aynı örnekler referanslar üzerinde de yapılabilir. Normal olarak bir referansa tür dönüştürmesi yapıldığında derleyici dönüştürülecek olan operandın adresini tutmak üzere bir geçici bölge oluşturur, sonra o geçici bölgedeki adres yoluyla nesneye erişimi sağlar. İfadenin sonunda geçici bölge boşaltılmaktadır. Eğer dönüştürülecek operand dönüştürülecek olan referansla aynı türden olmayan bir nesneyse ya da sabit ise o zaman dönüştürülecek referansın const referans olması gerekir, bu durumda iki geçici bölge yaratlılır. Birincisi referansın türünden olan ve operandı tutacak geçici bölge, ikincisi operandın adresini tutacak geçici bölge. Referansa dönüştürme işlemi standart bir dönüştürme işlemidir ve yeni dönüştürme operatörlerinden static_cast ile yapılabilir. Normal türler için referansa dönüştürmenin bir faydası yoktur, ancak bir türetme şeması içerisinde referansa dönüştürmeler tıpkı gösterici dönüştürmeleri gibi etkili bir biçimde kullanılabilir. Yani yukarı ve aşağı dönüştürme işlemleri gösterici yerine referanslarla da yürütülebilir. Örneğin D sınıfı A sınıfının bir türemiş sınıfı olsun, Func() ise D'nin bir üye fonksiyonu olsun, aşağıdaki işlem tamamen geçerlidir: D d; 134 A &r = d; static_cast<D &>(r).Func(); Ancak görüldüğü gibi bu tür işlemlerde referans kullamak iyi bir görüntü vermemektedir, tabii bazen zorunlu olabilir. Örneğin başkası tarafından yazılan fonksiyonun parametresi A türünden referans olsun, ama biz geçirilen türün D türünden olduğunu bilelim. D türüne aşağıya doğru dönüştürme uygulamak için iki şey yapılabilir: 1- Göstericiye dönülerek göstericiyle çalışılır. 2- Referansla işlemlere devam edilir. Aslında bu iki işlem içsel olarak neredeyse eşdeğerdir. void Sample(A &ra) { // 1. yontem D *pD = static_cast<D *>(&ra); } // 2. yontem D &rD = static_cast<D &>(ra); İkili Ağaç Yapıları İkili ağaç (binary tree) fiziksel olarak sıralı olmayan elemanların mantıksal bir biçimde sıralı gözükmesi için oluşturulmuş olan, logaritmik aramalara izin veren en önemli ağaç yapılarındandır. İkili ağaçta her düğümün iki göstericisi vardır, göstericilerden biri o düğümden küçük olan elemanı, diğeri ise büyük olan elemanı gösterir. İkili ağaç tipik olarak aşağıdaki gibi bir yapıyla ifade edilebilir: template <class T> struct BNODE { T val; BNODE *pLeft; BNODE *pRight; }; İkili ağaca yeni bir eleman eleneceği zaman önce eklenecek yer tepe düğümünden hareketle sola ve sağa gidilerek bulunur. Sonra sol ya da sağ düğüm üzerinde güncelleme yapılarak eleman eklenir. Örneğin aşağıdaki sayıların teker teker buraya ekleneceğini düşünelim: 8 21 7 16 44 3 17 9 28 33 135 8 7 21 3 16 9 44 17 28 33 İkili ağaçta en tepeden en uzun dala kadar olan eleman sayısına ağacın yüksekliği denir. Farklı dallarda ağacın yüksekliği aynı değilse bu tür ikili ağaçlara dengelenmemiş ikili ağaçlar denir. Son kademe hariç tüm dalların yüksekliği aynıysa böyle ağaçlara dengelenmiş ikili ağaç (balanced binary tree) denir. Eğer son kademede de hiç fazla eleman yoksa ağaç tam dengelenmiştir (complete binary tree). Dengelenmiş bir ağaçta tamamen ikili arama performansına sahiptir, yani en kötü arama sayısı log 2 n 'dir. Yine dengelenmiş ikili ağaca en kötü olasılıkla logaritmik olarak eleman eklenebilir. Bir dengelenmiş ikili ağaç basit bir kendi kendini çağıran fonksiyonla sıralı olarak dolaşılabilir (binary tree traversing), böylelikle ikili ağaçlar sıralı bir dizi görüntüsü de verebilir. İkili Ağaçlarla Bağlı Listelerin Karşılaştırılması 1- Eleman ekleme bakımından bağlı listeler daha iyidir. Çünkü bağlı listelere eleman ekleme sabit zamanlı bir işlemdir. Halbuki ikili ağaca eleman logaritmik karmaşıklıkta eklenir. 2- Çift bağlı listelerle ikili ağaç hemen hemen aynı büyüklükte bellek kullanır. Ancak tek bağlı listeler daha az bellek kullanmaktadır. 3- İkili ağaç arama işlemlerinde bağlı listelerden çok daha iyidir. Dengelenmiş ikili ağaçlarda başarısız aramalarda karmaşıklık log 2 n biçimindedir. Halbuki bağlı listelerde doğrusal arama söz konusudur. 4- Sıraya dizme konusunda her zaman ikili ağaç bağlı listeye göre çok daha iyidir. Çünkü basit bir kendi kendini çağıran fonksiyonla ikili ağaç küçükten büyüğe ya da büyükten küçüğe dolaşılabilir. Yukarıdaki açıklamalar eşliğinde şunlar söylenebilir: Eğer çok eleman ekleyip az arama işlemi yapılıyorsa bağlı liste, az eleman ekleyip çok arama yapılıyorsa ikili ağaç tercih edilmelidir. 136 map ve multimap Sınıfları STL'de ikili ağaç oluşturan tipik sınıflar map, multimap ve set, multiset sınıflarıdır. map ve set sınıfları birbirine çok benzer, aralarında küçük farklılıklar vardır (Bu sınıfların dengelenmiş ikili ağaç yöntemi ile oluşturulması zorunlu tutulmamışsa da genel işleyiş mekanizmaları için en uygun veri yapısı dengelenmiş ikili ağaçtır). multimap sınıfının map sınıfından tek farkı aynı elemanın eklenmesine izin vermesidir. map ve multimap yapılarında düğümler pair çiftlerini tutar. Anımsanacağı gibi pair yapısı first ve second isimli iki elemana sahiptir. Bu sınıflar anahtar olarak first bilgisini kullanırlar. Yani ağacı first elemanına göre oluştururlar. Tipik olarak arama işlemi first elemanına göre yapılır. Örneğin first bir kişinin numarası, second ise kimlik bilgileri olabilir. Arama numaraya göre yapılır. Bir kişinin kimlik bilgileri elde edilir. Bu sınıflar aşağıdaki gibi template parametrelerine sahiptir: template <class Key, class T, class Compare = less<Key>, class Allocator = allocator <pair<const Key, T> > > class map{ //... }; class multimap { //... }; Görüldüğü gibi sınıfın en az iki template parametresi belirtilmek zorundadır. Birinci parametre anahtar olarak kullanılacak türü (yani pair yapısının first türünü), ikinci parametre ise tutulacak bilgiyi (yani pair yapısının second elemanını göstermektedir). Örneğin bir kişinin numarasına göre ismini arayabileceğimiz bir map nesnesi şöyle tanımlanır: map<int, string> x; Sınıfın üçüncü template parametresi anahtar bilgiler ağaca yerleştirilirken hangi operatör ile karşılaştırma yapılacağını belirtir. Burada default olarak less binary predicate'i kullanılmıştır. Yani karşılaştırma sırasında default olarak < operatörü kullanılır. Bu durumun iki önemli sonucu vardır: 1- Anahtar bilgi bir sınıf ise, sınıfın küçüktür operatör fonksiyonu olmalıdır. 2- iterator yöntemi ile dolaşım yapılırsa küçükten büyüğe bir sıra görünür. Büyükten küçüğe görünüm elde etmek için reverse_iterator kullanılabilir ya da buradaki predicate sınıf greater olarak alınabilir. Sınıfın son template parametresi her sınıfta olduğu gibi allocator sınıfını belirtmektedir. 137 map ve multimap Sınıfının Üye Fonksiyonları Sınıfın default başlangıç fonksiyonu ve iki iterator arasındaki elemanlardan map yapan başlangıç fonksiyonları vardır. Ayrıca karşılaştırma fonksiyonu içeren bir başlangıç fonksiyonu da içermektedir. Örneğin: map<key, elem> map<key, elem> map<key, elem> x; y(v.begin(), v.end()); z(Comp); Yine sınıfın klasik olarak size() üye fonksiyonu eleman sayısına geri döner, empty() üye fonksiyonu boş mu diye bakar, clear() üye fonksiyonu tüm elemanları siler, erase() üye fonksiyonu iki iterator aralığını siler. Şüphesiz sınıfın önemli fonksiyonları eleman insert eden ve arayan fonksiyonlardır. map ve multimap Sınıflarına Eleman Insert Edilmesi Sınıfın insert() üye fonksiyonu elemanı ağaçtaki uygun yere insert eder. Fonksiyonun prototipi şöyledir: pair<iterator, bool> insert(const pair<key, elem> &r); Görüldüğü gibi insert() fonksiyonu bir pair yapısı alarak insert işlemini yapar. Fonksiyon map sınıfı söz konusuysa daha önce aynı elemandan varsa başarısız olabilir. Ancak multimap sınıfının insert() fonksiyonu başarısız olmaz. multimap sınıfının insert() fonksiyonunun prototipi şöyledir: iterator insert(const pair<key, elem> &r); Her iki sınıfın insert() fonksiyonunun geri dönüş değerindeki iterator yeni eklenen elemana ilişkin iterator değeridir. Örneğin map sınıfına aşağıdaki gibi eleman insert edilebilir. x.insert(pair<int, string>(10, "kaan")); x.insert(make_pair(10, string("ali")); Birinci insert işleminde geçici bir pair nesnesi oluşturarak pair yapısı elde edilmiştir. İkinci insert işleminde eklenecek pair make_pair() fonksiyonu ile elde edilmiştir. make_pair() fonksiyonu tamamen kolay bir pair nesnesi yaratmak için kullanılır. make_pair() fonksiyonu şöyle yazılmıştır: template <class A, class B> pair<A, B> make_pair(const A &a, const B &b) { return pair<A, B>(a, b); } 138 Görüldüğü gibi make_pair() fonksiyonunu kullanmanın tek avantajı template parametrelerini belirtmemektir. İşlemin başarısı kontrol edilebilir. Bunun için geri dönüş değerinin second elemanına bakmak gerekir. Bu işlem biraz karışık gibi görülebilir. if (x.insert(make_pair(15, string("veli"))).second) { //... } map ve multimap sınıfının iteratorleri bidirectional iterator'dür. Yani ileri ve geri yönde hareket edebilir. map ve multimap sınıfları pair yapılarından oluştuğu için iter bu sınıfın bir iterator'ü olmak üzere *iter de pair sınıfı türündendir. Bu durumda elemanların yazdırılması aşağıdaki gibi yapılmalıdır. map<int, string> x; //... //... map<int, string>::iterator iter; for (iter = x.begin(); iter != x.end(); ++iter) cout << (*iter).first << '\t' << (*iter).second); Örnek: /* map.cpp */ #pragma warning(disable:4786) #include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> x; map<int, string>::iterator iter; x.insert(make_pair(1, string("volkan"))); x.insert(make_pair(5, string("kaan"))); x.insert(make_pair(3, string("fatih"))); x.insert(make_pair(10, string("karga"))); x.insert(make_pair(7, string("murat"))); x.insert(make_pair(8, string("arda"))); for (iter = x.begin(); iter != x.end(); ++iter) cout << (*iter).first << '\t' << (*iter).second << endl; } return 0; 139 Insert işlemi en kolay [] operatörü ile yapılabilir. [] operatör fonksiyonunun genel yapısı şöyledir: elem &operator[](const key &r); Fonksiyon index değeri olarak pair yapısının first türünü alır ve ağaçtaki yerleşim yerini bularak eğer belirtilen elemandan yoksa yeni bir eleman insert eder ve o elemana ilişkin second değerinin referansına geri döner. Eğer index olarak verilen eleman ağaçta varsa yeni bir eleman insert etmez doğrudan ağaçta olan elemanın second değerinin referansına geri döner. Bu durumda tipik olarak [] operatörüyle insert şöyle yapılabilir: map<int, string> x; x[100] = "ali"; x[300] = "veli"; x[50] = "sacit"; //... /* map2.cpp */ #pragma warning(disable:4786) #include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> x; map<int, string>::iterator iter; x[100] = "volkan"; x[50] = "kaan"; x[57] = "falan"; x[54] = "filen"; for (iter = x.begin(); iter != x.end(); ++iter) cout << (*iter).first << '\t' << (*iter).second << endl; } return 0; map Sınıflarında Arama İşlemleri map ve set sınıfları dengelenmiş ikili ağacı kullanarak algoritmik arama yapmakta kullanılır. Arama yapmakta kullanılan üç temel fonksiyon vardır. find(key); lower_bound(key); 140 upper_bound(key); find() fonksiyonu bir key değerini parametre olarak alır ve ağaçta o değerin aynısından var mı diye bakar. Arama başarılıysa bulunan elemanın iterator değerine, başarısız ise end iterator değerine geri döner. Örneğin /* map3.cpp */ #pragma warning(disable:4786) #include <iostream> #include <string> #include <map> using namespace std; int main() { map<int, string> x; map<int, string>::iterator iter; x.insert(make_pair(10, x.insert(make_pair(20, x.insert(make_pair(30, x.insert(make_pair(40, string("ali"))); string("volkan"))); string("baris"))); string("emine"))); iter = x.find(30); if (iter == x.end()) cout << "bulunamadı\n"; else cout << (*iter).first << '\t' << (*iter).second << endl; return 0; } find() fonksiyonu tam uyan elemanı bulmakta kullanılır. Halbuki bazen buna en yakın küçük ya da büyük elemanı bulmak pek çok bakımdan gerekli olabilir. lower_bound() fonksiyonu key <= elem olan ilk elemanı bulur. Bu fonksiyon multimap söz konusu olduğunda aranan elemandan birden fazla olduğunda aranan elemanların ilkini bulacaktır. Örneğin aşağıdaki dizide 8’i arayacak olalım. lower_bound(8) 3 8 8 8 9 10 Aşağıdaki dizide 5’i arayacak olalım. 2 4 6 9 10 lower_bound(5) 141 upper_bound() fonksiyonu ilk key < elem koşulunu sağlayan düğümü bulur. Örneğin aşağıdaki dizide bu fonksiyon ile 8’i arayacak olalım. upper_bound(8) 3 8 8 8 9 10 Aşağıdaki dizide 5 aranacak olsun. 2 4 6 9 10 lower_bound(5) Görüldüğü gibi aranan eleman bulunamaz ise lower_bound() fonksiyonu ile upper_bound() fonksiyonu arasından bir fark olmaz. Ya da örneğin aranan elmandan bir tane varsa lower_bound() bulunan elemanın iterator değerini, upper_bound() bulunandan bir sonrakinin iterator değerini verir. Özetle elemanın bulunması durumunda lower_bound() öne, upper_bound() arkaya insert için iterator verir. /* map4.cpp */ #pragma warning(disable:4786) #include #include #include #include <iostream> <string> <map> <vector> using namespace std; int main() { vector<pair<int, string> > v; multimap<int, string> x; multimap<int, string>::iterator iter; x.insert(make_pair(100, string("ali"))); x.insert(make_pair(10, string("volkan"))); x.insert(make_pair(10, string("baris"))); x.insert(make_pair(8, string("emine"))); iter = x.lower_bound(10); copy(iter, x.upper_bound(10), back_inserter(v)); vector<pair<int, string> >::iterator iter2; 142 for (iter2 = v.begin(); iter2 != v.end(); ++iter2) cout << (*iter2).second; return 0; } map sınıfından silme yapmak için erase() fonksiyonlarının çeşitli versiyonları vardır. fonksiyonu kullanılabilir. erase() erase(elem); erase(first, last); Örneğin: #pragma warning(disable:4786) #include <iostream> #include <string> #include <map> using namespace std; int main() { multimap<int, string> x; multimap<int, string>::iterator iter; x.insert(make_pair(100, string("ali"))); x.insert(make_pair(10, string("volkan"))); x.insert(make_pair(10, string("baris"))); x.insert(make_pair(8, string("emine"))); x.erase(x.lower_bound(10), x.upper_bound(10)); for (iter = x.lower_bound(10); iter != x.upper_bound(10); ++iter) cout << (*iter).second << endl; return 0; } set ve multiset Sınıfları set ve multiset sınıfları tamamen map ve multimap sınıfları gibi çalışır. Yani bu sınıflar da dengelenmiş ikili ağaç kurarlar. map ve multimap elemanları pair biçiminde tutarken, bu sınıflar tekil biçimde tutarlar. Dolayısıyla bir anahtar alanları yoktur, tabii yeni elemanın ağaçtaki yere yerleşebilmesi için bir karşılaştırma fonksiyonuna gereksinim vardır. Sınıfın template bildirimi şöyledir: template <class T, class Compare = less<T>, class Allocator = allocator<T> > class set { //... 143 }; Görüldüğü gibi nesneyi tanımlarken en az bir türü belirtmek gerekir. Ağaçtaki yer default olarak < operatörü ile bulunur. Ağaçta tutulan bilgi int, float gibi basit bir bilgi ise set map’ten daha kullanışlıdır. Eğer ağaçta tutulacak bilgi yapı ya da sınıf ise, bu yapıya da sınıfın < operatör fonksiyonunu default olarak yazmak gerekir. [] operatör fonksiyonu sadece map sınıfı için tanımlıdır. set, multiset ve multimap sınıfları için tanımlı değildir. find(), erase(), insert(), lower_bound() ve upper_bound() fonksiyonları tamamen map ve multimap sınıflarındaki gibidir. Ancak anahtar bir tür ile çalışmazlar, sınıfta tutulan tür ile çalışırlar. Bu sınıflar <set> başlık dosyasındadır. /* set1.cpp */ #pragma warning(disable:4786) #include <iostream> #include <cstdlib> #include <set> using namespace std; int main() { set<int> x; for (int i = 0; i < 100; ++i) x.insert(rand() % 1000); copy(x.begin(), x.end(), ostream_iterator<int>(cout, "\n")); cout << endl << "Toplam eleman = " << x.size() << endl; } return 0; /* set2.cpp */ #pragma warning(disable:4786) #include <iostream> #include <string> #include <set> using namespace std; class Person { public: Person(const char *pName, int no):m_name(pName), m_no(no){} friend ostream &operator<<(ostream &r, const Person &per); bool operator<(const Person &per) const { return m_no < per.m_no; 144 } private: string m_name; int m_no; }; namespace std { // for visual C++ ostream &operator<<(ostream &r, const Person &per) { r << per.m_name << '\t' << per.m_no; return r; } } // off namespace int main() { set<Person> x; x.insert(Person("ali", 10)); x.insert(Person("volkan", 20)); x.insert(Person("veli", 30)); copy(x.begin(), x.end(), ostream_iterator<Person>(cout, "\n")); cout << endl << "Toplam eleman = " << x.size() << endl; return 0; } -> Operatör Fonksiyonunun Yazımı ve Smart Göstericiler -> operatörü sınıfın üye fonksiyonu olarak yazılmak zorundadır. Binary bir operator olmasına karşın sanki unary bir operatörmüş gibi ele alınıp yazılır. Böylesi bir tasarım smart göstericilerin kullanılmasına olanak verdiği için daha faydalı bulunmuştur. a bir sınıf nesnesi olmak üzere a-> işleminin eş değeri a.operator->()-> biçimindedir. Bu durumda a->b nin eş değeri a.operator->()->b biçimindedir. -> operatör fonksiyonunun geri dönüş değerinin bir sınıf ya da yapı adresi olması gerekir. Böylelikle a bir sınıf nesnesi olmak üzere a-> işleminden bir yapı ya da sınıf adresi elde edilmeli, b de elde edilen bu yapı ya da sınıfın elemanı olmalıdır. İşte bir sınıfta -> operatör fonksiyonunu yazarsak o sınıf türünden nesne sanki başka bir sınıf türünden göstericiymiş gibi davranır. Zaten smart gösterici demek gösterici gibi kullanılan sınıf nesnesi demektir. Smart göstericiler sayesinde referans sayma işlemleri gibi işlemler, güvenli gösterici kullanma mümkün hale gelebilir. Smart gösterici sistemlerinde bir asıl sınıf vardır, bir de -> operatör fonksiyonu yazılmış asıl sınıf türünden gösterici gibi davranan sınıf vardır. Hatta bazen asıl sınıf tamamen gizlenebilir, asıl sınıfa tamamen smart göstericiler ile erişilebilir. 145 Genel amaçlı template bir smart gösterici sınıfı aşağıdakine benzer tanımlanabilir: template<class T> class Smartptr { Public: Smartptr(T *p):m_pT(p){} ~Smartptr() { delete m_pT; } T operator*() { return *m_pT; } T *operator->() { return m_pT; } private: T *m_pT; }; class X { public: X(int a):m_a(a) {} void Disp() { cout << m_a << endl; } Private: int m_a; }; Smartptr<X> p(new X(10)); p->Disp(); // p.operator->()->Disp(); auto_ptr Sınıfı auto_ptr sınıfı smart gösterici gibi davranan bir STL sınıfıdır. Ancak sahipliğini devredebilir. Bu sınıf özellikle başlangıç fonksiyonlarında oluşan throw işlemleri için kullanılmaktadır. Bilindiği gibi başlangıç fonksiyonunda throw oluşursa o zamana kadar başlangıç fonksiyonu tamamen bitirilmiş başlangıç fonksiyonları için bitiş fonksiyonu çağırılır. Başlangıç fonksiyonu tam olarak bitirilmemiş sınıflar için ve dinamik tahsis edilmiş sınıf nesneleri için bitiş fonksiyonu çağırılmaz. Örneğin A bir sınıf olsun X::X() { m_p = new A(); -> throw işlemi yapıldı } Burada throw işlemi oluştuğunda X için bitiş fonksiyonu çağırılmayacağı gibi A için de çağırılmaz. Çünkü A dinamik olarak tahsis edilmiştir. Başka bir gösterim şöyle olabilir: X::X(): m_pA(new A()), m_a(10) { //... } 146 Burada m_a’nın içerisinde throw oluşmuşsa hiç bir sınıf için bitiş fonksiyonu çağırılmaz. İşte auto_ptr bu tür durumlarda otomatik bitiş fonksiyonu çağırılsın diye kullanılan smart gösterici sınıfıdır. auto_ptr sınıfı bir default başlangıç fonksiyonu bir de template parametreli gösterici olan başlangıç fonksiyonuna sahiptir. Örneğin: auto_ptr<int> x; auto_ptr<int> y(new int); Normal olarak gösterici parametreli başlangıç fonksiyonunda dinamik tahsis edilen alanın başlangıç fonksiyonu olmalıdır. Sınıfın hem atama operator fonksiyonu hem de kopya başlangıç fonksiyonu vardır. Her iki fonksiyon da sahipliği bırakmak için içerisinde tutukları göstericileri atadıktan sonra kendi gösterici elemanlarını NULL değerine çekerler. Böylece bir t anında yalnızca tek bir sınıf tahsis edilen alanı gösterir hale gelir. Bu durumda klasik olarak a = b işleminde b değeri de atamadan etkilenmektedir. Örneğin: /* auto_ptr.cpp */ #include <memory> #include <iostream> class A { public: A(int a) : m_a(a){} ~A(){} void Disp() { std::cout << m_a << std::endl; } private: int m_a; }; using namespace std; void Func(auto_ptr<A> x) { x->Disp(); } void main(void) { auto_ptr<A> a(new A(10)); auto_ptr<A> b; a->Disp(); b = a; b->Disp(); Func(b); } auto_ptr <memory> başlık dosyasının içerisindedir. 147 auto_ptr Sınıfının Yararlı Kullanımları Bu sınıf bir smart gösterici sınıfı olarak kullanılabilir. Ancak bu sınıfın asıl amacı sınıfın veri elemanı olan göstericileri nesne yapmaktır. Bazı programcılara göre sınıf gösterici veri elemanı içerecekse bu göstericiler nesne gibi tanımlanmalı, yani auto_ptr kullanılarak smart gösterici biçimine sokulmalıdır. Böylece bellek sızıntısı riski en aza indirilebilecektir. Ancak bu yaklaşım bazen abartılı olabilir. En iyisi bunu programcıya bırakmaktır. Örnek kullanım: #include <iostream> #include <memory> using namespace std; class B { public: B(int b) { m_b = b; throw m_b; } private: int m_b; }; class A { public: A(int a, int b):m_pi(new int(a)), m_pB(new B(b)) {} private: auto_ptr<int> m_pi; auto_ptr<B> m_pB; }; int main() { try { A a(10, 20); } catch(...) { cout << "exception...\n"; } return 0; } Sınıfın * ve -> operator fonksiyonları elemana erişimde kullanılır. Doğal türlere ilişkin sınıflarda *, sınıflara ilişkin auto_ptr nesnelerinde -> operatörü kullanılmalıdır. #include <iostream> #include <memory> using namespace std; class B { public: 148 B(int b) { m_b = b; } void Disp() { cout << m_b << endl; } private: int m_b; }; class A { public: A(int a, int b):m_pi(new int(a)), m_pB(new B(b)) {} void Func() throw() { cout << *m_pi << endl; m_pB->Disp(); } private: auto_ptr<int> m_pi; auto_ptr<B> m_pB; }; int main() { try { A a(10, 20); a.Func(); } catch(...) { cout << "exception...\n"; } return 0; } auto_ptr sınıfının release() ve reset() isimli iki üye fonksiyonu vardır. release() sahipliği bırakır, yani geri dönüş değeri olarak göstericiyi verir ve gösterici veri elemanına NULL atar. reset() eskisini delete ederek yeni bir dinamik nesne tutulmasına yol açar. Çoklu Türetme Bir sınıfın birden fazla taban sınıfı olması durumuna çoklu türetme (multiple inheritance) denir. Çoklu türetme türetilmiş nesnelerin içsel organizasyonu standart olarak belirlenmemiştir. Ancak derleyicilerin çoğu sol kolun tepesinden aşağıda soldan sağa elemanları ardışık dizerler. Örneğin: A B C 149 C c; -> A B C Farklı kollarda aynı isimli fonksiyonlar bulunabilir. Ancak bu fonksiyonların sınıf ismi ve çözünürlük operatörü olmadan kullanılmaları error oluşturur. Örneğin: #include <iostream> #include <memory> using namespace std; class A { public: void Func() { cout << "I am A::Func\n"; } }; class B { public: void Func() { cout << "I am B::Func"; } }; class C : public A, public B { }; int main() { C c; // c.Func(); Ambiguous mistake c.A::Func(); return 0; } Farklı kollardaki aynı isimli fonksiyonlar arasında overloading işlemi yapılmaz. Çünkü farklı parametreli aynı isimli fonksiyonların bulunması aynı faaliyet alanına özgüdür. Örneğin A sınıfının int parametreli bir Func() fonksiyonu, B sınıfının parametresiz Func() fonksiyonu olsun c.Func() işlemi error’dür. 150 Çoklu Türetilmiş Sınıflarda Türemiş Sınıf Taban Sınıf Dönüştürmeleri Çoklu türetilmiş bir sınıf nesnesinin adresini her taban sınıfına ilişkin bir göstericiye doğrudan atayabiliriz. Şüphesiz bu durumda çoklu türetilmiş sınıfın ilgili taban sınıf verilerinin bulunduğu bloğun adresi elde edilecektir. Örneğin: C c; B *pB; A *pA; pB = &c; pA = &c; ... A p B p C Görüldüğü gibi dönüştürme sonunda adresin sayısal bileşeni değişebilmektedir. Daha sonra ters bir dönüşümün yapılması, yani adresin eski haline getirilmesi mümkün olmayabilir. Çoklu Türetme Sınıflarında Sanal Fonksiyonların Kullanılması En karışık noktalardan birisi taban sınıfların sanal fonksiyona sahip olduğu durumda çoklu türetme uygulanmış olması durumudur. Bu durumda sanal fonksiyona sahip sınıf sayısı kadar türemiş sınıf içerisinde farklı sanal fonksiyon tablosu bulunması gerekir. Bu tür durumlarda çoklu türetilmiş sınıfın adresini taban sınıflardan birine ilişkin göstericiye aktarıp o gösterici yoluyla o sınıfın sanal fonksiyonunu çağırdığımızda derleyici o göstericinin gösterdiği yerde sanal fonksiyon tablosunu arayacaktır. Bu tasarım ancak çoklu türetilmiş sınıfta birden fazla sanal fonksiyon tablosunun ve sanal fonksiyon tablo göstericisinin bulunmasıyla sağlanır. Örneğin: class A { public: virtual void FuncA() {} }; class B { public: virtual void FuncB() {} }; class C : public A, public B { public: void FuncA() {} void FuncB() {} virtual void FuncC() {} }; 151 C sınıfı türünden bir nesne tanımlandığında sanal fonksiyon tabloları şöyle organize edilecektir: A //C_A’nın sanal fonksiyon tablosu &A::FuncA() () //C_B’nın sanal fonksiyon tablosu &B::FuncB() B C Şimdi C sınıfı türünden nesnenin adresi B sınıfı türünden bir göstericiye atanarak sanal fonksiyon çağırılsın: C c; B *pB; pB = &c; pB->FuncB(); Derleyici sanal fonksiyonu pB adresinden hareketle arayacaktır. Görüldüğü gibi C’nin sanal fonksiyon tablosu iki parçadan oluşmaktadır. C’nin kendi sanal fonksiyonları için ayrı bir sanal fonksiyon tablosuna gerek yoktur. Derleyici bunu C_A’nın altına yerleştirebilir. Çoklu türetmede taban sınıflardaki bir sanal fonksiyonun aşağıya doğru tek bir sonlanan fonksiyonu olmalıdır. Örneğin aşağıdaki gibi bir türetme şeması söz konusu olsun. A sınıfının Func() isimli bir sanal fonksiyonunun olduğunu düşünelim. Bu fonksiyon hem B’de hem E’de yeniden yazılmışsa bu durum error oluşturur. Yalnızca E sınıfında yazılmışsa ya da yalnızca F sınıfında yazılmışsa bu durum geçerlidir. A B C D E F Çoklu türetmelerde taban sınıfın bazen iki kopyası bulunur. Bu durum çoğu kez istenmeyen bir durumdur. Örneğin: 152 class A { //... }; class B : public A { //... }; class C : public A { //... }; A A B C D class D : public B , public C { //... }; Burada D sınıfından bir nesne tanımlarsak aşağıdaki gibi bir şekil oluşur: A B A C D Görüldüğü gibi A’dan iki tane bulunmaktadır. Halbuki bir tane bulunması daha istenen bir durumdur. Burada XA, A sınıfının bir elemanı olsun. D.XA erişimi geçersizdir. Çünkü hangi A sınıfının elemanına erişildiği belli değildir. Erişim şöyle yapılmalıdır: d.B::XA d.C::XA Halbuki C++’ın standart iostream sınıf sisteminde buradaki taban sınıftan bir tane bulunur. ios, istream, ostream, iostream. ios istream ostream iostream Burada istream üzerinde işlem yapıldığında ve bu işlemler ios elemanlarını değiştirdiğinde ostream bu değişiklikleri görür. Bu tür durumlarda taban sınıfı tek yapmak için taban sınıfı sanal tanımlamak gerekir. Derleyiciler sanal taban sınıf tanımlarını birleştirerek şemada bunu tek sınıf biçiminde ifade ederler. Yapılan işlem şöyle anlaşılabilir: Şema sanki sanal taban sınıf yokmuş gibi çizilir. Sonra sanal olarak belirtilmiş taban sınıflar birleştirilir. Örneğin: 153 class A { //... }; A class B : virtual public A { //... }; B class C : virtual public A { //... }; C D class D : public B , public C { //... }; Taban sınıflardan biri sanal bildirilmişse, diğeri normal bildirilmişse birleştirme yapılmaz. Örneğin: A A B C E F deque Sınıfı deque sınıfı tamamen vector sınıfı gibidir, ancak vector sınıfı içsel olarak tek bir dizi biçiminde tasarlanmasına karşın deque bloklu bağlı liste tekniği ile tasarlanır. ..... ..... ..... vector ile deque arasında şu benzer ve ayrılıklar vardır. 1- Her iki sınıfta da iterator’ler random access’dir. Yani her iki sınıfın da [] operatör fonksiyonu vardır. Kuşkusuz vector’ün [] operatörü deque’in [] operatöründen daha hızlı çalışacaktır. deque yapısında elemana erişme “amortized constant time” karmaşıklıktadır. 154 2- vector sınıfında push_front() ve pop_front() fonksiyonları yoktur. Çünkü bu fonksiyonlar olsaydı koskoca bir vector’ün kaydırılması gerekirdi ki bu fonksiyonların konulmamasının daha anlamlı olduğu düşünülmüştür. Ancak deque’in bloklu yapısı nedeniyle öne yapılan eklemeler ve silmeler yalnızca tek bir bloğu etkilemektedir. Dolayısıyla bu fonksiytonlar etkin çalışabilir. 3- vector için tahsis edilen alan asla küçültülemez. Yani capacity değeri ancak büyür. vector resize edilse bile yalnızca son elemanının nerede olduğu belirlemesi yapılmaktadır. Halbuki deque’in bloklu yapısı nedeniyle otomatik kapasite küçültmesi yapılabildiği gibi resize() fonksiyonu da capacity değerini düşürebilmektedir. Ne Zaman vector, Ne Zaman deque Kullanılmalıdır? Random access iterator gereken durumlarda vector ya da deque akla gelmelidir. Eğer ekleme yalnızca sona yapılacaksa tipik olarak vector tercih edilmelidir. Ancak ekleme ya da silme hem başa hem de sona yapılacaksa bu durumda deque tercih edilmelidir. deque’in kullandığı toplam bellek vector’den az olma eğilimindedir. stack adaptör sınıfı da default olarak deque sınıfı kullanılarak yapılmıştır. deque veri yapısı <deque> başlık dosyasında bulunmaktadır. queue Sınıfı queue tipik olarak FIFO kuyruk sistemidir. Bilindiği gibi gibi LIFO sistemlerine stack denir. queue sınıfı bir adaptör sınıftır ve <queue> başlık dosyasında bulunur. Bildirimi şöyledir. template <class T, class Container = deque<T> > class queue { //... }; Görüldüğü gibi queue sınıfı da default olarak deque sınıfı kullanılarak yapılmıştır. Tipik olarak queue sınıfının push() ve pop() fonksiyonları vardır. push() kuyruğa eleman ekler, pop() sıradaki elemanı kuyruktan siler. Eleman push() ile başa eklenir, front() fonksiyonu ile sıradaki eleman alınır, pop() fonksiyonu ile de silinir. #include <iostream> #include <queue> using namespace std; int main() { queue<int> x; for (int i = 0; i < 10; ++i) x.push(i); for (i = 0; i < 10; ++i) { cout << x.front() << endl; 155 } x.pop(); return 0; } C ile C++ Arasındaki Uyumsuzluklar C++, C’nin bir üst kümesidir. Pek çok durumda *.c uzantılı bir dosya C++ derleyicisi tarafından başarılı olarak derlenir. Ancak bazı küçük uyumsuzluklar da bulunmaktadır. 1- C++’daki // yorumlama biçimi 99 standartlarında C’ye de eklenmiştir. 2- C++ ile eklenen bazı anahtar sözcükler bir C programında kullanılmışsa problem çıkar. 3- C’de sizeof(‘a’) == sizeof(int)’tir, ancak C++’da sizeof(‘a’) == sizeof(char)’dır. 4- Stringler C’de ve 96 öncesinde C++’da char * türündendi. Ancak C++’da şimdilik string’in char * türüne atanması kabul edilse de depricated yapılmıştır, gerçek türü const char *’dır (bir fonksiyon string ile çağırılmışsa hem char * parametreli hem de const char * parametreli tür varsa, const char * olan seçilir). 5- C’de yapı içerisinde bir yapı bildirildiğinde içteki yapı tamamen dışarıda yapılmış gibi kabul edilir. Ancak C++’a göre içteki yapı dıştaki yapının faaliyet alanı dışındadır. 6- Global const değişkenler C++’da (genel olarak const nesneler sabit ifadesi belirttiği için) static kabul edilir. Yani o modüle özgüdür. Bu durum onların başlık dosyasına yerleştirilmesini mümkün hale getirmiştir. C’de böyle bir durum söz konusu değildir. 7- C++’da main anahtar sözcüktür. Kendi kendini çağıramaz ama C’de anahtar sözcük değildir ve recursive olarak kullanılabilir. 8- C’de void bir adresin tür dönüştürmesi yapılmadan her hangi bir türe atanması tamamen normal kabul edilmiştir. Ancak bu durum araya bir void gösterici sokarak farklı türler arasındaki göstericilerin birbirine atanması mümkün olmuştur. C++’da bu durum tamamen error olarak değerlendirilmiştir. C’de de bu durumda tür dönüştürmesi uygulanması tavsiye edilmektedir. 9- C’de const bir değişkenin adresi void türden göstericiye doğrudan atanabilir. Ancak C++’da bu durum yasaklanmıştır, göstericinin const void olması gerekmektedir. 10- C++’da fonksiyon parametre parantezinin içinin boş bırakılması C’deki gibi parametresi türce ya da sayıca kontrol etme anlamına gelmez. Boş bırakmakta void yazmak aynı anlamdadır. 11- C++’da fonksiyonun geri dönüş değeri void değilse kesinlikle return yazılmak zorundadır. Halbuki C’de bu en fazla bir uyarıdır. 12- C++’da yapı ya da sınıf türünden nesne tanımlarken struct ya da class anahtar sözcükleri kullanılmayabilir. C++’da bu nedenle C’de geçerli olan aşağıdaki gibi bir ifade geçerli değildir. typedef struct X { //... } X; struct X a; 156 int X; 13- C++’da const değişkenler ilk değer verilerek tanımlanmak zorundadır. C’de bu zorunlu değildir. 14- C++’da fonksiyon tanımlarken geri dönüş değerinin türü yazılmak zorundadır. Yazılmaz ise int kabul edilir kuralı geçerli değildir. 15- C’de enum türleri int kabul edilir. C++’da bütün enum türleri özgün türlerdir ve bir enum nesnesine yalnızca enum sabiti atanabilir. 16- C++’da fonksiyon daha yukarıda tanımlanmadıysa prototip zorunludur. 17- Aşağıdaki istisna durum C’de normal C++’da error’dür. char s[4] = “abcd”; C derleyicileri bu durumda ‘\0’ karakteri eklemezler, fakat C++’da bu durum error’dür. 157