Bu yazıda nesne yönelimli programlama konusuna devam edeceğiz. Yazı içerisinde miras alma, Referanslar ve İşaretçilerin OOP’de Kullanımı ve OOP’de Cast Kullanımı ( Nesne Sınıfı Değiştirme ) konularından bahsedeceğim. Ayrıca konunun anlatımını kuvvetlendirmek için pek çok kod örneğine de yazı içerisinde yer verilmiştir.
Miras Alma
Miras alma işlemi sınıflarımızda ortak olan alanları tekrar tekrar her sınıf için oluşturmadan kullanabilme konusunda bizlere yardımcı olur. Özelliklerin miras alındığı sınıf temel sınıf olarak adlandırılırken, özellikleri kullanan sınıflar türetilmiş sınıf adıyla anılır.
Örneğin bir oyun geliştiriyorsunuz ve oyunda kazma, çekiç, balta gibi çeşitli benzer işlerde aletler mevcut işe sap uzunluğu, dayanıklılık gibi özellikleri her alet için ayrı ayrı yazmak yerine sadece bir tane alet sınıfı oluşturarak bu özellikleri miras alabiliriz.
Türetilmiş sınıfları tanımlarken class türetilmiş_sınıfın_ismi : public temel_sınıfın_ismi { sınıf içerisinde yapılacak işlemler }; vb. şeklinde bir ifade yapısı kullanılır.
Aşağıdaki örnekte alet temel sınıf iken balta, kazma ve çekiç sınıfları işe türetilmiş sınıflardır. Örnekte balta sınıfında B18 isminde, kazma sınıfından K1 isminde ve çekiç sınıfından C10 isminde birer nesne oluşturulmuş ve bu nesneler çeşitli işlemlere tabi tutulmuştur. Aşağıdaki örnekte görülebileceği üzere türetilmiş sınıflar temel sınıfın özelliklerini kullanabilmektedir.
#include<iostream>
class alet {
public:
float uzunluk;
int kuvvet;
std::string isim;
std::string renk;
};
class balta : public alet {
public:
float kesicilik;
void kır(){
std::cout << "kesiciligi daha yuksek bir kirma eylemi basladi.";
}
};
class kazma : public alet {
public:
float kırıcılık;
void kır(){
std::cout << "kaya vb. maddelere karsi daha kuvvetli bir kirma eylemi basladi.";
}
};
class çekiç : public alet {
public:
float tamir_kuvveti;
void kır(){
std::cout << "kirma eylemi basladi.";
}
void tamir(){
std::cout << "tamir ediliyor...";
}
};
int main() {
balta B18;
kazma K1;
çekiç C10;
B18.isim = "balta alfa";
K1.uzunluk = 16;
C10.renk = "Mavi";
B18.kesicilik = 81;
B18.kır();
std::cout << std::endl;
K1.kır();
std::cout << std::endl;
C10.tamir_kuvveti = 18;
C10.kır();
std::cout << std::endl;
C10.tamir();
std::cout << std::endl;
std::cout << B18.isim << "\n";
std::cout << C10.renk << "\n";
std::cout << C10.tamir_kuvveti;
}
Yukarıdaki programın ekran çıktısı aşağıdaki gibidir: kesiciligi daha yuksek bir kirma eylemi basladi.
kaya vb. maddelere karsi daha kuvvetli bir kirma eylemi basladi.
kirma eylemi basladi.
tamir ediliyor…
balta alfa
Mavi
18
Miras almış bir sınıftan nesne oluşturulurken, Ram’de ilk önce miras alınan sınıf için yani temel sınıf için bir nesne oluşturulur ve daha sonra oluşturmak istediğimiz sınıf için yani türetilmiş sınıf için bir nesne oluşturulur. Eğer miras aldığımız sınıflarda başka sınıflardan miras alma yoluyla oluşturuldu ise, miras veren ilk temel sınıftan başlayarak her miras alma işlemi için Ram’de birer nesne oluşturulacaktır.
Aşağıdaki örnekte nesnelerin oluşturulma ve kaldırılma sıraları yapıcı ve yıkıcı metotlar vasıtası ile gösterilmiştir. Görüldüğü üzere en son sırada oluşturulan ve ilk sırada kaldırılan nesne cccc sınıfı vasıtası ile oluşturulan nesne olurken, ilk sırada oluşturulan ve en son sırada kaldırılan nesne, kodumuz içerisindeki en temel sınıf olan aaaa sınıfı vasıtası ile oluşturulan nesne olmuştur.
#include<iostream>
class aaaa {
public:
aaaa() {
std::cout << "aaaa sinifindan bir nesne olusturuldu.";
}
~aaaa() {
std::cout << std::endl << "aaaa sinifindan bir nesne kaldirildi.";
}
};
class bbbb : public aaaa {
public:
bbbb() {
std::cout << std::endl << "bbbb sinifindan bir nesne olusturuldu.";
}
~bbbb() {
std::cout << std::endl << "bbbb sinifindan bir nesne kaldirildi.";
}
};
class cccc : public bbbb {
public:
cccc() {
std::cout << std::endl << "cccc sinifindan bir nesne olusturuldu.";
}
~cccc() {
std::cout << std::endl << "cccc sinifindan bir nesne kaldirildi.";
}
};
void main() {
cccc nesne1;
}
Yukarıdaki programın ekran çıktısı aşağıdaki gibidir: aaaa sinifindan bir nesne olusturuldu.
bbbb sinifindan bir nesne olusturuldu.
cccc sinifindan bir nesne olusturuldu.
cccc sinifindan bir nesne kaldirildi.
bbbb sinifindan bir nesne kaldirildi.
aaaa sinifindan bir nesne kaldirildi.
Sınıf içerisinde protected olarak tanımlanan özellikler miras alan sınıflar içerisinde kullanılabilir ancak nesneyi oluşturan kullanıcı tarafından nesne özelliği çağırma yöntemi ile çağrılamaz. Örneğin protected olarak tanımlanmış menzil özelliği, miras alan sınıf içerisinde kullanılabilir iken, nesneyi oluşturan kişi tarafından çağrılarak kullanılamaz.
Aşağıda sınıf oluşturma ve nesne kullanımı ile alakalı bir örnek verilmiştir. Eğer aşağıdaki programın ana fonksiyonunda vuruş1.saldırı_adı = “Ateşİmparatotu“; şeklinde bir özellik kullanımı yapmaya çalışırsanız, saldırı_adı korumalı bir özellik olduğu için Hata C2248 ‘engel::saldırı_adı’: protected üye (‘engel’ sınıfında bildirimi yapılan) erişilebilir değil şeklinde bir hata alır ve programı çalıştıramazsınız. Ama aşağıdaki örnekte saldırı sınıfının içerisinde isim fonksiyonu vasıtası ile isimlendirme yaptığımız gibi, korumalı (protected) özelliklerden türetilmiş sınıf içerisinde faydalanabilirsiniz.
#include<iostream>
class engel {
protected:
std::string saldırı_adı;
public:
int kalkan1( int s ) {
return s - 5;
}
int kalkan2( int s ) {
return s - 12;
}
int kılıç_blok( int s ) {
return s - 15;
}
};
class saldırı : public engel {
public:
int s;
void isim() {
std::string a;
std::cout << "suanda saldiri sinifi vasitasi ile saldiriniza isim veriyorsuz:";
std::cin >> a;
saldırı_adı = a;
}
void isim_yaz() {
std::cout << "saldiri ismi:";
std::cout << saldırı_adı;
}
int saldır() {
std::cout << "saldiri kuvveti:";
std::cin >> s;
return s;
}
int vur(int s) {
std::cout << "dusmani etkileyen kuvvet:";
engel vuruş;
int a = vuruş.kılıç_blok( s );
return a;
}
};
void main() {
saldırı vuruş1;
vuruş1.isim();
vuruş1.isim_yaz();
std::cout << std::endl;
std::cout << vuruş1.vur(vuruş1.saldır());
}
Yukarıdaki kodun örnek bir kullanımına aşağıda yer verilmiştir: suanda saldiri sinifi vasitasi ile saldiriniza isim veriyorsuz:Ateşİmparatotu saldiri ismi:Ateşİmparatotu
saldiri kuvveti:18
dusmani etkileyen kuvvet:3
Referanslar ve İşaretçilerin OOP’de Kullanımı
Öncelikle referans bir değişkene farklı isimler vasıtası ile erişmemizi sağlayan bir yapıdır. Örneğin adı a1 olan int veri tipindeki bir değişken için int& refa1 = a1; ifadesini kullanarak refa1’i a1’in referansı haline getirebiliriz. Aynı şekilde Alet sınıfından türetilmiş A1 ismindeki bir nesnenin Alet& ReferansA1 = A1; gibi bir ifade vasıtası ile referansını oluşturabiliriz. Referanslar programda nesnenin yerine kullanıldığında, normalde olduğu gibi referans isim ile özelik ismi arasında nokta koyularak kullanılabilir. Ancak işaretçi kullanımında durum farklıdır. İşaretçileri nesnelerin yerine kullanırken, işaretçi ismi ile özellik ismi arasına nokta yerine -> yerleştirilir. Alet sınıfından türetilmiş A1 ismindeki bir nesnenin işaretçisini, Alet*ptrA1=&A1; ve benzeri bir ifade yardımı ile oluşturabiliriz, bu örnekte ptrA1 değişkeni A1 nenesinin bellek adresini temsil etmektedir.
Bir fonksiyona belirli bir sınıfa ait nesnenin referansını göndereceğimizi belirttiğimizde, ilgili fonksiyona herhangi bir referans oluşturmadan doğrudan nesnenin orijinalini de gönderebiliriz. Ancak bir fonksiyona ilgili sınıfa ait nesnenin işaretçisini göndereceğimizi belirttiğimizde ilgili fonksiyona ilgili sınıfa ait nesnenin işaretçisini göndermemiz gerekir, bu şartı nesne isminin başına & koyarak da sağlayabiliriz, iki şekilde de göndereceğimiz veri bir işaretçi yani bellek adresi olacaktır.
Aşağıdaki örnekte yukarıdaki konunun örnek bir uygulaması ele alınmıştır. Örnek içerisinde referans ve işaretçi ile birlikte özellik kullanımına örnek vermek amacıyla, üç değişken ile de isimlendirme özelliği kullanılmıştır. Aşağıdaki fonksiyonda Alet& ile ifade edilen kısım referans, Alet* ile ifade edilen kısım işe işaretçi beklemekte olup, RA ve İA fonksiyon içerisinde ilgili nesneleri temsil edecektir. Aşağıdaki örnekte İA nesnenin işaretçisini temsil edeceği için özellik kullanırken nokta yerine -> yazılmalıdır. Aşağıdaki kod ile ekrana yan yana sekiz kere balta yazılacaktır.
#include<iostream>
class Alet {
public:
std::string isim;
};
void yaz( Alet& RA, Alet* İA ) {
std::cout << RA.isim;
std::cout << İA->isim;
}
void main() {
Alet alet1;
alet1.isim = "balta";
Alet& refAlet = alet1;
refAlet.isim = "balta";
Alet* İşaretçiAlet = &alet1;
İşaretçiAlet->isim = "balta";
yaz(alet1, &alet1);
yaz(refAlet, İşaretçiAlet);
yaz(alet1, İşaretçiAlet);
yaz(refAlet, &alet1);
}
OOP’de Cast Kullanımı ( Nesne Sınıfı Değiştirme )
Cast yapısı veri tipleri için belli bir veri tipindeki verinin veya değişkenin veri tipini başka bir veri tipiyle değiştirmeye yaramakta idi. Nesne yönelimli programlamada da benzer bir dönüşüm gerçekleştirmemiz mümkündür. Bu özelliğin kullanılabilmesi için nesneler arasında bir ilişki bulunması gerekmektedir. Aralarında miras alma ilişkisi bulunan nesneler bir biri arasında dönüşüm gerçekleştirebilirler. Örneğin alet sınıfından türetilmiş çekiç sınıfından üretilen bir nesne alet nesnesine dönüştürülebilirken, alet sınıfından üretilen bir nesnede çekiç nesnesine dönüştürülebilir. Bu kullanım şekilleri sırasıyla yukarı dönüşüm (çekiç -> alet ) ve aşağı dönüşüm (alet -> çekiç) olarak adlandırılır. Aşağı dönüşüm (alet -> çekiç) gerçekleştirirken bir hayli dikkatli olmanız gerekmektedir çünkü yanlış bir verinin kullanılma olasılığı yüksektir. Bu durumun sebebini daha iyi anlayabilmek için türetilmiş sınıfın temel sınıfı kapsar yapıda olduğunu düşünebilirsiniz.
Konu içerisinde bahsetmiş olduğum dönüşüm kavramı, tam bir dönüştürme işleminden ziyade, derleyicinin nesneyi ele alma biçimini değiştirmesi olarak düşünülebilir. C++’da bu işlem belirli bir ifade yardımıyla değil, doğrudan bizim hafıza alanlarına erişebilme kabiliyetimizi kullanmamız sayesinde mümkün olabilmektedir.
Cast yapısı, programlarımızda aynı sınıftan türetilmiş sınıfların ortak alanlarını işlemek gibi görevler de isimizi bir hayli kolaylaştıracaktır. Mesela aynı sınıftan türetilmiş farklı sınıflar vasıtası ile üretilen nesneler için ortak alanları işleyecek ayrı ayrı fonksiyonlar yazmak yerine, yukarı dönüştürme işlemi vasıtası ile bu alanları ortak bir fonksiyon aracılığı ile de işleyebilirsiniz.
Yukarı dönüştürme ( türetilmiş sınıf -> temel sınıf )
Aşağıda bir Yukarı dönüştürme ( çekiç -> alet ) örneği yer almaktadır. Bu işlemi alet sınıfından türetilmiş çekiç sınıfı vasıtası ile üretilen C1 nesnesinin bellek adresini alet sınıfı vasıtası ile üretilmiş bir nesnenin bellek adresiymiş gibi bir değişkene atama vasıtası ile gerçekleştirdik.
Bu örnekte ptrA alet sınıfından üretilmiş bir nesnenin bellek adresi gibi ele alınacağı için nesnenin sadece alet sınıfına dahil olan özellikleri kullanılabilir olacaktır. Bu örnekte ptrA vasıtası ile gerçekleştirmiş olduğumuz tüm işlemler, oluşturmuş olduğumuz C1 nesnesi üzerinde gerçekleşecektir. Çünkü bu yöntem vasıtasıyla gerçekleştirmiş olduğumuz işlem aslında, nesnenin belli özelliklerine hafıza erişimi sağlama ve ilgili hafızayı işleme durumudur.
ptrA nesnesinin hız özelliğine erişimi yoktur, çünkü alet sınıfından üretilmiş bir nesne gibi ele alınacaktır. Aşağıdaki programla ekrana alt alta iki defa 1 numara10 yazdırılacaktır.
#include<iostream>
class alet {
public:
std::string isim;
int kuvvet;
};
class çekiç : public alet {
public:
int hız;
};
void main() {
çekiç C1;
alet* ptrA = &C1;
ptrA->isim = "1 numara";
ptrA->kuvvet = 10;
std::cout << C1.isim << C1.kuvvet;
std::cout << std::endl;
std::cout << ptrA->isim << ptrA->kuvvet;
}
Aşağı dönüştürme ( temel sınıf -> türetilmiş sınıf )
Aşağıda bir aşağı dönüştürme ( alet -> çekiç ) örneği yer almaktadır. Aşağıdaki işaretçi dönüşümünün normalde hata vermesi gerekir anacak (çekiç*) ifadesi yardımıyla işlemin gerçekleştirilmeye devam etmesini sağlıyoruz. (sınıf adı*) ifade yapısı yardımı ile programın nesnenin işaretçisinin eşitleneceği ilgili sınıftan nesnenin sınıfının farklı oluşunu görmezden gelerek, sanki aynı sınıf içinde bir eşitleme işlemi yapılıyormuş gibi devam etmesini sağlıyoruz.
Belirli bir sınıfa dahil olan veriler bellekte alt alta yerleştirilir ve program hangi veriye hangi bellek adresi vasıtası ile ulaşacağını bu şekilde bilir. Eğer hafız adresini aldığınız nesne bu işlem için uygun olmayan, yani devamında programınızla alakasız verileri de yanlışlıkla sisteminizde kullanmanıza sebep olabilecek bir bellek alanı işe, bu durum programınızın hatalı çalışmasına sebep olabilir.
Aşağıdaki örnekte hatasız çalışması beklenen bir aşağı dönüştürme işlemi uygulanmıştır. Bu işlemi güvenli bir şekilde yanlış bellek adreslerine erişmeden gerçekleştirmenin yollarından birisi, yukardaki gibi türetilmiş sınıfın bellek bölgesini de devamında barındıran bir nesne ile çalışmaktır. Aşağıdaki program ile ekrana sırasıyla alt alta 1 numara107, 1 numara10, 1 numara107 yazdırılacaktır.
#include<iostream>
class alet {
public:
std::string isim;
int kuvvet;
};
class çekiç : public alet {
public:
int hız;
};
void main() {
çekiç C1;
alet* ptrA = &C1;
çekiç* ptrC = (çekiç*) ptrA;
ptrC->isim = "1 numara";
ptrC->kuvvet = 10;
ptrC->hız = 7;
std::cout << C1.isim << C1.kuvvet << C1.hız;
std::cout << std::endl;
std::cout << ptrA->isim << ptrA->kuvvet ;
std::cout << std::endl;
std::cout << ptrC->isim << ptrC->kuvvet << ptrC->hız;
}