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

Benzer belgeler