Download as doc
Download as doc
You are on page 1of 125

İLERİ DERECEDE C++ PROGRAMLAMA

Tam grafiklerle desteklenmiş kaynak dosyaya[Linkleri görebilmek için üye


olmalısınız. Üye olmak için tıklayınız...]

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.

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.

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:
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) 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.
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 ç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 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

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;
//...
};

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

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;

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;

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’;

- 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:

i- İşlemi doğrudan yapacak global ya da üye operatör fonksiyonu araştırır.


Her ikisinin birden bulunması error oluşturur.
ii- 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.
iii- 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;
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;

CStringsı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”;
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 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.
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 DirProc(Shell *pShell);


void RenameProc(Shell *pShell);
void CopyProc(Shell *pShell);
void MoveProc(Shell *pShell);
void RemoveProc(Shell *pShell);
void XcopyProc(Shell *pShell);
void QuitProc(Shell *pShell);

#endif

/* shell.cpp */

#include <stdio.h>
#include <stdlib.h>
#include <iostream.h>
#include "general.h"
#include "cstring.h"
#include "shell.h"

CMDPROC cmdProc[] = {{"dir", DirProc},


{"rename", RenameProc},
{"copy", CopyProc},
{"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);
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 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()
{
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;

Seek() fonksiyonunun ikinci parametresi CFile::begin, CFile::end,


CFile::current olabilir. Ayrıca sınıfın SeekToBegin() ve SeekToEnd()
fonksiyonları da vardır. Bu fonksiyonlar dosya göstericisini başa ve sona çeker.

- 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)
m_bSuccess = FALSE;
//...
}
BOOL CFile: perator !() const
{
return !m_bSuccess;
}

zeet06
Açık Profil bilgileri

zeet06 - Daha fazla Mesajını bul

28.11.08, 15:03 #2

zeet06
Vefakar Üye

Üyelik tarihi: Jul 2008


Yaş: 23
Mesajlar: 482

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
ç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);

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;

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 {
//...
};

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.

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 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() /* error */
{
}
}

void X::Func() /* 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;

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:
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_aB 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 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>: perator <(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”);

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”);

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 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.

Sınıfın elemana erişim için kullanılan bir [] operatör fonksiyonu


vardır.

char &string: perator [](size_type idx);


char string: perator [](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.

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.)

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.
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 T *pointer;
typedef const T *const_pointer;
typedef T &reference;
typedef 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:

O(1) sabit
O(log n) logaritmik
O(n) doğrusal
O(n log n) doğrusal logaritmik
O( ) karesel
O( ) küpsel
O( ) ü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ığı ’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ı 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())
{
//...
}

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);
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 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.
X<int>::iterator iter;

zeet06
Açık Profil bilgileri

zeet06 - Daha fazla Mesajını bul

28.11.08,
#3
15:05
zeet06
Vefakar Üye

Üyelik tarihi: Jul


2008
Yaş: 23
Mesajlar: 482

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 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 <iostream>
#include <stdlib.h>
#include <list>
#include <vector>
#include <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");
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;
};

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>: isp()
{
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;
}

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
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#

C++

Sample s;
s = new Sample();
Object o;
o = s;
o.ToString();
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 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.

Shape
Shape

Circle

Triangle

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:
Başlık
Tür
Uzunluk
İçerik
Tür
Uzunluk
İç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


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.

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:

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.

Ç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.
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();
}
}

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 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:

1- Blok faaliyet alanı


2- Fonksiyon faaliyet alanı
3- Sınıf faaliyet alanı
4- 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 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()


{
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. 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();
}
}

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];

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();

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)


{
//...
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)
{
//...
}

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
- Operatör fonksiyonları
- Template işlemleri
- Default parametre kavramı
- Çoklu türetme
- Yerel sınıf nesneleri tanımlama
Diğer küçük özellikler

zeet06
Açık Profil bilgileri

zeet06 - Daha fazla Mesajını bul

28.11.08, 15:07 #4

zeet06
Vefakar Üye

Üyelik tarihi: Jul 2008


Yaş: 23
Mesajlar: 482

Ü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)();
//...
}

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; //error
X x;
(x.*pX)();

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; //normal
Y y;
(y.*pY)();

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:
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)();
}
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 <iostream>
#include <functional>
#include <algorithm>
#include <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());
x.push_back(new C());
x.push_back(new B());

for_each(x.begin(), x.end(), mem_fun(&A::Func));


}

Örnek 2:
#include <list>
#include <iostream>
#include <algorithm>
#include <functional>
#include <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 <iostream>
#include <string>
#include <list>
#include <algorithm>
#include <stdlib.h>
#include <functional>

using namespace std;


void main (void)
{
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;
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:
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.

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();

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.

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();

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:

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.

allocator Sınıfının Elemanları

Sınıfta aşağıdaki typedef isimleri bulunmak zorundadır.


typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T *pointer;
typedef const T *const_pointer;
typedef T &reference;
typedef const T &const_reference;
typedef T 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>: ointer p;

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 {
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>: ointer 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.

Ö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 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; //Normal biçim
int (a); //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)

Ö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;

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.

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.

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 dönüştürme
yerindedir ve dynamic_cast dönüştürmeyi yapar. Örneğin
şöyle bir türetme şeması olsun:

zeet06
Açık Profil bilgileri

zeet06 - Daha fazla Mesajını bul

28.11.08, 15:09 #5

zeet06
Vefakar Üye A

Üyelik tarihi: Jul 2008 C


Yaş: 23
Mesajlar: 482

E
Ş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 pAE 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 {

};

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;
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

21

16

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ı '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
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.

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.

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> x;
map<key, elem> y(v.begin(), v.end());
map<key, elem> 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);
}

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;
}

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);
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, string("ali")));
x.insert(make_pair(20, string("volkan")));
x.insert(make_pair(30, string("baris")));
x.insert(make_pair(40, 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)

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 <iostream>
#include <string>
#include <map>
#include <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;

for (iter2 = v.begin(); iter2 != v.end(); ++iter2)


cout << (*iter2).second;

return 0;
}

map sınıfından silme yapmak için erase() fonksiyonu kullanılabilir.


erase() fonksiyonlarının çeşitli versiyonları vardır.

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 {
//...
};

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;
}
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.

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)
{
//...
}

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.

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:
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 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.

Ç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

pA

pB

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() {}
};

C sınıfı türünden bir nesne tanımlandığında sanal fonksiyon tabloları


şöyle organize edilecektir:

//C_A’nın sanal fonksiyon tablosu


&A::FuncA()
&C::FuncC()

//C_B’nın sanal fonksiyon tablosu


&B::FuncB()
Ş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.

Çoklu türetmelerde taban sınıfın bazen iki kopyası bulunur. Bu durum


çoğu kez istenmeyen bir durumdur. Örneğin:
class A {
A

//...
};

class B : public A {
//...
C

};

class C : public A {
//...
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:

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:

class A {
//...
};

class B : virtual public A {


//...
};

class C : virtual public A {


//...
};
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:

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.
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;
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;
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.
SON
Anonim

You might also like